10. Багатошарові додатки
10.1. Introduction

Тепер ми дізнаємося, як структурувати додаток PHP за рівнями:

На цій схемі лівий рівень ініціює використання правого рівня. Ролі рівнів такі:
- [1]: шар, який називається [dao] (Data Access Objects), відповідає за взаємодію із зовнішніми сховищами даних [4] (файли, бази даних, веб-сервіси тощо). Цей рівень іноді називають [DAL] (Data Access Layer) — цей термін краще описує роль рівня. Цей рівень може зчитувати дані [3] або записувати їх [2]. Вона отримує запити від шару [métier] [6] і повертає йому результати [7];
- [5]: рівень під назвою [métier], який об’єднує «бізнес-процедури», яким надаються всі необхідні дані. Зазвичай це найстабільніший рівень проекту, оскільки він не залежить від способу отримання даних. Дані мають два джерела:
- [9]: дані, що надаються скриптом PHP;
- [6,7]: дані, що запитуються у шарі [dao].
- [8]: головний скрипт є «диригентом». У консольному додатку він:
- створює шари [métier] та [dao];
- виконує алгоритм програми. Цей алгоритм діє як диригент: у ньому немає жодного «бізнес-коду» чи коду доступу до даних. Головний скрипт лише викликає процедури шарів [métier] та [9]. Він повністю ігнорує рівень [dao] та зовнішні дані. Він може надавати рівню [métier] дані з рівня [9]. У консольному додатку ці дані можуть надходити з конфігураційних файлів або від користувача скрипта. Він отримує результати [10] від шару [métier]. Йому може знадобитися зберегти певні результати: для цього він знову використовує процедури шару [métier], які, у свою чергу, звертаються до шару [dao], який виконує цю роботу;
- серед результатів головний скрипт може отримувати винятки. Його завданням є обробка винятків, що надходять з усіх шарів;
Такий поділ на шари призначений для полегшення розвитку додатка. Для цього кожен шар реалізує інтерфейс. Припустимо, що:
- шар [dao] реалізовано класом [Dao], який реалізує інтерфейс [IDao];
- шар [métier] реалізовано класом [Métier], який реалізує інтерфейс [IMétier];
Процедури рівня [métier] будуть використовувати інтерфейс [IDao] замість класу [Dao]. Це дозволяє модернізувати рівень [Dao], не зачіпаючи рівень [Métier]. Припустимо, що у версії 1 рівень [Dao1] використовує дані з бази даних. Після оновлення ці дані тепер надаються веб-сервісом у версії 2, [Dao2]. Необхідно переконатися, що обидва класи [Dao1] та [Dao2] реалізують один і той самий інтерфейс [IDao] і що вони генерують однакові винятки. Якщо це підтвердиться, рівень [Métier], який працює з інтерфейсом [IDao], залишиться без змін;
Те саме міркування стосується рівня [Métier].
Розглянемо реалізацію з використанням класів та інтерфейсів:

Додаток реалізує таку багаторівневу структуру:

10.2. Об’єкти, що обмінюються між шарами
Зазвичай шари обмінюються різними об’єктами. У цьому випадку вони обмінюватимуться таким класом: [Personne.php]:
<?php
// особа
class Personne {
// ідентифікатор
private $id;
// конструктор
public function __construct(int $id) {
$this->id = $id;
}
// toString
public function __toString(): string {
return "[Personne($this->id)]";
}
}
Коментарі
- рядок 6: особа має лише один атрибут — її ідентифікатор [$id]. Припустимо, що він позначає унікальну особу в сховищі даних;
- рядки 9–11: конструктор, що дозволяє створити тип [Personne] із його ідентифікатором;
- рядки 14–16: метод [__toString], який виводить ідентифікатор особи;
10.3. Рівень [dao]
Інтерфейс [IDao], реалізований рівнем [dao, 1], має такий вигляд: [IDao.php]:
<?php
// шар [dao] ------------------------
interface IDao {
// вилучення особи зі зовнішнього складу
// передача ідентифікатора особи
public function get(int $id): Personne;
// збереження даних особи у зовнішньому сховищі
public function save(Personne $p): void;
}
Цей інтерфейс реалізовано наступним класом [Dao1] [Dao1.php]:
<?php
class Dao1 implements IDao {
// збереження даних про особу у зовнішньому сховищі
public function save(Personne $p): void {
print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
}
// витягнення особи з зовнішнього сховища
public function get(int $id): Personne {
print "[Dao1] : Récupération de la personne d'identité ($id) en base de données [locale]\n";
return new Personne($id);
}
}
Коментарі
- клас не взаємодіє з сховищем даних. Ми обмежуємося відображенням повідомлень для відстеження виконання коду (рядки 7 та 12);
- рядок 13: дійсно повертається особа з ідентифікатором, переданим як параметр методу (рядок 11);
Ми також реалізуємо інтерфейс [IDao] за допомогою наступного класу [Dao2] [Dao2.php]:
<?php
class Dao2 implements IDao {
// збереження особи у зовнішньому сховищі
public function save(Personne $p): void {
print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
}
// витягнення особи з зовнішнього сховища
public function get(int $id): Personne {
print "[Dao2] : Récupération de la personne d'identité ($id) en base de données [distante]\n";
return new Personne($id);
}
}
Клас [Dao2] аналогічний класу [Dao1], за винятком того, що ми змінили повідомлення, які відображаються.
10.4. Шар [métier]
Рівень [métier, 2] надає такий інтерфейс [IMétier]: [IMetier.php]:
<?php
// шар [métier] ------------------------
interface IMétier extends IDao {
// обробка особи, ідентифікованої за її ідентифікатором
public function doSomething(Personne $p): void;
}
Коментарі
- Інтерфейс [IMétier] розширює інтерфейс [IDao]. Це зовсім не обов’язково. Ми робимо це тут, оскільки приклад є простим;
- рядок 7: метод [doSomething] є власним для шару [Métier];
Інтерфейс [IMétier] буде реалізовано наступним класом [Métier] [Métier.php]:
<?php
class Métier implements IMétier {
// шар [dao]
private $dao;
// геттер / сеттер
public function setDao(IDao $dao): void {
$this->dao = $dao;
}
public function getDao(): IDao {
return $this->dao;
}
// використовується особа
public function doSomething(Personne $p): void {
// обробка
print "Métier : Traitement métier de la personne $p\n";
}
// збереження даних про особу
public function save(Personne $p): void {
print "[Métier] : Sauvegarde de la personne $p\n";
// запит до шару [dao] щодо збереження
$this->dao->save($p);
}
public function get(int $id): Personne {
print "[Métier] : récupération de la personne d'identifiant $id\n";
// запит щодо особи до шару [dao]
$personne = $this->dao->get($id);
return $personne;
}
}
Коментарі
- рядок 5: шар [métier] повинен мати посилання на шар [dao], щоб мати змогу використовувати його методи;
- рядки 8–10: метод [setDao] дозволяє надати шару [métier] посилання на шар [dao]. Зауважимо, що тип параметра — [IDao]. Таким чином, шар [métier] зможе працювати з будь-яким класом, що реалізує інтерфейс [IDao]. Якщо перейти від шару [Dao1] до шару [Dao2] і обидва вони реалізують інтерфейс [IDao], шар [métier] не потрібно переписувати;
- рядки 17–20: реалізація [IMetier::doSomething];
- рядки 23–27: реалізація [IMetier::save]. Цей метод повинен зберегти дані про особу в сховищі даних. Рівень [métier] не вміє цього робити. Він звертається до рівня [dao], щоб той здійснив це збереження;
- рядки 29–34: реалізація методу [IMetier::get]. Цей метод повинен отримати зі сховища даних особу, ідентифікатор якої передано йому як параметр. Рівень [métier] не може цього зробити. Він звертається до рівня [dao], щоб той виконав цю роботу (рядок 32);
Висновок
Як тільки рівень [métier] потребує доступу до даних, що зберігаються у сховищі даних, він повинен звернутися до рівня [dao], який було створено саме для доступу до цих даних.
10.5. Головний скрипт
Ми напишемо два скрипти, які керуватимуть цією програмою. Перший, [main1.php], використовуватиме шар [Dao1], тоді як другий, [main2.php], використовуватиме шар [Dao2]. Ми хочемо показати, що це жодним чином не впливає на код шару [Métier].
Скрипт [main1.php] має такий вигляд:
<?php
// суворе дотримання параметрів функцій
declare (strict_types=1);
// включення класів та інтерфейсів
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao1.php";
require_once __DIR__."/Métier.php";
// тест ----------------
// створення шарів
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// використання шару [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
Коментарі
- Нагадаємо кілька моментів: скрипт [main1.php] є «диригентом» додатка. Він створює шарову структуру додатка (рядки 15–17), а потім починає взаємодіяти з шаром [métier] (рядки 19–21). Структура, що складається з декількох рівнів, виглядає наступним чином:

Згідно з цією схемою, скрипт [main1.php] повинен звертатися лише до шару [métier]. Він не повинен звертатися до шару [dao], навіть якщо це теоретично можливо.
Результати виконання такі:
[Métier] : récupération de la personne d'identifiant 4
[Dao1] : Récupération de la personne d'identité (4) en base de données [locale]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao1] : Sauvegarde de la personne [Personne(4)] en base de données [locale]
Коментарі
- рядок 10 коду спричинив запис рядків 1 і 2 результатів;
- рядок 20 коду спричинив запис рядка 3 результатів;
- рядок 21 коду спричинив запис рядків 4 та 5 результатів;
Скрипт [main2.php] має такий вигляд:
<?php
// суворе дотримання параметрів функцій
declare (strict_types=1);
// включення класів та інтерфейсів
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao2.php";
require_once __DIR__."/Métier.php";
// тестування ----------------
// створення шарів
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// використання шару [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
Коментарі
- рядки 36–38: шарова структура тепер використовує шар [Dao2];
Результати виконання такі:
[Métier] : récupération de la personne d'identifiant 4
[Dao2] : Récupération de la personne d'identité (4) en base de données [distante]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao2] : Sauvegarde de la personne [Personne(4)] en base de données [distante]
Шар [Dao1] імітує доступ до локальної бази даних, тоді як шар [Dao2] імітує доступ до віддаленої бази даних. Оскільки обидва ці шари відповідають інтерфейсу [IDao], бачимо, що код шару [Métier] не потрібно було змінювати.
Ми застосуємо те, що щойно вивчили, до вправи з розрахунку податків, яка слугує нам основною темою.