Створення web-додатку на PHP з використанням Firebird і Laravel

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

У минулій статті я розповідав про пакет для підтримки СУБД Firebird у фреймворку Laravel. На цей раз ми розглянемо процес створення веб-додатки з використанням СУБД Firebird на мові PHP з використанням Laravel.

Огляд драйверів для роботи з Firebird
В PHP є два драйвери для роботи з СКБД 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). 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 вручну, то необхідно запустити

Скрипт установки 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",
"джерело": {
"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 Model extends
{
/**
* Таблиця, зв'язана з моделлю
*
* @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 when set
// created the 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 updating the event повертає 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('контакти', 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(); 
});

Все що знаходиться у функції зворотного виклику, яка є аргументом методу transaction, виконується в рамках однієї транзакції.

Створення контролерів та налаштування маршрутизації
Фреймворк Laravel має потужну підсистему маршрутизації. Ви можете відображати ваші маршрути, як на прості функції зворотного виклику, так і на методи контролерів. Найпростіші приклади маршрутів виглядають ось так

Route::get('/', function () {
return 'Hello World';
});

Route::post('foo/bar', function () {
return 'Hello World';
});

У першому випадку ми реєструємо обробник GET запиту для кореня сайту, у другому – для POST запиту з маршрутом /foo/bar.

Ви можете зареєструвати маршрут відразу на кілька типів HTTP запитів, наприклад

Route::match(['get', 'post'], 'foo/bar', function () {
return 'Hello World';
});

З маршруту можна витягати частина адреси і використовувати його в якості параметрів функції-обробника

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
//
});

Параметри маршруту завжди полягають у фігурні дужки. Детальніше про можливості налаштування маршрутизації ви можете подивитися в документації глава «Маршрутизація». Маршрути настроюються у файлі app/Http/routes.php у Laravel 5.2 і routes/wep.php у Laravel 5.3.

Замість того, щоб описувати обробку всіх запитів в єдиному файлі маршрутизації, ми можемо організувати її використовую класи Controller, які дозволяють групувати пов'язані обробники запитів в окремі класи. Контролери зберігаються в папці app/Http/Controllers.

Всі Laravel контролери повинні розширювати базовий клас контролера App\Http\Controllers\Controller, присутній в Laravel за замовчуванням. Детальніше про написання контролерів ви можете почитати в документації у розділі HTTP-Контролери.

Напишемо наш перший контролер.

/*
* Контролер замовників
*/

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Customer;


class CustomerController extends Controller 
{

/**
* Відображає список замовників
*
* @return Response
*/
public function showCustomers() 
{
// запитуємо з моделі перші 20 замовників 
// відсортовані за алфавітом 
$customers = Customer::select()
->orderBy('NAME')
->take(20)
->get();
var_dump($customers);
}

}

Тепер необхідно пов'язати методи контролера з маршрутом. Для цього в routes.php (web.php) необхідно внести рядок

Route::get('/customers', 'CustomerController@showCustomers');

Тут ім'я контролера відокремлене від імені методу символом @.

Для швидкої побудови інтерфейсу з сітками і діалогами редагування будемо використовувати пакет «zofe/rapyd». Ми його вже підключили раніше. Класи пакету zofe/rapyd беруть на себе побудова типових запитів до моделей Eloquent ORM. Змінимо контролер замовників так, щоб він виводив дані в сітку (грід), дозволяв виробляти їх фільтрацію, а також додавати, редагувати і видаляти записи через діалоги редагування.

Контролер Customer
/*
* Контролер замовників
*/

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Customer;

class CustomerController extends Controller {

/**
* Відображає список замовників
*
* @return Response
*/
public function showCustomers() {
// Підключаємо віджет для пошуку
$filter = \DataFilter::source(new Customer);
// Пошук по найменуванню постачальника
$filter->add('NAME', 'Назва', 'text');
// Задаємо підпис кнопці пошуку
$filter->submit('Пошук');
// Додаємо кнопку скидання фільтра і ставимо її підпис
$filter->reset('Скинути');

// Створю сітку для відображення відфільтрованих даних
$grid = \DataGrid::source($filter);

// виводяться стовпці 
// Поле, підпис, сортовані
$grid->add('NAME', 'Назва', true);
$grid->add('ADDRESS', 'Адреса');
$grid->add('ZIPCODE', 'Індекс');
$grid->add('PHONE', 'Телефон');

// Додаємо кнопки для перегляду, редагування і видалення запису
$grid->edit('/customer/edit', 'Редагування', 'show|modify|delete'); 
// Додаємо кнопку додавання замовника
$grid->link('/customer/edit', "Додавання замовника", "TR");
// задаємо сортування
$grid->orderBy('NAME', 'asc'); 
// задаємо кількість записів на сторінку
$grid->paginate(10); 
// відображаємо шаблон customer і передаємо в нього фільтр і грід
return view('customer', compact('filter', 'grid'));
}

/**
* Додавання, редагування та видалення замовника
* 
* @return Response
*/
public function editCustomer() {
if (\Input::get('do_delete') == 1)
return "not the first";
// створюємо редактор
$edit = \DataEdit::source(new Customer());
// задаємо підпис діалогу в залежності від типу операції
switch ($edit->status) {
case 'create':
$edit->label('Додавання замовника');
break;
case 'modify':
$edit->label('Редагування замовника');
break;
case 'do_delete':
$edit->label('Видалення замовника');
break;
case 'show':
$edit->label('Картка замовника');
// додаємо посилання для повернення назад на список замовників
$edit->link('customers', 'Назад', 'TR');
break;
}
// задаємо що після операцій додавання, редагування та видалення 
// повертаємося до списку замовників 
$edit->back('insert|update|do_delete', 'customers');
// Додаємо редактори певного типу, задаємо їм підпис 
// і пов'язуємо їх з атрибутами моделі
$edit->add('NAME', 'Назва', 'text')->rule('required|max:60');
$edit->add('ADDRESS', 'Адреса', 'textarea')
->attributes(['rows' => 3])
->rule('max:250');
$edit->add('ZIPCODE', 'Індекс', 'text')->rule('max:10');
$edit->add('PHONE', 'Телефон', 'text')->rule('max:14');
// відображаємо шаблон customer_edit і передаємо в нього редактор
return $edit->view('customer_edit', compact('edit'));
}
}


Laravel за замовчуванням використовує шаблонизатор blade. Функція view знаходить потрібний шаблон у директорії resources/views, робить необхідні заміни в ньому і повертає текст сторінки HTML. Крім того, вона передає в нього змінні, які стають доступними в шаблоні. Опис синтаксису шаблонів blade ви можете знайти у документації в розділі Шаблонизатор Blade.

Шаблон для відображення замовників виглядає наступним чином:

@extends('example')

@section('title','Замовники')

@section('body')

<h1>Замовники</h1>
<p>
{!! $filter !!}
{!! $grid !!}
</p>
@stop

Даний шаблон успадкований від шаблону example і перевизначає його секцію body. Змінні $filter і $grid містять HTML-код для здійснення фільтрації та відображення даних в сітці. Шаблон example є загальним для всіх сторінок.

Шаблон example.blade
@extends('master')
@section('title', 'Приклад роботи з Firebird')

@section('body')

<h1>Приклад</h1>

@if(Session::has('message'))
<div class="alert alert-success">
{!! Session::get('message') !!}
</div>
@endif

<p>Приклад роботи з Firebird.<br/>
</p>
@stop

@section('content')
@include('menu')
@yield('body')
@stop


Цей шаблон сам успадкований від шаблону master, крім того він підключає шаблон menu. Меню досить просте, складається з трьох пунктів Замовники, Продукти і Рахунок фактури.

<nav class="navbar main">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".main-collapse">
<span class="sr-only"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="collapse navbar-collapse main-collapse">
<ul class="nav nav-tabs">
<li @if (Request::is('customer*')) class="active"@endif>{!! link_to("customers", "Замовники") !!}</li>
<li @if (Request::is('product*')) class="active"@endif>{!! link_to("products", "Товари") !!}</li>
<li @if (Request::is('invoice*')) class="active"@endif>{!! link_to("invoices", "Рахунок фактури") !!}</li>
</ul>
</div>
</nav>

В шаблоні master підключаються css стилі і JavaScript файли з бібліотеками.

Шаблон master.blade
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Web Приклад програми на Firebird')</title>
<meta name="description" content="@yield('description', 'Web Приклад програми на Firebird')" />
@section('meta', ")


<link href="http://fonts.googleapis.com/css?family=Bitter" rel="stylesheet" type="text/css" />

<link href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">


{!! Rapyd::styles(true) !!}
</head>

<body>
<div id="wrap"> 
<div class="container">
<br />
<div class="row">
<div class="col-sm-12">
@yield('content')
</div>
</div>

</div> 
</div> 

<div id="footer">
</div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> 
<script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> 
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.pjax/1.9.6/jquery.pjax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/riot/2.2.4/riot+compiler.min.js"></script> 

{!! Rapyd::scripts() !!}
</body> 
</html>


Шаблон редактора замовника customer_edit виглядає наступним чином

@extends('example')

@section('title', 'Редагування замовника')

@section('body')
<p>
{!! $edit !!}
</p>
@stop

Контролер товарів зроблений аналогічно контролеру постачальників.

Контролер Product
/*
* Контролер товарів
*/

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Product;

class ProductController extends Controller {
/**
* Відображає список продуктів
*
* @return Response
*/
public function showProducts() { 
// Підключаємо віджет для пошуку 
$filter = \DataFilter::source(new Product);
// Пошук по найменуванню продукту
$filter->add('NAME', 'Назва', 'text');
$filter->submit('Пошук');
$filter->reset('Скинути');

// Створю сітку для відображення відфільтрованих даних
$grid = \DataGrid::source($filter);

// виводяться стовпці сітки
// Поле, підпис, сортовані
$grid->add('NAME', 'Назва', true);
// задаємо формат з 2 знаками після коми
$grid->add('PRICE|number_format[2,., ]', 'Вартість');

$grid->row(function($row) {
// Грошові величини притискаємо вправо
$row->cell('PRICE')->style("text-align: right");
}); 
// Додаємо кнопки для перегляду, редагування і видалення запису
$grid->edit('/product/edit', 'Редагування', 'show|modify|delete'); 
// Додаємо кнопку додавання замовника
$grid->link('/product/edit', "Додавання товару", "TR");
// задаємо сортування
$grid->orderBy('NAME', 'asc');
// задаємо кількість записів на сторінку
$grid->paginate(10); 
// відображаємо шаблон customer і передаємо в нього фільтр і грід
return view('product', compact('filter', 'grid'));
} 

/**
* Додавання, редагування та видалення замовника
* 
* @return Response
*/
public function editProduct() {
if (\Input::get('do_delete') == 1)
return "not the first";
// створюємо редактор
$edit = \DataEdit::source(new Product());
// задаємо підпис діалогу в залежності від типу операції
switch ($edit->status) {
case 'create':
$edit->label('Додавання товару');
break;
case 'modify':
$edit->label('Редагування товару');
break;
case 'do_delete':
$edit->label('Видалення товару');
break;
case 'show':
$edit->label('Картка товару');
$edit->link('products', 'Назад', 'TR');
break;
}
// задаємо що після операцій додавання, редагування та видалення 
// повертаємося до списку замовників 
$edit->back('insert|update|do_delete', 'products');
// Додаємо редактори певного типу, задаємо їм підпис 
// і пов'язуємо їх з атрибутами моделі
$edit->add('NAME', 'Назва', 'text')->rule('required|max:100');
$edit->add('PRICE', 'Вартість', 'text')->rule('max:19');
$edit->add('DESCRIPTION', 'Опис', 'textarea')
->attributes(['rows' => 8])
->rule('max:8192');
// відображаємо шаблон product_edit і передаємо в нього редактор
return $edit->view('product_edit', compact('edit'));
} 
}


Контролер рахунок фактур є більш складним. В нього додана додаткова функція оплати рахунку. Сплачені рахунок фактури підсвічуються іншим кольором. При перегляді рахунок фактури відображаються так само її позиції. Під час редагування рахунок фактури є можливість редагувати та її позиції. Далі я наведу текст контролера з докладними коментарями.

Контролер Invoice
/*
* Контролер рахунок фактур
*/

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Invoice;
use App\Customer;
use App\Product;
use App\InvoiceLine;

class InvoiceController extends Controller {

/**
* Відображає список рахунок-фактур
*
* @return Response
*/
public function showInvoices(){
// Модель рахунок фактур буде одночасно 
// вибирати пов'язаних постачальників
$invoices = Invoice::with('customer');
// Підключаємо віджет для пошуку
$filter = \DataFilter::source($invoices);
// Дозволяємо фільтрувати по діапазону дат
$filter->add('INVOICE_DATE', 'Дата', 'daterange');
// і фільтрувати по імені замовника
$filter->add('customer.NAME', 'Замовник', 'text');
$filter->submit('Пошук');
$filter->reset('Скинути');

// Створю сітку для відображення відфільтрованих даних
$grid = \DataGrid::source($filter);

// виводяться стовпці сітки
// Поле, підпис, сортовані
// для дати задаємо додаткову функцію, яка перетворює дату в рядок
$grid->add('INVOICE_DATE|strtotime|date[d.m.Y H:i:s]', 'Дата', true);
// для грошей поставлю формат з двома знаками після коми
$grid->add('TOTAL_SALE|number_format[2,., ]', 'Сума');
$grid->add('customer.NAME', 'Замовник');
// Значення boolean відображаємо як Так/Немає
$grid->add('PAID', 'Сплачено')
->cell(function( $value, $row) {
return $value ? 'Так' : 'Немає';
});
// задаємо функцію обробки кожного рядка
$grid->row(function($row) {
// Грошові величини притискаємо вправо
$row->cell('TOTAL_SALE')->style("text-align: right");
// фарбуємо оплачені накладні в інший колір
if ($row->cell('PAID')->value == 'Так') {
$row->style("background-color: #ddffee;");
}
});

// Додаємо кнопки для перегляду, редагування і видалення запису
$grid->edit('/invoice/edit', 'Редагування', 'show|modify|delete');
// Додаємо кнопку додавання рахунок-фактури
$grid->link('/invoice/edit', "Додавання рахунки", "TR");
// задаємо сортування
$grid->orderBy('INVOICE_DATE', 'desc'); 
// задаємо кількість записів на сторінку
$grid->paginate(10); 
// відображаємо шаблон customer і передаємо в нього фільтр і грід
return view('invoice', compact('filter', 'grid'));
}

/**
* Додавання, редагування та видалення рахунок фактури
* 
* @return Response
*/
public function editInvoice() {
// Отримуємо текст збереженою помилки, якщо вона була
$error_msg = \Request::old('error_msg');
// створюємо редактор рахунок фактури
$edit = \DataEdit::source(new Invoice());
// якщо рахунок сплачено, то генеруємо помилку при спробі його редагування
if (($edit->model->PAID) && ($edit->status === 'змінити')) {
$edit->status = 'show';
$error_msg = 'Редагування не можливо. Рахунок уже сплачено.';
}
// якщо рахунок сплачено, то генеруємо помилку при спробі його видалення
if (($edit->model->PAID) && ($edit->status === 'delete')) {
$edit->status = 'show';
$error_msg = 'Видалення не можливо. Рахунок уже сплачено.';
} 
// задаємо підпис діалогу в залежності від типу операції
switch ($edit->status) {
case 'create':
$edit->label('Додавання рахунку');
break;
case 'modify':
$edit->label('Редагування рахунку');
break;
case 'do_delete':
$edit->label('Видалення рахунку');
break;
case 'show':
$edit->label('Рахунок');
$edit->link('invoices', 'Назад', 'TR');
// Якщо рахунок фактури не оплачена, показуємо кнопку оплатити
if (!$edit->model->PAID)
$edit->link('invoice/pay/' . $edit->model->INVOICE_ID, 'Оплатити', 'BL');
break;
}

// задаємо що після операцій додавання, редагування та видалення 
// повертаємося до списку рахунок фактур 
$edit->back('insert|update|do_delete', 'invoices');

// Задаємо для поля дата, що воно обов'язкове
// За умовчанням ставиться поточна дата
$edit->add('INVOICE_DATE', 'Дата', 'datetime')
->rule('required')
->insertValue(date('Y-m-d H:i:s'));

// Додаємо поле для введення замовника. При наборі імені замовника
// буде відображатися список підказок
$edit->add('customer.NAME', 'Замовник', 'autocomplete')
->rule('required')
->options(Customer::lists('NAME', 'CUSTOMER_ID')->all());
// додаємо поле, яке буде відображати суму накладної, тільки для читання
$edit->add('TOTAL_SALE', 'Сума', 'text')
->mode('readonly')
->insertValue('0.00');
// Додаємо галочку Оплачено
$paidCheckbox = $edit->add('PAID', 'Сплачено', 'checkbox')
->insertValue('0')
->mode('readonly');
$paidCheckbox->checked_output = 'Так';
$paidCheckbox->unchecked_output = 'Немає';

// створюємо грід для відображення рядків рахунок фактури
$grid = $this->getInvoiceLineGrid($edit->model, $edit->status);
// відображаємо шаблон invoice_edit і передаємо в нього редактор і 
// грід для відображення позицій
return $edit->view('invoice_edit', compact('edit', 'grid', 'error_msg'));
}

/**
* Оплата рахунок фактури
*
* @return Response
*/
public function payInvoice($id) {
try {
// знаходимо рахунок фактуру по ідентифікатору
$invoice = Invoice::findOrFail($id);
// викликаємо процедуру оплати
$invoice->pay();
} catch (\Illuminate\Database\QueryException $e) {
// якщо сталася помилка, то
// виділяємо текст виключення
$pos = strpos($e->getMessage(), 'E_INVOICE_ALREADY_PAYED');
if ($pos !== false) {
// перенаправляємо на сторінку редактора та відображаємо там помилку
return redirect('invoice/edit?show=' . $id)
->withInput(['error_msg' => 'Рахунок уже сплачено']);
} else
throw $e;
}
// перенаправляємо на сторінку редактора
return redirect('invoice/edit?show=' . $id);
}

/**
* Отримання сітки для рядків рахунки фактури
* @param \App\Invoice $invoice
* @param string $mode 
* @return \DataGrid
*/
private function getInvoiceLineGrid(Invoice $invoice, $mode) {
// Отримуємо рядка рахунок фактури
// Для кожної позиції рахунки ініціалізується пов'язаний з ним продукт
$lines = InvoiceLine::with('product')->where'INVOICE_ID', $invoice->INVOICE_ID);

// Створю сітку для відображення позицій накладної
$grid = \DataGrid::source($lines);
// виводяться стовпці сітки
// Поле, підпис, сортовані
$grid->add('product.NAME', 'Назва');
$grid->add('КІЛЬКІСТЬ', 'Кількість');
$grid->add('SALE_PRICE|number_format[2,., ]', 'Вартість')->style('min-width: 8em;');
$grid->add('SUM_PRICE|number_format[2,., ]', 'Сума')->style('min-width: 8em;');
// задаємо функцію обробки кожного рядка
$grid->row(function($row) {
$row->cell('КІЛЬКІСТЬ')->style("text-align: right");
// Грошові величини приживаем вправо
$row->cell('SALE_PRICE')->style("text-align: right");
$row->cell('SUM_PRICE')->style("text-align: right");
});

if ($mode == 'змінити') {
// Додаємо кнопки для перегляду, редагування і видалення запису
$grid->edit('/invoice/editline', 'Редагування', 'modify|delete');
// Додаємо кнопку додавання замовника
$grid->link('/invoice/editline?invoice_id=' . $invoice->INVOICE_ID, "Додавання позиції", "TR");
}

return $grid;
}

/**
* Додавання, редагування та видалення позицій рахунок фактури
* 
* @return Response
*/ 
public function editInvoiceLine() {
if (\Input::get('do_delete') == 1)
return "not the first";

$invoice_id = null;
// створюємо редактор позиції рахунок фактури
$edit = \DataEdit::source(new InvoiceLine());
// задаємо підпис діалогу в залежності від типу операції
switch ($edit->status) {
case 'create':
$edit->label('Додавання позиції');
$invoice_id = \Input::get('invoice_id');
break;
case 'modify':
$edit->label('Редагування позиції');
$invoice_id = $edit->model->INVOICE_ID;
break;
case 'delete':
$invoice_id = $edit->model->INVOICE_ID;
break;
case 'do_delete':
$edit->label('Видалення позиції');
$invoice_id = $edit->model->INVOICE_ID;
break;
}
// формуємо url для повернення
$base = str_replace(\Request::path(), ", strtok(\Request::fullUrl(), '?'));
$back_url = $base . 'invoice/edit?modify=' . $invoice_id;
// встановлюємо сторінку для повернення
$edit->back('insert|update|do_delete', $back_url);
$edit->back_url = $back_url;
// додаємо приховане поле з кодом рахунок фактури
$edit->add('INVOICE_ID', ", 'hidden')
->rule('required')
->insertValue($invoice_id)
->updateValue($invoice_id);
// Додаємо поле для введення товару. При наборі імені товару
// буде відображатися список підказок
$edit->add('product.NAME', 'Назва', 'autocomplete')
->rule('required')
->options(Product::lists('NAME', 'PRODUCT_ID')->all());
// поле для вводу кількості 
$edit->add('КІЛЬКІСТЬ', 'Кількість', 'text')
->rule('required');
// відображаємо шаблон invoice_line_edit і передаємо в нього редактор 
return $edit->view('invoice_line_edit', compact('edit'));
}
}


Редактор рахунок фактур має не стандартний для zofe/rapyd вигляд, оскільки нам необхідно виводити сітку з позиціями рахунок фактур. Для цього ми змінили шаблон invoice_edit наступним чином.

Шаблон invoice_edit.blade
@extends('example')

@section('title','Редагування рахунку')

@section('body')

<div class="container">
{!! $edit->header !!}

@if($error_msg)
<div class="alert alert-danger">
<strong>Помилка!</strong> {{ $error_msg }}
</div> 
@endif

{!! $edit->message !!}

@if(!$edit->message) 
<div class="row">
<div class="col-sm-4">
{!! $edit->render('INVOICE_DATE') !!}
{!! $edit->render('customer.NAME') !!}
{!! $edit->render('TOTAL_SALE') !!}
{!! $edit->render('PAID') !!}
</div> 
</div> 
{!! $grid !!} 
@endif

{!! $edit->footer !!}
</div>
@stop


Тепер, коли всі контролери написані, змінимо маршрути так, щоб наш сайт на стартовій сторінці відкривав список рахунок фактур. Нагадую, що маршрути настроюються у файлі app/Http/routes.php у Laravel 5.2 і routes/wep.php у Laravel 5.3.

// Кореневої маршрут
Route::get('/', 'InvoiceController@showInvoices');

Route::get('/customers', 'CustomerController@showCustomers');
Route::any('/customer/edit', 'CustomerController@editCustomer');

Route::get('/products', 'ProductController@showProducts');
Route::any('/product/edit', 'ProductController@editProduct');

Route::get('/invoices', 'InvoiceController@showInvoices');
Route::any('/invoice/edit', 'InvoiceController@editInvoice');
Route::any('/invoice/pay/{id}', 'InvoiceController@payInvoice');
Route::any('/invoice/editline', 'InvoiceController@editInvoiceLine');

Тут маршрут /invoice/pay/{id} виділяє ідентифікатор рахунок фактури з адреси і передає його в метод payInvoice. Інші маршрути не вимагають окремого пояснення.

Наостанок наведу кілька скріншотів отриманого веб додатки.





На цьому мій приклад закінчений. Вихідні коди ви можете завантажити за посиланням https://github.com/sim1984/phpfbexample
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.