Создание web приложения на PHP с иcпользованием Firebird и Laravel

firebird-logo Привет Хабр!

В прошлой статье я рассказывал о пакете для поддержки СУБД Firebird в фреймворке Laravel. На этот раз мы рассмотрим процесс создания web приложения с использованием СУБД Firebird на языке PHP с использованием Laravel.

Обзор драйверов для работы с Firebird


В PHP есть два драйвера для работы с СУБД Firebird:
  • Расширение Firebird/Interbase (ibase_ функции);
  • PDO драйвер для Firebird.

Обзор расширения Firebird/Interbase


Расширение Firebird/Interbase появилось раньше и является наиболее проверенным. Для установки расширения Firebird/Interbase в конфигурационном файле php.ini необходимо раскомментировать строку
extension=php_interbase.dll

или для UNIX подобных систем строку
extension=php_interbase.so

Это расширение требует, чтобы у вас была установлена клиентская библиотека fbclient.dll/gds32.dll (для UNIX подобных систем fbclient.so) соответствующей разрядности.
Замечание для пользователей Win32/Win64

Для работы этого расширения системной переменной Windows PATH должны быть доступны DLL-файлы fbclient.dll или gds32.dll. Хотя копирование DLL-файлов из директории PHP в системную папку Windows также решает проблему (потому что системная директория по умолчанию находится в переменной PATH), это не рекомендуется. Этому расширению требуются следующие файлы в переменной PATH: fbclient.dll или gds32.dll.


В Linux это расширение в зависимости от дистрибутива можно установить одной из следующих команд (необходимо уточнить поддерживаемые версии, возможно, необходимо подключить сторонний репозиторий):
apt-get install php5-firebird

rpm –ihv php5-firebird

yum install php70w-interbase

zypper install php5-firebird

Это расширение использует процедурный подход к написанию программ. Функции с префиксом ibase_ могут возвращать или принимать в качестве одного из параметров идентификатор соединения, транзакции, подготовленного запроса или курсора (результат SELECT запроса). Этот идентификатор имеет тип resource. Все выделенные ресурсы необходимо освобождать, как только они больше не требуются. Я не буду описывать каждую из функций подробно, вы можете посмотреть их описание по ссылке, вместо этого приведу несколько небольших примеров с комментариями.
$db = 'localhost:example';
$username = 'SYSDBA';
$password = 'masterkey';

// Подключение к БД
$dbh = ibase_connect($db, $username, $password);
$sql = 'SELECT login, email FROM users';
// Выполняем запрос
$rc = ibase_query($dbh, $sql);
// Получаем результат построчно в виде объекта
while ($row = ibase_fetch_object($rc)) {
    echo $row->email, "\n";
}
// Освобождаем хэндл связанный с результатом запроса
ibase_free_result($rc);
// Освобождаем хэндл связанный с подключением
ibase_close($dbh);

Вместо функции ibase_connect вы можете применять функцию ibase_pconnect, которая создаёт так называемые постоянные соединения. В этом случае при вызове ibase_close соединение не закрывается, все связанные с ней ресурсы освобождаются, транзакция по умолчанию подтверждается, другие виды транзакций откатываются. Такое соединение может быть использовано повторно в другой сессии, если параметры подключения совпадают. В некоторых случаях постоянные соединения могут значительно повысить эффективность вашего веб приложения. Это особенно заметно, если затраты на установку соединения велики. Они позволяют дочернему процессу на протяжении всего жизненного цикла использовать одно и то же соединение вместо того, чтобы создавать его при обработке каждой страницы, которая взаимодействует с SQL-сервером. Этим постоянные соединения напоминают работу с пулом соединений. Подробнее о постоянных соединениях вы может прочитать по ссылке.
Внимание!

Многие ibase функции позволяют не передавать в них идентификатор соединения (транзакции, подготовленного запроса). В этом случае эти функции используют идентификатор последнего установленного соединения (начатой транзакции). Я не рекомендую так делать, в особенности, если ваше веб приложение может использовать более одного подключения.


Функция ibase_query выполняет SQL запрос и возвращает идентификатор результата или true, если запрос не возвращает набор данных. Эта функция помимо идентификатора подключения (транзакции) и текста SQL запроса может принимать переменное число аргументов в качестве значений параметров SQL запроса. В этом случае наш пример выглядит следующим образом:
$sql = 'SELECT login, email FROM users WHERE id=?';
$id = 1;  
// Выполняем запрос
$rc = ibase_query($dbh, $sql, $id);
// Получаем результат построчно в виде объекта
if ($row = ibase_fetch_object($rc)) {
    echo $row->email, "\n";
}
// Освобождаем хэндл связанный с результатом запроса
ibase_free_result($rc);

Очень часто параметризованные запросы используются многократно с различным набором значений параметров, в этом случае для повышения производительности рекомендуется использовать подготовленные запросы. В этом случае сначала необходимо сначала получить идентификатор подготовленного запроса с помощью функции ibase_prepare, а затем выполнять подготовленный запрос с помощью функции ibase_execute.
$sql = 'SELECT login, email FROM users WHERE id=?';
// Подготавливаем запрос
$sth = ibase_prepare($dbh, $sql);
$id = 1;  
// Выполняем запрос
$rc = ibase_execute($sth, $id);
// Получаем результат построчно в виде объекта
if ($row = ibase_fetch_object($rc)) {
    echo $row->email, "\n";
}
// Освобождаем хэндл связанный с результатом запроса
ibase_free_result($rc);
// Освобождаем подготовленный запрос
ibase_free_query($sth);

Подготовленные запросы гораздо чаще используются, когда необходима массовая заливка данных.
$sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
// Подготавливаем запрос
$sth = ibase_prepare($dbh, $sql);
$users = [["user1", "user1@gmail.com"], ["user2", "user2@gmail.com"]];  
// Выполняем запрос
foreach ($users as $user)) {
  ibase_execute($sth, $user[0], $user[1]);
}
// Освобождаем подготовленный запрос
ibase_free_query($sth);

По последнему примеру можно увидеть один из недостатков этого расширения, а именно, функции с переменным числом аргументов не очень удобны для параметризованных запросов. Этот недостаток проявляется особенно ярко, если вы пытаетесь написать универсальный класс для исполнения любых запросов. Гораздо удобнее было бы, если параметры можно было передавать одним массивом. Конечно, существуют обходные пути вроде вот такого:
function fb_execute ($stmt, $data)
{
   if (!is_array($data))
       return ibase_execute($stmt, $data);
   array_unshift($data, $stmt); 
   $rc = call_user_func_array('ibase_execute', $data);
   return $rc;
}

Расширение Firebird/Interbase не работает с именованными параметрами запроса. По умолчанию расширение Firebird/Interbase автоматически подтверждает транзакцию после выполнения каждого SQL запроса, если вам необходимо явное управление транзакциями, то необходимо стартовать транзакцию с помощью функции ibase_trans. Если параметры транзакции не указаны, то транзакция будет начата с параметрами IBASE_WRITE | IBASE_CONCURRENCY | IBASE_WAIT. Описание констант для задания параметров транзакции можно найти по ссылке php.net/manual/ru/ibase.constants.php. Транзакцию необходимо завершать с помощью метода ibase_commit или ibase_rollback. Если вместо этих функций использовать функции ibase_commit_ret или ibase_rollback_ret, то транзакция будет завершаться как COMMIT RETAIN или ROLLBACK RETAIN.
Замечание.

Умолчательные параметры транзакции подходят для большинства случаев, и менять их параметры требуется очень редко. Дело в том что соединение с базой данных, как и все связанные с ним ресурсы существуют максимум до конца работы PHP скрипта. Даже если вы используете постоянные соединения, то все связанные ресурсы будут освобождены после вызова функции ibase_close. Несмотря на сказанное, настоятельно рекомендую завершать все выделенные ресурсы явно, вызывая соответствующие ibase_ функции.

Пользоваться функциями ibase_commit_ret и ibase_rollback_ret настоятельно не рекомендую, так как это не имеет смысла. COMMIT RETAIN и ROLLBACK RETAIN были введены для того, чтобы в настольных приложениях сохранять открытыми курсоры при завершении транзакции.


$sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
// Подготавливаем запрос
$sth = ibase_prepare($dbh, $sql);
$users = [["user1", "user1@gmail.com"], ["user2", "user2@gmail.com"]]; 
$trh = ibase_trans($dbh, IBASE_WRITE | IBASE_CONCURRENCY | IBASE_WAIT); 
try {
  // Выполняем запрос
  foreach ($users as $user)) {
    ibase_execute($sth, $user[0], $user[1]);
    // Если произошла ошибка, бросаем исключение
    $err_msg = ibase_errmsg();
    if ($err_msg)
      throw new \Exception($err_msg);
  }
  ibase_commit($trh);
}
catch(\Exception $e) {
  ibase_rollback($trh);
  echo $e->getMessage();
}
// Освобождаем подготовленный запрос
ibase_free_query($sth);

Внимание!

ibase функции не бросают исключение в случае возникновения ошибки. Потенциально ошибка может возникнуть поле вызова любой ibase функции. Текст ошибки можно узнать с помощью функции ibase_errmsg. Код ошибки можно получить с помощью функции ibase_errcode.


Расширение Firebird/Interbase позволяет взаимодействовать с сервером Firebird не только посредством SQL запросов, но и используя Service API (см. функции ibase_service_attach, ibase_service_detach, ibase_server_info, ibase_maintain_db, ibase_db_info, ibase_backup, ibase_restore). Эти функции позволяют получить информацию о сервере Firebird, сделать резервное копирование, восстановление или получить статистику. Эта функциональность требуется в основном для администрирования БД, поэтому мы не будем рассматривать её подробно.

Расширение Firebird/Interbase так же поддерживает работу с событиями Firebird (см. функции ibase_set_event_handler, ibase_free_event_handler, ibase_wait_event).

Обзор расширения PDO (драйвер Firebird)


Расширение PDO предоставляет обобщённый интерфейс для доступа к различным типам БД. Каждый драйвер базы данных, в котором реализован этот интерфейс, может представить специфичный для базы данных функционал в виде стандартных функций расширения.

PDO и все основные драйверы внедрены в PHP как загружаемые модули. Чтобы их использовать, требуется их просто включить, отредактировав файл php.ini следующим образом:

extension=php_pdo.dll

Замечание

Этот шаг необязателен для версий PHP 5.3 и выше, так как для работы PDO больше не требуются DLL.


Далее нужно выбрать DLL конкретных баз данных и либо загружать их во время выполнения функцией dl (), либо включить их в php.ini после php_pdo.dll. Например:
extension=php_pdo.dll
extension=php_pdo_firebird.dll

Эти DLL должны лежать в директории extension_dir. Драйвер pdo_firebird требует, чтобы у вас была установлена клиентская библиотека fbclient.dll/gds32.dll (для UNIX подобных систем fbclient.so) соответствующей разрядности.

В Linux это расширение в зависимости от дистрибутива можно установить одной из следующих команд (необходимо уточнить поддерживаемые версии, возможно, необходимо подключить сторонний репозиторий):

apt-get install php5-firebird

rpm –ihv php5-firebird

yum install php70w-firebird

zypper install php5-firebird

PDO использует объектно-ориентированный подход к написанию программ. Какой именно драйвер будет использоваться в PDO, зависит от строки подключения, называемой так же DSN (Data Source Name). DSN состоит из префикса, который и определяет тип базы данных, и набора параметров в виде <ключ>=<значение>, разделённых точкой с запятой »;». Допустимый набор параметров зависит от типа базы данных. Для работы с Firebird строка подключения должна начинаться с префикса firebird: и иметь вид, описанный в документации в разделе PDO_FIREBIRD DSN.

Соединения устанавливаются автоматически при создании объекта PDO от его базового класса. Конструктор класса принимает аргументы для задания источника данных (DSN), а также необязательные имя пользователя и пароль (если есть). Четвёртым аргументом можно передать массив специфичных для драйвера настроек подключения в формате ключ=>значение.

$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
  // Подключение к БД
  $dbh = new \PDO($dsn, $username, $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
  $sql = 'SELECT login, email FROM users';
  // Выполняем запрос
  $query = $dbh->query($sql);
  // Получаем результат построчно в виде объекта
  while ($row = $query->fetch(\PDO::FETCH_OBJ)) {
    echo $row->email, "\n";
  }
  $query->closeCursor(); // Закрываем курсор
} catch (\PDOException $e) {
  echo $e->getMessage();
}

Установив свойство \PDO: ATTR_ERRMODE в значение \PDO: ERRMODE_EXCEPTION, мы установили режим, при котором любая ошибка, в том числе и ошибка при подключении к БД, будет возбуждать исключение \PDOException. Работать в таком режиме гораздо удобнее, чем проверять наличие ошибки после каждого вызова ibase_ функций.
Для того чтобы PDO использовал постоянные соединения необходимо в конструктор PDO в массиве свойств передать PDO: ATTR_PERSISTENT => true.

Метод query выполняет SQL запрос и возвращает результирующий набор в виде объекта \PDOStatement. В этот метод помимо SQL запросы вы можете передать способ возвращения значений при фетче. Это может быть столбец, экземпляр заданного класса, объект. Различные способы вызова вы можете посмотреть по ссылке http://php.net/manual/ru/pdo.query.php.

Если необходимо выполнить SQL запрос, не возвращающий набор данных, то вы можете воспользоваться методом exec, который возвращает количество задействованных строк. Этот метод не обеспечивает выполнение подготовленных запросов.

Если в запросе используются параметры, то необходимо пользоваться подготовленными запросами. В этом случае вместо метода query необходимо вызвать метод prepare. Этот метод возвращает объект класса \PDOStatement, который инкапсулирует в себе методы для работы с подготовленными запросами и их результатами. Для выполнения запроса необходимо вызвать метод execute, который может принимать в качестве аргумента массив с именованными или неименованными параметрами. Результат выполнения селективного запроса можно получить с помощью методов fetch, fetchAll, fetchColumn, fetchObject. Методы fetch и fetchAll могут возвращать результаты в различном виде: ассоциативный массив, объект или экземпляр определённого класса. Последнее довольно часто используется в MVC паттерне при работе с моделями.

$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
  // Подключение к БД
  $dbh = new \PDO($dsn, $username, $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
  $sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
  $users = [
    ["user1", "user1@gmail.com"], 
    ["user2", "user2@gmail.com"]
  ]; 

  // Подготавливаем запрос
  $query = $dbh->prepare($sql);
  // Выполняем запрос
  foreach ($users as $user)) {
      $query->execute($user);
  }
} catch (\PDOException $e) {
  echo $e->getMessage();
}

Пример использования именованных параметров.
$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
  // Подключение к БД
  $dbh = new \PDO($dsn, $username, $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
  $sql = 'INSERT INTO users(login, email) VALUES(:login, :email)';
     $users = [
    [":login" => "user1", ":email" => "user1@gmail.com"], 
    [":login" => "user2", ":email" => "user2@gmail.com"]
  ]; 
  // Подготавливаем запрос
  $query = $dbh->prepare($sql);
  // Выполняем запрос
  foreach ($users as $user)) {
      $query->execute($user);
  }
} catch (\PDOException $e) {
  echo $e->getMessage();
}

Замечание

Для поддержки именованных параметров PDO производит предобработку запроса и заменяет параметры вида : paramname на »?», сохраняя при этом массив соответствия между именем параметра и номерами его позиций в запросе. По этой причине оператор EXECUTE BLOCK не будет работать, если внутри него используются переменные маркированные двоеточием. На данный момент нет никакой возможности заставить работать PDO с оператором EXECUTE BLOCK иначе, например, задать альтернативный префикс параметров, как это сделано в некоторых компонентах доступа.


Передать параметры в запрос можно и другим способом, используя так называемое связывание. Метод bindValue привязывает значение к именованному или неименованному параметру. Метод bindParam привязывает переменную к именованному или неименованному параметру. Последний метод особенно полезен для хранимых процедур, которые возвращают значение через OUT или IN OUT параметр (в Firebird механизм возврата значений из хранимых процедур другой).
$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
  // Подключение к БД
  $dbh = new \PDO($dsn, $username, $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
  $sql = 'INSERT INTO users(login, email) VALUES(:login, :email)';
  $users = [
    ["user1", "user1@gmail.com"], 
    ["user2", "user2@gmail.com"]
  ]; 

  // Подготавливаем запрос
  $query = $dbh->prepare($sql);
  // Выполняем запрос
  foreach ($users as $user)) {
    $query->bindValue(":login", $user[0]);
    $query->bindValue(":email", $user[1]);
    $query->execute();
  }
} catch (\PDOException $e) {
  echo $e->getMessage();
}

Внимание

Нумерация неименованных параметров в методах bindParam и bindValue начинается с 1.


$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
  // Подключение к БД
  $dbh = new \PDO($dsn, $username, $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
  $sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
  $users = [
    ["user1", "user1@gmail.com"], 
    ["user2", "user2@gmail.com"]
  ]; 

  // Подготавливаем запрос
  $query = $dbh->prepare($sql);
  // Выполняем запрос
  foreach ($users as $user)) {
    $query->bindValue(1, $user[0]);
    $query->bindValue(2, $user[1]);
    $query->execute();
  }
} catch (\PDOException $e) {
  echo $e->getMessage();
}

По умолчанию PDO автоматически подтверждает транзакцию после выполнения каждого SQL запроса, если вам необходимо явное управление транзакциями, то необходимо стартовать транзакцию с помощью метода \PDO: beginTransaction. По умолчанию транзакция стартует с параметрами CONCURRENCY | WAIT | READ_WRITE. Завершить транзакцию можно методом \PDO: commit или \PDO: rollback.
$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
  // Подключение к БД
  $dbh = new \PDO($dsn, $username, $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
  // Стартуем транзакцию для обеспечения согласованности между запросами
  $dbh->beginTransaction();
  // Получаем пользователей из одной таблицы
  $users_stmt = $dbh->prepare('SELECT login, email FROM old_users');
  $users_stmt->execute();  
  $users = $users_stmt->fetchAll(\PDO::FETCH_OBJECT);
  $users_stmt->closeCursor();
   
  // И переносим их в другую
  $sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
  // Подготавливаем запрос
  $query = $dbh->prepare($sql);
  // Выполняем запрос
  foreach ($users as $user)) {
    $query->bindValue(1, $user->LOGIN);
    $query->bindValue(2, $user->EMAIL]);
    $query->execute();
  }
  // Подтверждаем транзакцию
  $dbh->commit();
} catch (\PDOException $e) {
  // Если соединение произошло и транзакция стартовала, откатываем её
  if ($dbh &&  $dbh->inTransaction())
    $dbh->rollback();
  echo $e->getMessage();
}

К сожалению метод beginTransaction не предоставляет возможности изменить параметры транзакции, однако вы можете сделать хитрый трюк, задав параметры транзакции оператором SET TRANSACTION.
$dbh = new \PDO($dsn, $username, $password);
$dbh->setAttribute(\PDO::ATTR_AUTOCOMMIT, false);
$dbh->exec("SET TRANSACTION READ ONLY ISOLATION LEVEL READ COMMITTED NO WAIT");
// Выполняем действия в транзакции
// ….
$dbh->exec("COMMIT");
$dbh->setAttribute(\PDO::ATTR_AUTOCOMMIT, true);

Ниже представлена сводная таблица возможностей различных драйверов для работы с Firebird.
Возможность Расширение Firebird/Interbase PDO
Парадигма программирования Функциональная Объектно-ориентированная
Поддерживаемые БД Firebird, Interbase, Yaffil и другие клоны Interbase. Любая БД, для которой существует PDO драйвер, в том числе Firebird.
Работа с параметрами запросов Только неименованные параметры, работать не очень удобно, поскольку используется функция с переменным числом аргументов. Есть возможность работать как с именованными, так и неименованными параметрами. Работать очень удобно, однако некоторые возможности Firebird (оператор EXECUTE BLOCK) не работают.
Обработка ошибок Проверка результата функций ibase_errmsg, ibase_errcode. Ошибка может произойти после вызова любой ibase функции при этом исключение не будет возбуждено. Есть возможность установить режим, при котором любая ошибка приведёт к возбуждению исключения.
Управление транзакциями Даёт возможность задать параметры транзакции. Не даёт возможность задать параметры транзакции. Есть обходной путь через выполнение оператора SET TRANSACTION.
Специфичные возможности Interbase/Firebird Есть возможность работать с расширениями Service API (backup, restore, получение статистики и т.д.), а также с событиями базы данных. Не позволяет использовать специфичные возможности, с которыми невозможно работать, используя SQL.

Из приведённой таблицы видно, что большинству фреймворков гораздо удобнее пользоваться PDO.

Выбор фреймворка для построения WEB приложения


Небольшие web сайты можно писать, не используя паттерн MVC. Однако чем больше становится ваш сайт, тем сложнее его поддерживать, особенно если над ним работает не один человек. Поэтому при разработке нашего web приложения сразу договоримся об использовании этого паттерна.

Итак, мы решили использовать паттерн MVC. Однако написание приложение с использованием этого паттерна не такая простая задача как кажется, особенно если мы не пользуемся сторонними библиотеками. Если всё писать самому, то необходимо решить множество задач: автозагрузка файлов .php, включающих определение классов, маршрутизация и др. Для решения этих задач было создано большое количество фреймворков, например Yii, Laravel, Symphony, Kohana и многие другие. Лично мне нравится Laravel, поэтому далее я буду описывать создание приложения с использованием этого фреймворка.

Установка Laravel и создание проекта


Прежде чем устанавливать Laravel вам необходимо убедится, что ваше системное окружение соответствует требованиям.
  • PHP >= 5.5.9
  • PDO расширение для PHP (для версии 5.1+)
  • MCrypt расширение для PHP (для версии 5.0)
  • OpenSSL (расширение для PHP)
  • Mbstring (расширение для PHP)
  • Tokenizer (расширение для PHP)

Laravel использует Composer для управления зависимостями. Поэтому сначала установите Composer, а затем Laravel.

Самый простой способ установить composer под windows — это скачать и запустить инсталлятор Composer-Setup.exe. Инсталлятор установит Composer и настроит PATH, так что вы можете вызвать Composer из любой директории в командной строке.

Если необходимо установить Composer вручную, то необходимо запустить

Скрипт уcтановки Composer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('SHA384', 'composer-setup.php') === 'aa96f26c2b67226a324c27919f1eb05f21c248b987e6195cad9690d5c1ff713d53020a02ac8c217dbf90a7eacc9d141d') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"


Этот скрипт осуществляет следующие действия:
  • Скачивает инсталлятор в текущую директорию
  • Проверяет инсталлятор с помощью SHA-384
  • Запускает скрипт инсталляции
  • Удаляет скрипт инсталляции

После запуска этого скрипта у вас появится файл composer.phar (phar — это архив) — по сути это PHP скрипт, который может принимать несколько команд (install, update, …) и умеет скачивать и распаковывать библиотеки. Если вы работаете под windows, то вы можете облегчить себе задачу, создав файл composer.bat и поместив его в PATH. Для этого необходимо выполнить команду
echo @php "%~dp0composer.phar" %*>composer.bat

Подробнее об установке composer смотри здесь.

Теперь устанавливаем сам Laravel:

composer global require "laravel/installer"

Если установка прошла успешно, то приступаем к созданию каркаса проекта.
laravel new fbexample

Ждём завершения, после чего у нас будет создан каркас проекта. Описание структуры каталогов можно найти в документации по Laravel. Нас будут интересовать следующие каталоги:
  • app — основной каталог нашего приложения. В корне каталога будут размещены модели. В подкаталоге Http находится все, что касается работы с браузером. В подкаталоге Http/Controllers — наши контроллеры.
  • config — каталог файлов конфигурации. Подробней о конфигурировании будет написано ниже.
  • public — корневой каталог web приложения (DocumentRoot). Содержит статические файлы — css, js, изображения и т.п.
  • resources — здесь находятся шаблоны (Views), файлы локализации и, если таковые имеются, рабочие файлы LESS, SASS и js-приложения на фреймворках типа ReactJS, AngularJS или Ember, которые потом собираются внешним инструментом в папку public.

В корне папки нашего приложения есть файл composer.json, который описывает, какие пакеты, потребуются нашему приложению помимо тех, что уже есть в Laravel. Нам потребуется два таких пакета: «zofe/rapyd-laravel» — для быстрого построения интерфейса с сетками (grids) и диалогами редактирования, и «sim1984/laravel-firebird» — расширение для работы с СУБД Firebird. Пакет «sim1984/laravel-firebird» является форком пакета «jacquestvanzuydam/laravel-firebird» поэтому его установка несколько отличается (описание отличий пакета от оригинального вы можете найти в статье «Пакет для работы с СУБД Firebird в Laravel»). Не забудьте установить параметр minimum-stability равный dev, так как пакет не является стабильным, а так же добавить ссылки на репозиторий.
… 
   "repositories": [
        {
            "type": "package",
            "package": {
                "version": "dev-master",
                "name": "sim1984/laravel-firebird",
                "source": {
                    "url": "https://github.com/sim1984/laravel-firebird",
                    "type": "git",
                    "reference": "master"
                },
                "autoload": {
                    "classmap": [""]
                }
            }
        }
    ],
    …

В секции require добавьте требуемые пакеты следующим образом:
"zofe/rapyd": "2.2.*",
"sim1984/laravel-firebird": "dev-master"

Теперь можно запустить обновление пакетов командой (запускать надо в корне веб приложения)
composer update

После выполнения этой команды новые пакеты будут установлены в ваше приложение. Теперь можно приступить к настройке. Для начала выполним команду
php artisan vendor:publish

которая создаст дополнительные файлы конфигурации для пакета zofe/rapyd.

В файле config/app.php добавим два новых провайдера. Для этого добавим две новых записи в ключ providers

        Zofe\Rapyd\RapydServiceProvider::class,
        Firebird\FirebirdServiceProvider::class,

Теперь перейдём к файлу config/databases.conf, который содержит настройки подключения к базе данных. Добавим в ключ connections следующие строки
       'firebird' => [
            'driver' => 'firebird',
            'host' => env('DB_HOST', 'localhost'),           
            'port' => env('DB_PORT', '3050'),
            'database' => env('DB_DATABASE', 'examples'),
            'username' => env('DB_USERNAME', 'SYSDBA'),
            'password' => env('DB_PASSWORD', 'masterkey'),
            'charset' => env('DB_CHARSET', 'UTF8'),
            'engine_version' => '3.0.0',
        ],

Поскольку мы будем использовать наше подключение в качестве подключения по умолчанию, установим следующее
'default' => env('DB_CONNECTION', 'firebird'),

Обратите внимание на функцию env, которая используется для чтения переменных окружения приложения из специального файла .env, находящегося в корне проекта. Исправим в этом файле .env следующие строки
DB_CONNECTION=firebird
DB_HOST=localhost
DB_PORT=3050
DB_DATABASE=examples
DB_USERNAME=SYSDBA
DB_PASSWORD=masterkey

В файле конфигурации config/rapid.php изменим отображение дат так, чтобы они были в формате принятом в России:
'fields' => [
        'attributes' => ['class' => 'form-control'],
        'date' => [
            'format' => 'd.m.Y',
        ],
        'datetime' => [
            'format' => 'd.m.Y H:i:s',
            'store_as' => 'Y-m-d H:i:s',
        ],
    ],

Первоначальная настройка закончена, теперь мы можем приступить непосредственно к написанию логики web приложения.

Создание моделей


Фреймворк Laravel поддерживает ORM Eloquent. ORM Eloquent — красивая и простая реализация паттерна ActiveRecord для работы с базой данных. Каждая таблица имеет соответствующий класс-модель, который используется для работы с этой таблицей. Модели позволяют читать данные из таблиц и записывать данные в таблицу.

Создадим модель заказчиков, для упрощения этого процесса в Laravel есть artisan команда.

php artisan make:model Customer

Этой командой мы создаём шаблон модели. Теперь изменим нашу модель так, чтобы она выглядела следующим образом:
namespace App;

use Firebird\Eloquent\Model;

class Customer extends Model
{
    /**
     * Таблица, связанная с моделью
     *
     * @var string
     */
    protected $table = 'CUSTOMER';
    
    /**
     * Первичный ключ модели
     *
     * @var string
     */
    protected $primaryKey = 'CUSTOMER_ID';    
    
    /**
     * Наша модель не имеет временной метки
     *
     * @var bool
     */
    public $timestamps = false;  
    
    /**
     * Имя последовательности для генерации первичного ключа
	 
     * @var string 
     */
    protected $sequence = 'GEN_CUSTOMER_ID';
}

Обратите внимание, мы используем модифицированную модель Firebird\Eloquent\Model из пакета sim1984/laravel-firebird в качестве базовой. Она позволяет воспользоваться последовательностью, указанной в свойстве $sequence, для генерирования значения идентификатора первичного ключа.

По аналогии создадим модель товаров — Product.

Модель Product
namespace App;

use Firebird\Eloquent\Model;

class Product extends Model
{
    /**
     * Таблица, связанная с моделью
     *
     * @var string
     */
    protected $table = 'PRODUCT';
    
    /**
     * Первичный ключ модели
     *
     * @var string
     */
    protected $primaryKey = 'PRODUCT_ID';    
    
    /**
     * Наша модель не имеет временной метки
     *
     * @var bool
     */
    public $timestamps = false;  
    
    /**
     * Имя последовательности для генерации первичного ключа
	 
     * @var string 
     */
    protected $sequence = 'GEN_PRODUCT_ID';     
}


Теперь создадим модель для шапки счёт-фактуры.
Модель Invoice
namespace App;

use Firebird\Eloquent\Model;

class Invoice extends Model {

    /**
     * Таблица, связанная с моделью
     *
     * @var string
     */
    protected $table = 'INVOICE';

    /**
     * Первичный ключ модели
     *
     * @var string
     */
    protected $primaryKey = 'INVOICE_ID';

    /**
     * Наша модель не имеет временной метки
     *
     * @var bool
     */
    public $timestamps = false;

    /**
     * Имя последовательности для генерации первичного ключа
     *
     * @var string 
     */
    protected $sequence = 'GEN_INVOICE_ID';

    /**
     * Заказчик
     *
     * @return \App\Customer
     */
    public function customer() {
        return $this->belongsTo('App\Customer', 'CUSTOMER_ID');
    }

    /**
     * Позиции счёт фактуры
	 
     * @return \App\InvoiceLine[]
     */
    public function lines() {
        return $this->hasMany('App\InvoiceLine', 'INVOICE_ID');
    }
    
    /**
     * Оплата 
     */
    public function pay() {
        $connection = $this->getConnection();

        $attributes = $this->attributes;

        $connection->executeProcedure('SP_PAY_FOR_INOVICE', [$attributes['INVOICE_ID']]);
    }
 
}


В этой модели можно заметить несколько дополнительных функций. Функция customer возвращает заказчика связанного со счёт фактурой через поле CUSTOMER_ID. Для осуществления такой связи используется метод belongsTo, в который передаются имя класса модели и имя поле связи. Функция lines возвращают позиции счёт-фактуры, которые представлены коллекцией моделей InvoiceLine (будет описана далее). Для осуществления связи один ко многим в функции lines используется метод hasMany, в который передаётся имя класса модели и поле связи. Подробнее о задании отношений между сущностями вы можете почитать в разделе Отношения документации Laravel.

Функция pay осуществляет оплату счёт фактуры. Для этого вызывается хранимая процедура SP_PAY_FOR_INVOICE. В неё передаётся идентификатор счёт фактуры. Значение любого поля (атрибута модели) можно получить из свойства attributes. Вызов хранимой процедуры осуществляется с помощью метода executeProcedure. Этот метод доступен только при использовании расширения sim1984/laravel-firebird.

Теперь создадим модель для позиций счёт фактуры.

Модель InvoiceLine
namespace App;

use Firebird\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class InvoiceLine extends Model {

    /**
     * Таблица, связанная с моделью
     *
     * @var string
     */
    protected $table = 'INVOICE_LINE';

    /**
     * Первичный ключ модели
     *
     * @var string
     */
    protected $primaryKey = 'INVOICE_LINE_ID';

    /**
     * Наша модель не имеет временной метки
     *
     * @var bool
     */
    public $timestamps = false;

    /**
     * Имя последовательности для генерации первичного ключа
     *
     * @var string 
     */
    protected $sequence = 'GEN_INVOICE_LINE_ID';
	
    /**
      * Массив имён вычисляемых полей
      *
      * @var array
      */
    protected $appends = ['SUM_PRICE'];

    /**
     * Товар
     *
     * @return \App\Product
     */	
    public function product() {
        return $this->belongsTo('App\Product', 'PRODUCT_ID');
    }

    /**
     * Сумма по позиции
     *
     * @return double
     */		
    public function getSumPriceAttribute() {
        return $this->SALE_PRICE * $this->QUANTITY;
    }

    /**
     * Добавление объекта модели в БД
     * Переопределяем этот метод, т.к. в данном случаем мы работаем с помощью ХП 
     * 
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @param  array  $options
     * @return bool
     */
    protected function performInsert(Builder $query, array $options = []) {

        if ($this->fireModelEvent('creating') === false) {
            return false;
        }

        $connection = $this->getConnection();

        $attributes = $this->attributes;
        
        $connection->executeProcedure('SP_ADD_INVOICE_LINE', [
            $attributes['INVOICE_ID'],
            $attributes['PRODUCT_ID'],
            $attributes['QUANTITY']
        ]);

        // We will go ahead and set the exists property to true, so that it is set when
        // the created event is fired, just in case the developer tries to update it
        // during the event. This will allow them to do so and run an update here.
        $this->exists = true;

        $this->wasRecentlyCreated = true;

        $this->fireModelEvent('created', false);

        return true;
    }

    /**
     * Сохранение изменений текущего экземпляра модели в БД
     * Переопределяем этот метод, т.к. в данном случаем мы работаем с помощью ХП 
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @param  array  $options
     * @return bool
     */
    protected function performUpdate(Builder $query, array $options = []) {
        $dirty = $this->getDirty();

        if (count($dirty) > 0) {
            // If the updating event returns false, we will cancel the update operation so
            // developers can hook Validation systems into their models and cancel this
            // operation if the model does not pass validation. Otherwise, we update.
            if ($this->fireModelEvent('updating') === false) {
                return false;
            }

            $connection = $this->getConnection();

            $attributes = $this->attributes;
            
            $connection->executeProcedure('SP_EDIT_INVOICE_LINE', [
                $attributes['INVOICE_LINE_ID'],
                $attributes['QUANTITY']
            ]);            


            $this->fireModelEvent('updated', false);
        }
    }

    /**
     * Удаление текущего экземпляра модели в БД
     * Переопределяем этот метод, т.к. в данном случаем мы работаем с помощью ХП 
     *
     * @return void
     */
    protected function performDeleteOnModel() {

        $connection = $this->getConnection();

        $attributes = $this->attributes;
        
        $connection->executeProcedure('SP_DELETE_INVOICE_LINE', 
            [$attributes['INVOICE_LINE_ID']]);          

    }
}


В этой модели есть функция product, которая возвращает продукт (модель App/Product), указанный в позиции счёт фактуры. Связь осуществляется по полю PRODUCT_ID с помощью метода belongsTo.

Вычисляемое поле SumPrice вычисляется с помощью функции getSumPriceAttribute. Для того чтобы это вычисляемое поле было доступно в модели, его имя должно быть указано в массиве имён вычисляемых полей $appends.

В этой модели мы переопределили операции insert, update и delete так, чтобы они выполнялись, используя хранимые процедуры. Эти хранимые процедуры помимо собственно операций вставки, редактирования и удаления пересчитывают сумму в шапке накладной. Этого можно было бы и не делать, но тогда пришлось бы выполнять в одной транзакции модификацию нескольких моделей. Как это сделать будет показано далее.

Теперь немного поговорим о том, как работать с моделями в Laravel для выборки, вставки, редактирования и удаления данных. Laravel оперирует данными с помощью конструктора запросов. Полное описание синтаксиса и возможностей этого конструктора вы можете найти по ссылке. Например, для получения всех строк поставщиков вы можете выполнить следующий запрос

$customers = DB::table('CUSTOMER')->get();

Этот конструктор запросов является довольно мощным средством для построения и выполнения SQL запросов. Вы можете выполнять также фильтрация, сортировку и соединения таблиц, например
DB::table('users')
        ->join('contacts', function ($join) {
            $join->on('users.id', '=', 'contacts.user_id')->orOn(...);
        })
        ->get();

Однако гораздо удобнее работать с использованием моделей. Описание моделей Eloquent ORM и синтаксиса запроса к ним можно найти по ссылке laravel.ru/docs/v5/eloquent. Так для получения всех элементов коллекции поставщиков необходимо выполнить следующий запрос
$customers = Customer::all();

Следующий запрос вернёт первые 20 поставщиков отсортированных по алфавиту.
$customers = App\Customer::select()
               ->orderBy('name')
               ->take(20)
               ->get();

Для сложных моделей связанные отношения или коллекции отношений могут быть получены через динамические атрибуты. Например, следующий запрос вернёт позиции счёт-фактуры с идентификатором 1.
$lines = Invoice::find(1)->lines;

Добавление записей осуществляется через создание экземпляра модели, инициализации его свойств и сохранение модели с помощью метода save.
$flight = new Flight;
$flight->name = $request->name;
$flight->save();

Для изменения запись её необходимо найти, изменить необходимые атрибуты и сохранить методом save.
$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();

Для удаления записи её необходимо найти и вызвать метод delete.
$flight = App\Flight::find(1);
$flight->delete();

Удалить запись по ключу можно и гораздо быстрее с помощью метода destroy. В этом случае можно удалить модель не получая её экземпляр.
App\Flight::destroy(1);

Существуют и другие способы удаления записей, например «мягкое» удаление. Подробно о способах удаления вы можете прочитать по ссылке.

Теперь поговорим немного о транзакциях. Что это такое я рассказывать не буду, а лишь покажу, как их можно использовать совместно с Eloquent ORM.

DB::transaction(function () {
  // Создаём новую позицию в счёт фактуре
  $line = new App\InvoiceLine();
  $line->CUSTOMER_ID = 45;
  $line->PRODUCT_ID = 342;
  $line->QUANTITY = 10;
  $line->COST = 12.45;
  $line->save();	

  // добавляем сумму позиции по строке к сумме накладной 
  $invoice = App\Invoice::find($line->CUSTOMER_ID);
  $invoice->INVOICE_SUM += $line->SUM_PRICE;
  $invoice->save();  
});

Всё что&nb

© Habrahabr.ru