23. Практичне завдання — версія 12
У цьому розділі ми напишемо веб-додаток, що відповідає архітектурі MVC (Модель-Вигляд-Контролер). Додаток зможе видавати відповіді у трьох форматах: jSON, XML, HTML. Існує значний стрибок у складності між тим, що ми будемо робити зараз, і тим, що робили раніше. Ми будемо використовувати більшість концепцій, розглянутих до цього моменту, і детально розглянемо всі етапи, що ведуть до створення кінцевого додатка.
23.1. Архітектура MVC
Ми реалізуємо архітектурну модель, відому як MVC (Модель – Вигляд – Контролер), таким чином:

Обробка запиту клієнта відбуватиметься наступним чином:
- 1 — запит
Запити URL матимуть такий виглядhttp://machine:port/contexte/….?action=uneAction¶m1=v1¶m2=v2&… [Contrôleur principal] використовуватиме файл конфігурації для «маршрутизації» запиту до відповідного контролера та відповідної дії в межах цього контролера. Для цього він використовуватиме поле [action] з URL. Решта URL [param1=v1¶m2=v2&…] складається з необов’язкових параметрів, які будуть передані до дії. Значенням параметра C у MVC тут є рядок [Contrôleur principal, Contrôleur / Action]. Якщо жоден контролер не може обробити запитувану дію, веб-сервер відповість, що запитувана дія URL не знайдена.
- 2 — обробка
- обрана дія [2a] може використовувати параметри parami, які їй передала дія [Contrôleur principal]. Вони можуть походити з кількох джерел:
- з шляху [/param1/param2/…] об’єкта URL,
- з параметрів [param1=v1¶m2=v2] від URL,
- з параметрів, надісланих браузером разом із запитом;
- під час обробки запиту користувача дії може знадобитися рівень [métier] [2b]. Після обробки запиту клієнта ця дія може викликати різні відповіді. Типовим прикладом є:
- відповідь про помилку, якщо запит не вдалося обробити належним чином;
- у іншому випадку — підтверджувальний відповідь;
- [Contrôleur / Action] надішле свою відповідь [2c] головному контролеру разом із кодом стану. Ці коди стану однозначно відображатимуть стан, у якому перебуває додаток. Це будуть або коди успіху, або коди помилок;
- обрана дія [2a] може використовувати параметри parami, які їй передала дія [Contrôleur principal]. Вони можуть походити з кількох джерел:
- 3 — відповідь
- залежно від того, чи клієнт запросив відповідь jSON, XML чи HTML, [Contrôleur principal] створить екземпляр відповідного типу відповіді [3a] і попросить його надіслати відповідь клієнту. [Contrôleur principal] передасть йому як відповідь, так і код стану, надані [Contrôleur / Action], який було виконано;
- якщо бажана відповідь має тип jSON або XML, вибрана відповідь відформатує відповідь від [Contrôleur / Action], яку їй передали, і надішле її [3c]. Клієнтом, здатним обробити цю відповідь, може бути консольний скрипт PHP або скрипт JavaScript, розміщений на сторінці HTML;
- якщо бажана відповідь має тип HTML, то обрана відповідь [3b] вибере один із переглядів HTML або [Vuei] за допомогою наданого їй коду стану. Це V з MVC. Кожному коду стану відповідає лише один вигляд. Цей вигляд V відобразить відповідь [Contrôleur / Action], який було виконано. Вона оформлює дані цієї відповіді за допомогою HTML, CSS та JavaScript. Ці дані називаються моделлю представлення. Це «M» у MVC. Клієнтом, як правило, є браузер;
Тепер уточнимо зв’язок між веб-архітектурою MVC та багаторівневою архітектурою. Залежно від того, як ми визначаємо модель, ці два поняття можуть бути пов’язані або ні. Візьмемо однорівневий веб-додаток MVC:

У наведеному вище прикладі кожен з [Contrôleur / Action] включає частину шарів [métier] та [dao]. У шарі [web] дійсно присутня архітектура MVC, але загалом додаток не має багатошарової архітектури. Тут є лише один шар, який виконує всі функції.
Тепер розглянемо багатошарову веб-архітектуру:

Рівень [web] можна реалізувати, не дотримуючись моделі MVC. У цьому випадку ми маємо багаторівневу архітектуру, але веб-рівень не реалізує модель MVC.
Наприклад, у середовищі .NET цей шар [web]може бути реалізований за допомогою ASP.NET та MVC, і тоді ми отримуємо багатошарову архітектуру з шаром [web] типу MVC. Зробивши це, можна замінити цей шар ASP.NET MVC на класичний шар ASP.NET (WebForms), зберігаючи решту (бізнес-логіка, DAO, драйвер) без змін. У результаті ми отримуємо багаторівневу архітектуру з рівнем [web], який більше не є типом MVC.
У MVC ми зазначили, що модель M відповідає представленню V, c.a.d, тобто сукупності даних, що відображаються представленням V. Наводимо ще одне визначення моделі M для MVC:

Багато авторів вважають, що те, що знаходиться праворуч від шару [web], утворює модель M для MVC. Щоб уникнути двозначностей, можна говорити про:
- про модель домену, коли мається на увазі все, що знаходиться праворуч від шару [web];
- про модель подання, коли йдеться про дані, що відображаються поданням V;
23.2. Дерево проекту NetBeans
Для проекту NetBeans ми застосуємо архітектуру, що відображає модель MVC:

- [3]: [main.php] є головним контролером нашої моделі MVC. Це C у MVC;
- [4]: папка [Controllers] міститиме вторинні контролери. Кожен з них обробляє певну дію. Ця дія вказана у файлі URL, наприклад, […/main.php?action=authentifier-utilisateur]. Завдяки цій дії [Contrôleur principal] та [main.php] виберуть [Contrôleur secondaire], у даному випадку [AuthentifierUtilisateurController], для обробки запитуваної дії. Ці контролери також входять до складу C об’єкта MVC;
- [5]: папка [Model] міститиме шари [métier] та [dao] додатка. Відповідно до раніше прийнятих термінів, ці елементи представляють модель домену, а згідно з термінологією, прийнятою для M, можуть представляти M у MVC;
- [6]: папка [Responses] містить класи, відповідальні за надсилання відповіді клієнту. Для кожного типу бажаної відповіді існує окремий клас:
- [JsonResponse]: для відповіді jSON;
- [XmlResponse]: для відповіді XML;
- [HtmlResponse]: для відповіді HTML;
- [7]: папка [Views] містить перегляди HTML, коли потрібна відповідь HTML. Це V у MVC. Вони активуються класом [HtmlResponse], який передає їм дані для відображення. Ці дані є шаблоном подання. Відповідно до термінології, прийнятої для M, ці дані можуть бути M у MVC;
- [8]: папка [Utilities] містить утиліти:
- [Logger]: клас, що дозволяє вести журнали у текстовому файлі;
- [Sendmail]: клас, що дозволяє надсилати електронні листи;
- [9]: папка [Logs] містить файл журналу [logs.txt];
- [10]: папка [Entities] містить класи, що використовуються різними контролерами;
За допомогою цієї структури можна описати хід обробки дії, запитуваної клієнтом:
- [main.php] [3] отримує запит;
- після проведення кількох попередніх перевірок (чи входить ця дія до переліку дозволених дій?) він передає запит вторинному контролеру [4], відповідальному за обробку цієї дії;
- вторинний контролер виконує свої обов’язки. У своїй роботі йому можуть знадобитися рівні [métier], [dao] та [5], а також об’єкти з папки [10]. Він надсилає свою відповідь головному контролеру [main.php], який його активував;
- залежно від типу відповіді [jSON, XML, HTML], бажаного клієнтом, головний контролер [main.php] активує одну з відповідей з папки [Responses] [6];
- відповіді [JsonResponse, XmlResponse] надсилають клієнту відповідно відповідь jSON або XML;
- відповідь [HtmlResponse] використовує один із переглядів папки [Views] [7] для надсилання відповіді HTML клієнту;
- різні контролери мають доступ до класу [Logger] з папки [8] для запису журналів у файл журналів папки [9]. У журнал записуються:
- запитана дія;
- відповідь відповідного контролера. Вона записується у форматі jSON незалежно від запитуваного типу [jSON, XML, HTML];
- у разі виникнення фатальної помилки (HTTP_INTERNAL_SERVER_ERROR) головний контролер [main.php] надсилає електронного листа адміністратору за допомогою класу [SendMail] із папки [8];
23.3. Дії додатка
Клієнт передає веб-серверу дію, яку потрібно виконати, у вигляді параметра [action] у URL [/main.php?action=xxx]. Дозволені дії перелічені у файлі [config.json], який налаштовує головний контролер [main.php]:
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
- рядок 1: ключ [actions] зі словника jSON;
- рядки 3–9: словник [action:contrôleur]. З кожною дією пов’язаний допоміжний контролер, який відповідає за її обробку;
- рядок 3: [init-session]: запускає сесію моделювання розрахунків податків. Ця дія вказує на тип бажаних відповідей [jSON, XML, HTML];
- рядок 4: після визначення типу сесії клієнт повинен пройти аутентифікацію за допомогою дії [authentifier-utilisateur]. Доки він не пройде аутентифікацію, всі інші дії заборонені, за винятком [init-session];
- рядок 5: після аутентифікації клієнт зможе виконати серію розрахунків податку за допомогою дії [calculer-impot];
- рядок 6: у будь-який момент клієнт може переглянути список своїх моделювань за допомогою дії [lister-simulations];
- рядок 7: він зможе видалити деякі з них за допомогою дії [supprimer-simulation];
- рядок 8: клієнт завершує сесію моделювання за допомогою дії [fin-session]. З цього моменту йому доведеться знову пройти аутентифікацію, якщо він захоче скористатися додатком;
- рядок 9: у додатку HTML дія [afficher-calcul-impot] викликає відображення форми для розрахунку податку;
23.4. Налаштування веб-додатку
Додаток налаштовується за допомогою таких файлів: jSON та [config.json]:
{
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/Simulation.php",
"/Entities/Database.php",
"/Entities/TaxAdminData.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Logger.php",
"/Utilities/SendAdminMail.php",
"/Model/InterfaceServerDao.php",
"/Model/ServerDao.php",
"/Model/ServerDaoWithSession.php",
"/Model/InterfaceServerMetier.php",
"/Model/ServerMetier.php",
"/Responses/InterfaceResponse.php",
"/Responses/ParentResponse.php",
"/Responses/JsonResponse.php",
"/Responses/XmlResponse.php",
"/Responses/HtmlResponse.php",
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
"users": [
{
"login": "admin",
"passwd": "admin"
}
],
"adminMail": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "plantage du serveur de calcul d'impôts",
"tls": "FALSE",
"attachments": []
},
"logsFilename": "Logs/logs.txt",
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
Коментарі
- рядок 2: ім’я файлу jSON, що містить налаштування доступу до бази даних;
- рядки 3–39: налаштування залежностей проєкту. Тут перелічено всі скрипти PHP із структури проєкту;
- рядки 40–44: користувач, уповноважений користуватися додатком;
- рядки 46–54: адреса електронної пошти адміністратора додатка;
- рядок 55: шлях до файлу журналів;
- рядки 56–65: асоціації [action => contrôleur secondaire chargé de la traiter];
- рядки 66–70: асоціації [type de réponse => classe Response chargée d’envoyer la réponse au client];
- рядки 71–75: асоціації [vue HTML => tableau des codes d’état menant à cette vue];
- рядок 76: погляд [vue-erreurs] відображається у сеансі HTML щоразу, коли трапляється нестандартна помилка:
- зазвичай запит до додатка jSON або XML надсилається за допомогою запрограмованого клієнта. Клієнт передає серверу параметри, які можуть бути відсутніми або помилковими. Усі контролери обробляють ці випадки та повертають клієнту коди помилок. Усі можливі випадки помилок мають бути оброблені;
- а з додатком HTML ситуація дещо інша. За звичайних умов веб-додаток використовує лише частину можливих варіантів використання клієнтами jSON та XML. Розглянемо приклад: дія [calculer-impot] очікує на три параметри, надіслані методом POST (від POST): [marié, enfants, salaire].
- якщо у нас є клієнт jSON, що дозволяє вводити URL вручну, можна запросити дію [calculer-impot] із GET замість POST, або з POST без жодного параметра, хоча їх має бути три, тощо… Сервер jSON повинен обробляти всі ці випадки;
- У веб-додатку запит на дію [calculer-impot] буде надсилатися з веб-форми, де жоден із двох попередніх випадків не буде можливим: дія [calculer-impot] буде викликана разом із POST та трьома параметрами [marié, enfants, salaire]. Деякі з цих параметрів можуть мати неправильне значення, але вони будуть присутні. Однак користувач може відтворити певні помилки, самостійно вводячи URL у браузері. З міркувань безпеки цей випадок слід врахувати;
- погляд [vue-erreurs] відображатиметься щоразу, коли вторинний контролер поверне код стану, несумісний із веб-додатком, тобто код стану, якого немає у рядках 72–74 файлу конфігурації. Ми обираємо це рішення з педагогічних міркувань. Іншим можливим варіантом було б нічого не робити і просто повторно відобразити вигляд, який наразі відображається у браузері клієнта, щоб у користувача склалося враження, що сервер не відповідає на його власноруч створені запити URL;
23.5. Встановлення інструментів та бібліотек
23.5.1. Postman
[Postman] — це інструмент, який дозволить нам надсилати запити до різних URL нашого веб-додатку. Він дає нам змогу:
- використовувати будь-які URL: вони створені вручну;
- надсилати запити до веб-сервера за допомогою GET, POST, PUT, OPTIONS…;
- вказати параметри GET або POST;
- визначити заголовки HTTP запиту;
- отримати відповідь у форматі jSON, XML, HTML,
- отримати доступ до заголовків HTTP відповіді. Таким чином, ми отримуємо доступ до повної відповіді HTTP від сервера;
Оскільки ми вручну створюємо запити URL, ми зможемо перевірити всі можливі випадки помилок і побачити, як реагує сервер.
[Postman] доступний за адресою URL [https://www.getpostman.com/downloads/]. Версія, доступна в червні 2019 року, — 7.2. Ця версія має аномалію: під час послідовних запитів до веб-сервера клієнт [Postman 7.2] не повертає автоматично файли cookie, які сервер йому надсилає, зокрема сесійний файл cookie. Щоб зберегти сесію, доводиться вручну вписувати сесійний файл cookie в заголовки HTTP наступних запитів. Це не дуже складно, але не дуже зручно. Ця помилка не була присутня в попередніх версіях. Усвідомлюючи наявність цього багу, команда [Postman] виправила його в альфа-версії (яка може бути нестабільною) під назвою [Postman Canary], доступній на URL [https://www.getpostman.com/downloads/canary]. Саме ця версія використовується в даному випадку. Ми опишемо процес її встановлення. Якщо доступна стабільна версія [Postman 7.3] або пізніша, ви можете завантажити її: ймовірно, помилка вже виправлена.
Проведіть встановлення вашої версії [Postman]. Під час встановлення вас попросять створити обліковий запис: він тут не знадобиться. Обліковий запис [Postman] призначений для синхронізації різних пристроїв, щоб налаштування одного з них були скопійовані на інший. Ніщо з цього тут не знадобиться.
Після встановлення [Postman] відображає такий інтерфейс:

- у [2-3] можна отримати доступ до налаштувань продукту;

- у [6] — версія, що використовується в цьому документі;
- якщо ви створили обліковий запис, відбувається синхронізація між вашим комп’ютером та віддаленим сервером [Postman]. Це символізує колесо [7], яке обертається щоразу, коли ви вносите зміни до проєкту [Postman]. Щоб зупинити цю непотрібну синхронізацію, вийдіть із системи в [8-9];
23.5.2. Бібліотека Symfony / Serializer
Для серіалізації об’єктів у jSON та XML ми будемо використовувати бібліотеку [Symfony / Serializer]. Вона має тут дві переваги:
- вона є уніфікованою у використанні для серіалізації у формати jSON або XML: це дозволяє уникнути необхідності вивчати дві різні бібліотеки API (інтерфейс прикладного програмування);
- вона від початку вміє серіалізувати об’єкти у форматі jSON або XML, навіть якщо їхні атрибути є приватними. Нагадаємо, що в jSON для серіалізації об’єкта його клас мав реалізовувати інтерфейс [\JsonSerializable]. У результаті отримувався рядок jSON, що представляв асоціативний масив, ключами якого були атрибути класу. Під час десеріалізації цього рядка jSON отримували примітивний асоціативний масив, який потім потрібно було перетворити на об’єкт класу, що підлягав серіалізації. З [Symfony / Serializer] десеріалізація відразу ж створює об’єкт серіалізованого класу. Це простіше;
Документація до бібліотеки [Symfony / Serializer] доступна за посиланням: URL : [https://symfony.com/doc/current/components/serializer.html] (червень 2019 року).
Щоб встановити цю бібліотеку, відкрийте термінал Laragon (див. посилання в параграфі) і введіть таку команду:

- [1] — команда для встановлення бібліотеки [symfony/serializer];
- [2] — ще одна бібліотека, необхідна для нашого проєкту: забезпечує серіалізацію об’єктів;

23.6. Елементи додатка

Елементи [BaseEntity, Database, ExceptionImpots, TaxAdminData] використовувалися вже з версії 08 веб-сервісу (див. параграф «посилання»).
Клас [Simulation] слугуватиме для інкапсуляції елементів симуляції розрахунку податку:
<?php
namespace Application;
class Simulation extends BaseEntity {
// атрибути симуляції розрахунку податку
protected $marié;
protected $enfants;
protected $salaire;
protected $impôt;
protected $surcôte;
protected $décôte;
protected $réduction;
protected $taux;
// гетери
public function getMarié() {
return $this->marié;
}
public function getEnfants() {
return $this->enfants;
}
public function getSalaire() {
return $this->salaire;
}
public function getImpôt() {
return $this->impôt;
}
public function getSurcôte() {
return $this->surcôte;
}
public function getDécôte() {
return $this->décôte;
}
public function getRéduction() {
return $this->réduction;
}
public function getTaux() {
return $this->taux;
}
}
Коментарі
- рядок 5: клас [Simulation] розширює клас [BaseEntity] і, отже, успадковує такі методи:
- [setFromArrayOfAttributes($arrayOfAttributes)]: що дозволяє ініціалізувати атрибути класу;
- [__toString]: який повертає рядок jSON об’єкта;
- рядки 7–14: атрибути симуляції;
- рядки 16–47: методи-гетери класу;
23.7. Допоміжні функції додатка
![]()
Клас [Logger] дозволяє реєструвати події у текстовому файлі. Цей клас описано у розділі посилання.
Клас [SendAdminMail] дозволяє надсилати електронні листи адміністратору додатка. Цей клас описано у розділі за посиланням.
23.8. Шари [métier] та [dao]


Класи та інтерфейси шарів [métier] та [dao] зібрані у папці [Model]. Усі вони були визначені та використовувалися в попередніх версіях:
ExceptionImpots | Клас винятків, що генеруються шаром [dao]. Визначено в розділі «Посилання». |
InterfaceServerDao | Інтерфейс, реалізований шаром [dao] сервера. Визначено у розділі «Посилання». |
ServerDao | Реалізація інтерфейсу [InterfaceServerDao]. Реалізує рівень [dao] сервера. Визначено у розділі посилання. |
ServerDaoWithSession | Реалізація інтерфейсу [InterfaceServerDao]. Реалізує рівень [dao] сервера. Визначено у розділі «Посилання». |
InterfaceServerMetier | Інтерфейс, реалізований рівнем [métier] сервера. Визначено в розділі «Посилання». |
ServerMetier | Реалізація інтерфейсу [InterfaceMetier]. Реалізує рівень [metier] сервера. Визначено в розділі «Посилання». |
Додаток, що зараз розробляється, широко використовує елементи, які вже були представлені та застосовувалися:
- шари [métier] та [dao];
- утиліти [Logger] та [SendAdminMail];
- об’єкти [ExceptionImpots, TaxAdminData, Database];
Ми зосередимося на шарі [web] додатка:

23.9. Головний контролер [main.php]
23.9.1. Вступ

- [1-2]: головний контролер [main.php] [1] налаштовується за допомогою файлу [config.json] [2];
Нагадаємо про місце головного контролера в нашій архітектурі MVC:

У [1] головний контролер [main.php] є першим елементом архітектури MVC, який обробляє запит клієнта. Він виконує кілька функцій:
- спочатку він виконує базові перевірки:
- чи існує його файл конфігурації та чи є він дійсним;
- завантажує всі залежності проєкту. Це означає завантаження всіх елементів архітектури MVC;
- чи було вказано запитувану дію? Якщо так, чи є вона дійсною?
- якщо запитувана дія є дійсною, вибрати [2a] вторинний контролер, який її опрацює, та передати йому необхідну інформацію: запит HTTP, сесію, конфігурацію додатка;
- отримати [2c] відповідь від вторинного контролера. Залежно від типу (jSON, XML, HTML) додатка, який запитував клієнт, вибрати [3a] відповідь (JsonResponse, XmlResponse, HtmlResponse), яка відповідає за надсилання відповіді клієнту, та передати їй усю необхідну інформацію (запит HTTP, сесію, конфігурацію додатка, відповідь вторинного контролера);
- після надсилання цієї відповіді [3c] звільнити ресурси, які могли бути задіяні для обробки запиту;
23.9.2. [main.php] - 1
Код головного контролера [main.php] є таким:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// обробка помилок за допомогою PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// отримано конфігурацію
$configFilename = "config.json";
$fileContents = \file_get_contents($configFilename);
$erreur = FALSE;
// помилка?
if (!$fileContents) {
// фіксуємо помилку
$état = 131;
$erreur = TRUE;
$message = "Le fichier de configuration [$configFilename] n'existe pas";
}
if (!$erreur) {
// завантажуємо код JSON із файлу конфігурації в асоціативний масив
$config = \json_decode($fileContents, true);
// помилка?
if (!$config) {
// зафіксовано помилку
$erreur = TRUE;
$état = 132;
$message = "Le fichier de configuration [$configFilename] n'a pu être exploité correctement";
}
}
// помилка?
if ($erreur) {
// підготовка відповіді JSON від сервера
// неможливо скористатися файлом конфігурації
// залежності Symfony
require_once "C:/myprograms/laragon-lite/www/vendor/autoload.php";
// підготовка відповіді
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// код статусу
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
// вміст
$response->setContent(json_encode(["action" => "", "état" => $état, "réponse" => $message], JSON_UNESCAPED_UNICODE));
// відправлення
$response->send();
// завершення
exit;
}
…
Коментарі
- рядки 10–12: головний контролер використовує такі об’єкти Symfony:
- [Request]: запит HTTP, що обробляється;
- [Session]: сесія веб-додатку;
- [Response]: відповідь HTTP клієнту;
- рядок 15: протягом усього процесу розробки цей рядок залишатиметься у коментарях: помилки PHP у цьому випадку включаються до текстового потоку, що надсилається клієнту. Якщо клієнтом є браузер, це дозволяє бачити помилки, що виникають на сервері. Це допомагає у налагодженні;
- рядок 16: повідомляються всі помилки (E_ALL), крім попереджень (! E_WARNING) та некритичних повідомлень (! E_NOTICE). Наприклад, якщо файл не вдається відкрити, PHP видає помилку типу [E_NOTICE]. Якщо у рядку 15 дозволено відображення помилок, помилка відкриття файлу з’являється у браузері клієнта. Це добре, якщо ви забули перевірити результат відкриття файлу, але гірше, якщо ви запланували таку перевірку: у такому разі рядок [notice] захаращує відповідь сервера клієнту. На етапі розробки рядок 16 також слід закомментувати: ви ж не хочете пропустити жодної помилки;
- рядок 19: зчитується файл конфігурації;
- рядки 22–27: якщо читання пройшло невдало, фіксується помилка (рядок 25), додаток переводиться у стан [131] і готується повідомлення про помилку;
- рядок 30: декодується рядок jSON із конфігураційного файлу;
- рядки 32–37: якщо декодування завершилося невдало, фіксуємо помилку (рядок 34), переводимо додаток у стан [132] і готуємо повідомлення про помилку;
- рядки 40–57: у разі помилки під час читання конфігураційного файлу подальша робота неможлива. Тоді готується відповідь jSON для клієнта:
- рядок 44: оскільки файл конфігурації не було прочитано, необхідно вручну імпортувати файл [autoload], необхідний для [Symfony];
- рядки 46–47: готуємо відповідь jSON;
- рядок 50: код HTTP відповіді становитиме 500 INTERNAL_SERVER_ERROR;
- рядок 52: встановлюємо зміст відповіді jSON. Усі відповіді, що надаються досліджуваним веб-додатком, матимуть три ключі:
- [action]: дія, яку запитує клієнт;
- [état] — стан додатка після виконання цієї дії;
- [réponse]: відповідь веб-сервера;
- рядок 54: відповідь jSON надсилається клієнту;
23.9.3. Тести [Postman] - 1
Ми перевіримо поведінку сервера, коли файл конфігурації відсутній або містить помилки:

Ми об’єднаємо різні запити, які наш клієнт [Postman] надсилатиме до податкового сервера, у колекції.
- У [1] створіть нову колекцію;
- у [2] надайте їй ім’я;
- у [3] опис є необов’язковим;

- у колекціях [4] тепер з’являється колекція з назвою [impots-server-tests-version12] [5];
- у [6] можна додати новий запит до колекції;

- у [7] запиту надається ім’я;
- у [8] опис є необов’язковим;

- у [9-11] запит додано до колекції;
- у [12] — вибір типу запиту, тут — запит [GET]. У [19] — різні доступні типи запитів;
- у [13] тут вводиться URL сервера;
- у [14] тут вказуються параметри, додані до URL, які, отже, стануть параметрами GET. Перевага введення їх саме тут, а не безпосередньо в URL, полягає в тому, що вони будуть URL-кодовані [Postman]. Якщо ви введете їх самостійно в URL, вам доведеться самостійно виконати їх URL-кодування;
- у [15] файл [Authorization] слугує для визначення користувача, який буде входити в систему. Нам не доведеться використовувати цю можливість;
- у [16] — заголовки HTTP, які супроводжуватимуть запит. Деякі заголовки автоматично включаються до запиту. Тут ви можете додати нові;
- У [17], [Body] позначають параметри операції [POST]. Нам доведеться скористатися цією опцією;
Ми проведемо такий тест:
- у [main.php] вказано, що файлом конфігурації є [config2.json], якого не існує:

- 16-й рядок коду потрібно розкоментувати;
- рядок 18: помилка в імені конфігураційного файлу;
Перейдемо до [Postman] [13, 20], URL веб-сервера розрахунку податків і запустимо [21]:

Відповідь, повернута сервером (звичайно, Laragon має бути активним), така:

- у [22] сервер повернув код HTTP [500 Internal Server Error];
- у [23] [Body] позначає тіло відповіді, тобто документ, надісланий сервером за заголовками HTTP [28];
- у [26] видно, що [Postman] отримав відповідь jSON;
- у [27] — відформатована відповідь jSON;
- у [28] — відповідь jSON у необробленому вигляді без форматування;
- у [29] режим [Preview] використовується, коли відповідь має формат HTML. У цьому випадку режим [Preview] відображає отриману сторінку;
- у [30] — відповідь jSON від сервера. Це саме та відповідь, на яку ми чекали;
У режимі [25] заголовки HTTP, надіслані у відповіді сервера, є такими:

- у [32] — тип відповіді jSON;
Цей перший тест дозволив нам переконатися, що:
- можна надсилати будь-який тип запиту на тестований сервер;
- можна задавати параметри GET або POST;
- отримано повну відповідь: заголовки HTTP та документ, що йде за цими заголовками [Body];
Тепер проведемо другий тест:

- у [1-3] файл [config3.json] є синтаксично некоректним файлом jSON;
- у [4] файл [main.php] налаштований на використання [config3.json];
Ми додаємо новий запит у [Postman]:

- У [1-3] клацаємо правою кнопкою миші на [2] і вибираємо опцію [duplicate], щоб продублювати запит [2];
- у [4] новий запит має попередньо визначену назву, яку змінюємо на [5];

- в [6] — перейменований запит;
- в [9-10] надсилається той самий запит GET, що й раніше;

- у [11] — відповідь сервера jSON;
Тут ми показали, як будуть тестуватися різні дії веб-сервісу з розрахунку податку.
23.9.4. [main.php] – 2
Продовжуємо розгляд коду головного контролера [main.php]:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// обробка помилок за допомогою PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// отримання конфігурації
$configFilename = "config.json";
…
// додаємо необхідні для скрипта залежності
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require_once "$rootDirectory$dependency";
}
// абсолютні залежності (сторонні бібліотеки)
foreach ($config["absoluteDependencies"] as $dependency) {
require_once "$dependency";
}
// створення файлу журналу
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// не вдалося створити файл журналу — внутрішня помилка сервера
$état = 133;
(new JsonResponse())->send(
NULL, NULL, $config,
Response::HTTP_INTERNAL_SERVER_ERROR,
["action" => "non déterminée", "état" => $état, "réponse" => "Le fichier de logs [{$config['logsFilename']}] n'a pu être créé"],
[]);
// завершено
exit;
}
Коментарі
- рядок 18: ми маємо файл конфігурації [config.json], який тепер існує і є синтаксично правильним. Крім того, слід перевірити, чи дійсно присутні очікувані ключі в цьому файлі. Ми вважатимемо, що це є частиною звичайної роботи розробника з налагодження. Ми могли б застосувати те саме міркування до двох попередніх помилок;
- рядки 20–28: ми включаємо всі необхідні залежності для веб-проєкту. Ми вже неодноразово зустрічали цей код;
- рядки 31–43: ми намагаємося створити об’єкт [Logger], який дозволить нам реєструвати події у файлі [$config['logsFilename']]. Створення цього об’єкта може завершитися невдачею;
- рядки 33–43: обробка помилки під час створення об’єкта [Logger];
- рядок 35: встановлюємо номер стану;
- рядки 36–40: надсилається відповідь jSON;
- рядок 42: зупинка скрипта;
Усі відповіді, що надсилаються клієнту, реалізують наступний інтерфейс [InterfaceResponse]:

Код інтерфейсу [InterfaceResponse] такий:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Запит $request: запит обробляється
// Сесія $session: сесія веб-додатку
// масив $config: конфігурація додатка
// int statusCode: код статусу відповіді HTTP
// масив $content: відповідь сервера
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
// Logger $logger: програма для запису журналів
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
- рядки 19–27: інтерфейс [InterfaceResponse] має єдиний метод [send] для надсилання відповіді клієнту;
- рядки 11–17: значення різних параметрів методу [send];
- рядки 23–25: параметри [$statusCode, $content, $headers] містяться у стандартному результаті вторинних контролерів додатка. Однак відповідь може потребувати додаткової інформації. Тому їй передаються перші три параметри (рядки 20–22), які надають доступ до повної інформації щодо запиту, сесії та конфігурації;
- рядок 26: відповіді потрібен параметр [Logger], оскільки вона буде реєструвати відповідь, надіслану клієнту;
Клас [JsonResponse] реалізує інтерфейс [InterfaceResponse] наступним чином:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// Запит $request: запит обробляється
// Сесія $session: сесія веб-додатку
// масив $config: конфігурація додатка
// int statusCode: код статусу відповіді HTTP
// масив $content: відповідь сервера
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
// Logger $logger: логер для запису журналів
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// підготовка серіалізатора Symfony
$serializer = new Serializer(
[
// необхідний для серіалізації об’єктів
new ObjectNormalizer()],
// кодер jSON
// для параметрів, вкажіть OU між різними параметрами
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// серіалізація jSON
$json = $serializer->serialize($content, 'json');
// заголовки
$headers = array_merge($headers, ["content-type" => "application/json"]);
// відправлення відповіді
parent::sendResponse($statusCode, $json, $headers);
// журнал
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
Коментарі
- рядок 13: клас реалізує інтерфейс [InterfaceResponse];
- рядок 13: клас успадковує клас [ParentResponse]. Усі типи [Response] успадковують цей клас. Саме цей батьківський клас надсилає відповідь клієнту (рядок 46). Оскільки цей код був спільним для всіх типів [Response], його було винесено в батьківський клас;
- рядки 33–40: створення екземпляра серіалізатора [Symfony], який перетворить відповідь сервера [$content] на рядок jSON (рядок 42);
- рядки 34–36: перший параметр конструктора [Serializer] — це масив. У нього поміщається екземпляр класу [ObjectNormalizer], необхідний для серіалізації об’єктів. У цьому додатку такий випадок зустрічається у списку симуляцій, де кожна симуляція є екземпляром класу [Simulation];
- рядок 39: другий параметр конструктора [Serializer] також є масивом: туди поміщаються всі кодери, що використовуються під час серіалізації (XML, jSON, CSV…);
- рядок 39: тут буде лише один кодер типу [JsonEncoder]. Конструктора без параметрів могло б вистачити. Тут ми передали конструктору параметр [JsonEncode] виключно для того, щоб передати параметри кодування jSON;
- рядок 39: параметр конструктора [JsonEncode] є масивом опцій. Тут використовується опція [JSON_UNESCAPED_UNICODE], щоб символи UTF-8 у рядку jSON відображалися у вихідному вигляді, а не «ескейпувалися»;
- рядок 42: тіло відповіді HTTP серіалізується в jSON за допомогою попереднього серіалізатора;
- рядок 44: додаємо заголовок HTTP, який повідомляє клієнту, що йому буде надіслано jSON;
- рядок 46: надсилаємо запит до батьківського класу, щоб він відправив відповідь клієнту;
- рядки 48–50: записуємо у журнал відповідь jSON;
Код батьківського класу [ParentResponse] такий:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode: код статусу відповіді HTTP
// рядок $content: тіло відповіді, що надсилається
// залежно від випадку, це рядок jSON, XML, HTML
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// підготовка текстової відповіді сервера
$response = new Response();
$response->setCharset("utf-8");
// код статусу
$response->setStatusCode($statusCode);
// заголовки
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// відправлення відповіді
$response->setContent($content);
$response->send();
}
}
Коментарі
- рядки 10–13: значення трьох параметрів методу [send];
- рядок 17: слід зауважити, що тіло відповіді має тип [string] і, отже, готове до відправлення (рядок 30);
- рядок 22: відповідь міститиме символи UTF-8;
- рядок 24: код статусу HTTP відповіді;
- рядки 26–28: додавання заголовків HTTP, наданих кодом виклику;
- рядки 30–31: надсилання відповіді клієнту;
Ми детально описали весь цикл обробки відповіді jSON. Далі ми не будемо до цього повертатися. Слід лише запам’ятати сигнатуру інтерфейсу [InterfaceResponse]:
interface InterfaceResponse {
// Запит $request: запит обробляється
// Сесія $session: сесія веб-додатку
// масив $config: конфігурація додатка
// int statusCode: код статусу відповіді HTTP
// масив $content: відповідь сервера
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
// Logger $logger: логер для запису журналів
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
Головний контролер [main.php] повинен дотримуватися цього сигнатура щоразу, коли він вимагатиме надсилання відповіді клієнту.
23.9.5. Тести [Postman] – 2
Ми змінюємо файл [config.json] наступним чином:

- на [1], вказуємо, що файлом журналу є [Logs], який є папкою [2]. Отже, створення файлу [Logs] має завершитися невдачею;
Ми створюємо новий запит [Postman] [3], який називається [erreur-133]:

- [2-4]: ми визначаємо той самий запит, що й у двох попередніх тестах;
- [5-7]: ми отримуємо очікуване значення jSON;
23.9.6. [main.php] – 3
Продовжимо вивчення головного контролера [main.php]:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// обробка помилок за допомогою PHP
…
// створення файлу журналу
…
// перший журнал
$logger->write("\n---nouvelle requête\n");
// поточний запит
$request = Request::createFromGlobals();
// сесія
$session = new Session();
$session->start();
// список помилок
$erreurs = [];
$erreur = FALSE;
// обробка запитуваної дії
if (!$request->query->has("action")) {
$erreurs[] = "paramètre [action] manquant";
$erreur = TRUE;
$état = 101;
$action = "";
} else {
// зберігаємо дію
$action = strtolower($request->query->get("action"));
}
// дія заноситься до журналу
$logger->write("action [$action] demandée\n");
// чи існує ця дія?
if (!$erreur && !array_key_exists($action, $config["actions"])) {
$erreurs[] = "action [$action] invalide";
$erreur = TRUE;
$état = 102;
}
// перед виконанням певних дій необхідно знати тип сеансу
if (!$erreur && !$session->has("type") && $action !== "init-session") {
$erreurs[] = "pas de session en cours. Commencer par action [init-session]";
$erreur = TRUE;
$état = 103;
}
// для виконання деяких дій необхідно пройти автентифікацію
if (!$erreur && !$session->has("user") && $action !== "authentifier-utilisateur" && $action !== "init-session") {
$erreurs[] = "action demandée par utilisateur non authentifié";
$erreur = TRUE;
$état = 104;
}
// помилки?
if ($erreurs) {
// відповідь готується, але не надсилається
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// виконується дія за допомогою відповідного контролера
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- надсилається відповідь
// випадок фатальної помилки HTTP_INTERNAL_SERVER_ERROR
// надсилається електронний лист адміністратору, якщо це можливо
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// відповідь залежить від типу сеансу
if ($session->has("type")) {
// тип сеансу вказано в сеансі
$type = $session->get("type");
} else {
// якщо тип у сесії відсутній, то за замовчуванням буде відповідь у форматі jSON
$type = "json";
}
// до відповіді контролера додаються ключі [action, état]
$content = ["action" => $action, "état" => $état] + $content;
// створюється екземпляр об’єкта [Response], який відповідає за відправку відповіді клієнту
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// відповідь надіслано — звільняємо ресурси
$logger->close();
exit;
Коментарі
- після того, як проведені перші перевірки і контролер переконався, що може працювати, він переходить до виконання дії, яку від нього вимагають: вона повинна відповідати певним умовам;
- рядок 21: фіксуємо в журналі, що надійшов новий запит. Раніше цього зробити не можна було, оскільки не було впевненості в наявності дійсного файлу журналу;
- рядок 23: уся інформація про запит клієнта інкапсулюється в об’єкт Symfony [Request];
- рядок 26: запускаємо нову сесію або отримуємо існуючу, якщо вона є;
- рядок 27: сесія активована;
- рядок 29: масив повідомлень про помилки;
- рядок 30: логічне значення, яке під час тестування вказує, чи сталася помилка;
- рядок 32: параметр [action] повинен бути частиною URL у вигляді [main.php?action=uneAction]. Отже, параметр [action] входить до складу параметра [$request→query];
- рядки 33–36: випадок відсутності параметра [action] у URL. Помилка фіксується, і їй присвоюється стан [101];
- рядок 39: якщо параметр [action] присутній у URL, він зберігається;
- рядок 42: тип дії реєструється;
- рядки 45–49: якщо параметр [action] присутній, він повинен бути дійсним. Усі дозволені дії визначені в асоціативній таблиці [$config["actions"]];
- рядки 46–48: якщо дія є недійсною, фіксується помилка та їй присвоюється стан [102];
- рядки 52–56: дія є допустимою. Вона ще має відповідати іншим умовам. Веб-додаток надає три типи відповідей (jSON, XML, HTML). Цей тип визначається дією [init-session]. Ця дія записує тип сесії в ключ [type];
- рядок 52: за винятком дії [init-session], будь-яка інша дія повинна виконуватися з ключем [type] у сесії;
- рядки 53–55: якщо це не так, фіксується помилка та сесії присвоюється стан [103];
- рядки 58–63: за винятком дій [init-session] та [authentifier-utilisateur], усі інші дії повинні виконуватися після автентифікації. Аутентифікація здійснюється за допомогою дії [authentifier-utilisateur], яка в разі успішної аутентифікації встановлює ключ [user] у сесії;
- рядок 59: якщо дія не є ні [init-session], ні [authentifier-utilisateur], і ключ [user] відсутній у сесії, то виникає помилка;
- рядки 60–62: фіксуємо помилку та присвоюємо їй стан [104];
- рядки 66–71: перевіряється, чи масив [$erreurs] не порожній. Якщо так, то запитувана дія або її контекст виконання є помилковими;
- рядки 68–70: готується відповідь для відправлення клієнту, але її ще не надсилають;
- рядок 68: код статусу HTTP;
- рядок 69: тіло відповіді;
- рядок 70: заголовки, які потрібно додати до відповіді, тут їх немає;
- рядок 73: маємо дійсну дію. Попросимо її контролер (вторинний) обробити її;
- рядок 74: формуємо ім’я класу контролера, який потрібно виконати. [__NAMESPACE__] — це простір імен, у якому ми перебуваємо, тут — [Application] (рядок 7);
- імена класів вторинних контролерів містяться у файлі [config.json]:
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
Кожній дії відповідає вторинний контролер. Якщо дія — [authentifier-utilisateur], то змінна [$controller] у рядку 74 матиме значення [Application/AuthentifierUtilisateurController];
- рядок 75: записується ім’я вторинного контролера для перевірки під час розробки;
- рядок 76: виконується вторинний контролер. Ми повернемося до вторинних контролерів трохи пізніше;
- рядок 76: усі вторинні контролери повертають один і той самий тип результату — масив:
- перший елемент масиву [$statusCode] — це код статусу HTTP відповіді, яку потрібно надіслати;
- другий елемент [$état] — це стан додатка після виконання контролера;
- третій елемент [$content] — це асоціативний масив з єдиним ключем [réponse], який є тілом відповіді, що має бути надіслана клієнту;
- четвертий елемент [$headers] — це масив заголовків HTTP, які слід додати до відповіді, що надсилається клієнту;
- рядок 79: тут ми опиняємося:
- або через те, що сталася помилка (рядки 68–70);
- або після виконання контролера (рядки 72–76);
- в обох випадках елементи [$statusCode, $état, $content, $headers], необхідні для формування відповіді клієнту, відомі;
- рядки 82–87: обробляють особливий випадок коду статусу [500 Internal Server Error]. Якщо контролер встановив цей код статусу, це означає, що додаток не може працювати. Наприклад, це стосується розрахунку податку, якщо використовуваний SGBD не був запущений або перестав відповідати. У такому разі адміністратору додатка надсилається електронний лист із відповідним повідомленням. Ми не будемо окремо коментувати цей код. Використання класу [SendAdminMail] вже було описано (абзац із посиланням);
- рядки 89–95: визначається тип [jSON, XML, HTML] веб-додатка. Якщо дія [init-session] була успішно виконана, цей тип знаходиться в сесії, пов’язаній із ключем [type] (рядок 91). Якщо це не так, то для відповіді довільно встановлюється тип jSON (рядок 94);
- рядок 97: [$content] — це масив з єдиним ключем [réponse] та єдиним значенням — тілом відповіді, що має бути надіслано клієнту. До нього додаються ключі [action] та [état]. Ключ [action] дозволить краще відстежувати журнали файлу [logs.txt]. Ключ [état] матиме дві функції:
- вона дозволить клієнтам jSON та XML дізнатися, у який стан виконана дія перевела веб-додаток;
- у разі відповіді HTML він дозволить вибрати представлення HTML, яке потрібно надіслати до браузера клієнта;
- рядок 99: обирається тип класу [Response], який потрібно виконати для надсилання відповіді клієнту;
Ми вже розглядали клас [JsonResponse] у розділі «Посилання». Він реалізує інтерфейс [InterfaceResponse] і розширює клас [ParentResponse]. Те саме стосується двох інших класів — [XmlResponse] та [HtmlResponse].
Відповіді зібрано у папці [Responses]:

Усі ці класи реалізують інтерфейс [InterfaceResponse], який також представлено у розділі за посиланням:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Запит $request: запит обробляється
// Сесія $session: сесія веб-додатку
// масив $config: конфігурація додатка
// int statusCode: код статусу відповіді HTTP
// масив $content: відповідь сервера
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
// Logger $logger: логер для запису журналів
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
Цей інтерфейс має єдиний метод [send], який відповідає за відправку відповіді клієнту. Цей метод має 7 параметрів, описаних у рядках 11–17. Усі класи та інтерфейси папки [Responses] знаходяться в просторі імен [Application] (рядок 3).
Повернемося до коду класу [main.php]:
…
// додаємо ключі [action, état] до відповіді контролера
$content = ["action" => $action, "état" => $état] + $content;
// створюється екземпляр об’єкта [Response], який відповідає за надсилання відповіді клієнту
$response = __NAMESPACE__ . $config["types"][$type];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// відповідь надіслано — звільняємо ресурси
$logger->close();
exit;
- рядок 5: створюється екземпляр класу [Response], який відповідає типу додатка. Ці класи визначені у файлі [config.json] наступним чином:
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
- рядок 5: перед назвою класу стоїть префікс його простору імен;
- рядок 6: створюється екземпляр класу [Response] і викликається його метод [send] із 7 параметрами, які він очікує. Ці параметри відповідають інтерфейсу [InterfaceResponse], який реалізують усі класи відповідей. Це надсилає відповідь клієнту;
- рядок 9: закривається файл журналу;
- рядок 10: головний контролер завершив свою роботу;
23.9.7. Тести [Postman] – 3
Ми протестуємо різні випадки помилок параметра [action] у URL.

- у [1]:
- [erreur-101]: випадок, коли параметр [action] відсутній у URL;
- [erreur-102]: випадок, коли параметр [action] присутній у URL, але не розпізнається;
- [erreur-103]: випадок, коли параметр [action] присутній у URL, розпізнаний, але очікуваний тип відповіді [json, xml, html] не був визначений;
Кожен запит виконується. Ми безпосередньо наводимо отримані результати:
Вище:
- у [2-4] — запит без параметра [action] у URL [4];
- у [5-7] — результат jSON;

Вище:
- у [5-9] — запит із недійсним параметром [action];
- у [10-13] — відповідь jSON;

Вище:
- у [14-19] — дія розпізнана, але тип (json, xml, html) ще не вказано;
- у [20-23] — відповідь jSON від сервера;
23.10. Допоміжні контролери
Кожна дія виконується одним із контролерів з папки [Controllers]:


У загальній архітектурі вищезазначеного додатка вторинні контролери знаходяться у [2a].
Кожен контролер реалізує наступний інтерфейс [InterfaceController]:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceController {
// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos=NULL): array;
}
Коментарі
- усі вторинні контролери виконуються за допомогою методу [execute] у рядку 17. Цьому методу передається інформація, відома головному контролеру:
- рядок 18: [array $config], що інкапсулює конфігурацію додатка;
- рядок 19: [Request $request] — це запит HTTP, що зараз обробляється;
- рядок 20: [Session $session] — поточна сесія веб-додатку;
- рядок 21: [array $infos=NULL] — це додатковий масив інформації для контролера на випадок, якщо перших трьох параметрів методу буде недостатньо. У цьому додатку цей параметр ніколи не використовувався. Він присутній тут з міркувань обережності;
- рядок 21: метод [execute] повертає масив [$statusCode, $état, $content, $headers]
- [int $statusCode]: код статусу відповіді HTTP;
- [int $état]: стан, у якому перебуває додаток наприкінці виконання;
- [array $content]: асоціативний масив [réponse=>résultat], де [résultat] може бути будь-якого типу: це результат, згенерований контролером, який буде надіслано клієнту після серіалізації у вигляді символьного рядка;
- [array $headers]: список заголовків HTTP, які потрібно включити до відповіді HTTP сервера;
Кожен вторинний контролер викликається за допомогою такого коду головного контролера:
// виконується дія за допомогою відповідного контролера
$controller = __NAMESPACE__ . $config["actions"][$action];
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
У рядку 3 видно, що 4-й параметр [array $infos=NULL] методу [execute] не використовується.
23.11. Дії
Тепер розглянемо різні можливі дії веб-сервісу:
Дія | Роль | Контекст виконання |
init-session | Служить для визначення типу (json, xml, html) бажаних відповідей | Запит GET main.php?action=init-session&type=x може бути відправлений у будь-який момент |
аутентифікація користувача | Дозволяє або забороняє користувачеві увійти | Запит POST main.php?action=authentifier-utilisateur Запит повинен містити два параметри, надіслані методом POST [user, password] Може бути відправлений лише у тому випадку, якщо тип сесії (json, xml, html) відомий |
calculer-impot | Виконує симуляцію розрахунку податку | Запит POST main.php?action=calculer-impot Запит повинен містити три параметри, надіслані методом POST: [marié, enfants, salaire] Може бути відправлений лише у разі, якщо тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
lister-simulations | Запит на перегляд списку симуляцій, виконаних з початку сеансу | Запит GET main.php?action=lister-simulations Запит не приймає жодних інших параметрів Може бути відправлений лише за умови, що тип сеансу (json, xml, html) відомий, а користувач пройшов автентифікацію |
видалити-симуляцію | Видаляє симуляцію зі списку симуляцій | Запит GET main.php?action=lister-simulations&numéro=x Запит не приймає жодних інших параметрів Може бути відправлений лише за умови, що тип сеансу (json, xml, html) відомий, а користувач пройшов автентифікацію |
завершення-сесії | Завершує сесію симуляцій. | Технічно старий веб-сеанс видаляється, а створюється новий Може бути відправлено лише в тому випадку, якщо тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
Усі вторинні контролери діють однаково:
- вони перевіряють свої параметри. Ці параметри містяться в об’єкті [Request→query] для параметрів, що містяться в URL, та в об’єкті [Request→request] для тих, що надсилаються (запит POST);
- контролер схожий на функцію або метод, який перевіряє правильність своїх параметрів. Однак у випадку контролера все дещо складніше:
- очікувані параметри можуть бути відсутніми;
- очікувані параметри — це виключно символьні рядки, тоді як функція може задавати тип своїх параметрів. Якщо очікуваний параметр є числом, то потрібно перевірити, чи символьний рядок параметра дійсно відповідає числу;
- після перевірки наявності та синтаксичної правильності очікуваних параметрів потрібно переконатися, що вони є дійсними в поточному контексті виконання. Цей контекст міститься в сесії. Приклад аутентифікації є прикладом контексту виконання. Деякі дії повинні оброблятися лише після аутентифікації клієнта. Зазвичай ключ у сесії вказує, чи відбулася ця аутентифікація чи ні;
- після виконання попередніх перевірок вторинний контролер може розпочати роботу. Ця робота з перевірки параметрів є дуже важливою. Не можна допустити, щоб клієнт надсилав нам що завгодно в будь-який момент життєвого циклу додатка. Ми повинні повністю контролювати його життєвий цикл;
- виконавши свою роботу, вторинний контролер повертає масив [$statusCode, $état, $content, $headers], на який чекає головний контролер, що його викликав;
Тепер ми розглянемо різні контролери або, що те саме, різні дії, які визначають роботу веб-додатка.
23.11.1. Дія [init-session]
Дія [init-session] обробляється наступним контролером [InitSessionController]:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
class InitSessionController implements InterfaceController {
// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// повинен містити GET та один параметр, відмінний від [action]
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
if ($erreur) {
$état = 701;
$message = "méthode GET exigée avec paramètres [action, type] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// отримуємо параметри з GET
$erreur = FALSE;
// тип
if (!$request->query->has("type")) {
$erreur = TRUE;
$état = 702;
$message = "paramètre [type] manquant";
} else {
$type = strtolower($request->query->get("type"));
}
// перевірка типу
if (!$erreur && !array_key_exists($type, $config["types"])) {
$erreur = TRUE;
$état = 703;
$message = "paramètre type [$type] invalide";
}
// помилка?
if ($erreur) {
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// встановлюємо тип сеансу в сеансі
$session->set("type", $type);
// повідомлення про успішне виконання
$message = "session démarrée avec type [$type]";
$état = 700;
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
Коментарі
- очікується запит [GET main.php?action=init-session&type=xxx]
- рядки 25–26: перевіряється, чи запит є запитом GET із двома параметрами в URL;
- рядки 27–31: якщо це не так, фіксується помилка та надсилається результат [$statusCode, $état, $content, $headers] до головного контролера;
- рядки 35–39: перевіряється, чи параметр [type] дійсно присутній у URL. Якщо це не так, фіксується помилка;
- рядок 40: фіксується тип сеансу;
- рядки 43–47: перевіряється, чи тип сесії відповідає одному з термінів (json, xml, html). Якщо це не так, фіксується помилка;
- рядки 49–51: якщо сталася помилка, надсилається результат [$statusCode, $état, $content, $headers] до головного контролера;
- рядок 53: тип сесії записується у сесію веб-додатку;
- рядки 55–57: контролер завершив свою роботу. Головному контролеру надсилається результат успіху [$statusCode, $état, $content, $headers];
Нагадаємо, що робить головний контролер із відповідями від підлеглих контролерів:
// помилки?
if ($erreurs) {
// підготовка відповіді без її відправлення
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// виконуємо дію за допомогою відповідного контролера
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- надсилається відповідь
// випадок фатальної помилки HTTP_INTERNAL_SERVER_ERROR
// надсилається електронний лист адміністратору, якщо це можливо
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// відповідь залежить від типу сеансу
if ($session->has("type")) {
// тип сеансу вказано в сеансі
$type = $session->get("type");
} else {
// якщо тип у сесії відсутній, то за замовчуванням буде відповідь у форматі jSON
$type = "json";
}
// до відповіді контролера додаються ключі [action, état]
$content = ["action" => $action, "état" => $état] + $content;
// створюється екземпляр об’єкта [Response], який відповідає за відправку відповіді клієнту
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// відповідь надіслано — звільняємо ресурси
$logger->close();
exit;
- рядок 12: головний контролер отримує результат від допоміжного контролера;
- рядки 35–36: після проведення кількох перевірок він надсилає відповідь, створюючи екземпляр одного з класів [JsonResponse, XmlResponse, HtmlResponse] залежно від типу (json, xml, html) поточної сесії;
Далі ми проведемо тестування [Postman] у рамках сесії моделювання з типом [json]. Принцип роботи класу [JsonResponse] було описано у розділі за посиланням.
23.11.2. Тести [Postman]

Вище:
- у [2] — три нові тести;
- у [3-7] — дія [init-session] із відсутнім параметром [type];
- у [8-11] — відповідь jSON від сервера;

Вище:
- у [1-7] — дія [init-session] з неправильним параметром [type];
- у [8-11] — відповідь jSON від сервера;

Вище:
- у [1-8] — дія [init-session] з типом jSON;
- у [9-12] — відповідь jSON від сервера;
23.11.3. Дія [authentifier-utilisateur]
Дія [authentifier-utilisateur] виконується наступним контролером [AuthentifierUtilisateurController]:
<?php
namespace Application;
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class AuthentifierUtilisateurController implements InterfaceController {
// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// повинен містити POST та єдиний параметр GET
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
$état = 201;
$message = "méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user,password]";
// результат передається головному контролеру
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// отримуємо параметри з POST
$erreurs = [];
// користувач
$état = 210;
if (!$request->request->has("user")) {
$état += 2;
$erreurs[] = "paramètre [user] manquant";
} else {
$user = $request->request->get("user");
}
// пароль
if (!$request->request->has("password")) {
$état += 4;
$erreurs[] = "paramètre [password] manquant";
} else {
$password = trim($request->request->get("password"));
}
// помилка?
if ($erreurs) {
// результат повертається головному контролеру
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// перевірка облікових даних користувача
// Чи існує користувач?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($user === $users[$i]["login"] && $users[$i]["passwd"] === $password);
$i++;
}
// знайдено?
if (!$trouvé) {
// повідомлення про помилку
$message = "Echec de l'authentification [$user, $password]";
$état = 221;
// результат передається головному контролеру
return [Response::HTTP_UNAUTHORIZED, $état, ["réponse" => $message], []];
} else {
// у сесії відзначається, що користувач пройшов автентифікацію
$session->set("user", TRUE);
// повідомлення про успішне виконання
$message = "Authentification réussie [$user, $password]";
$état = 200;
// повертаємо результат головному контролеру
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
}
Коментарі
- очікується запит [POST main.php?action=authentifier-utilisateur] з двома параметрами, надісланими методом POST [user, password];
- рядки 24–25: перевіряється наявність запиту POST з єдиним параметром у URL;
- рядки 26–31: якщо є помилка, її фіксують і повертають результат [$statusCode, $état, $content, $headers] головному контролеру;
- рядки 36–39: перевіряємо наявність параметра [user] у надісланих значеннях. Якщо його немає, фіксуємо помилку;
- рядки 43–45: перевіряється наявність параметра [password] у відправлених значеннях. Якщо його немає, фіксується помилка;
- рядки 50–53: якщо одне з переданих значень відсутнє, головному контролеру повертається результат [$statusCode, $état, $content, $headers];
- рядки 56–62: перевіряється, чи отримана пара [$user,$password] присутня в масиві [$config[‘users’]] файлу конфігурації;
- рядки 64–69: якщо це не так, фіксується помилка. Код стану HTTP змінюється на [Response::HTTP_UNAUTHORIZED], а результат [$statusCode, $état, $content, $headers] передається головному контролеру;
- рядок 72: автентифікація пройшла успішно. Це фіксується в сесії шляхом додавання до неї ключа [user]. Саме наявність цього ключа свідчить про успішну автентифікацію;
- рядки 73–77: головному контролеру повертається результат успішної аутентифікації [$statusCode, $état, $content, $headers];
23.11.4. Тести [Postman]
Ми проводимо тестування контролера [Postman] у режимі jSON;

Вище:
- у [1-6] — дія [authentifier-utilisateur] із GET [2], тоді як потрібно POST;
- у [7-10] — відповідь сервера jSON;
Замінимо GET на POST [2], не вказуючи параметрів у тілі відповіді [7]:

Вище:
- у [1-7] — POST без параметрів, надісланих у [7];
- у [8-11] — відповідь jSON від сервера;
Тепер додамо параметр [password] у тіло (body) запиту [4]:

Вище:
- у [1-6] — запит POST [2] із параметром [password], надісланим у [4-6]. Параметри, що надсилаються, повинні бути додані до тіла (body) запиту [4]. Існує кілька способів надсилання значень на сервер. Ми обираємо метод [x-www-form-urlencoded] [5];
- у [8-10] — відповідь jSON від сервера;
Тепер визначимо параметр [user] без параметра [password]:

Вище:
- у [1-7] — запит POST без параметра [password] [4-7];
- у [8-11] — відповідь jSON від сервера;
Тепер визначимо два параметри, що надсилаються, [user, password], але зі значеннями, які призводять до невдалої автентифікації:

Вище:
- у [1-9] — запит POST із неправильними параметрами, надісланими методом POST, [user, password];
- у [10-13] — відповідь jSON від сервера. Зверніть увагу на код статусу [401 Unauthorized] [10] у відповіді;
Тепер запит POST із правильними ідентифікаторами:

Вище:
- у [1-9] — запит POST [2] із дійсними ідентифікаторами [6-9];
- у [10-13] — відповідь jSON від сервера. Зверніть увагу на код статусу HTTP [200 OK] у [10];
23.11.5. Дія [calculer-impot]
Дія [calculer-impot] обробляється наступним контролером [CalculerImpotController]:
<?php
namespace Application;
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// псевдонім шару [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class CalculerImpotController implements InterfaceController {
// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// повинен містити один параметр GET та три параметри POST
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
// фіксується помилка
$message = "il faut utiliser la méthode [post] avec [action] dans l'URL et les paramètres postés [marié, enfants, salaire]";
$état = 301;
// результат повертається головному контролеру
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// отримуються параметри з POST
$erreurs = [];
$état = 310;
// сімейний стан
if (!$request->request->has("marié")) {
$état += 2;
$erreurs[] = "paramètre [marié] manquant";
} else {
$marié = trim(strtolower($request->request->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
if ($erreur) {
$état += 4;
$erreurs[] = "valeur [$marié] invalide pour le paramètre [marié]";
}
}
// отримано кількість дітей
if (!$request->request->has("enfants")) {
$état += 8;
$erreurs[] = "paramètre [enfants] manquant";
} else {
$enfants = trim($request->request->get("enfants"));
$erreur = !preg_match("/^\d+$/", $enfants);
if ($erreur) {
$état += 9;
$erreurs[] = "valeur [$enfants] invalide pour le paramètre [enfants]";
}
}
// отримано річний оклад
if (!$request->request->has("salaire")) {
$erreurs[] = "paramètre [salaire] manquant";
$état += 16;
} else {
$salaire = trim($request->request->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
if ($erreur) {
$état += 17;
$erreurs[] = "valeur [$salaire] invalide pour le paramètre [salaire]";
}
}
// помилка?
if ($erreurs) {
// повернення результату головному контролеру
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// у нас є все необхідне для роботи
// Redis
\Predis\Autoloader::register();
try {
// клієнт [predis]
$redis = new \Predis\Client();
// підключаємося до сервера, щоб перевірити, чи він працює
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// щось пішло не так
// повернення результату з помилкою до головного контролера
$état = 350;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// параметри є дійсними
// створення шару [dao]
if (!$redis->get("taxAdminData")) {
try {
// здійснюється запит до бази даних для отримання податкових даних
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// завантажуємо отримані дані в Redis
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// сталася помилка
// повернення результату з помилкою до головного контролера
$état = 340;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// податкові дані зберігаються в пам'яті з обмеженим доступом [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// інстанціювання шару [dao]
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
}
// створення шару [métier]
$métier = new ServerMetier($dao);
// у нас є все необхідне для роботи — розрахунок податку
$résultat = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// додаємо до сесії щойно виконане моделювання
$simulation = new Simulation();
$résultat = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $résultat;
$simulation->setFromArrayOfAttributes($résultat);
// чи існує список моделювань у сесії?
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// додавання симуляції до списку симуляцій
$simulations[] = $simulation;
// повертаємо симуляції до сесії
$session->set("simulations", $simulations);
// повернення результату до головного контролера
$état = 300;
return [Response::HTTP_OK, $état, ["réponse" => $résultat], []];
}
}
Коментарі
- очікуваний запит — [POST main.php?action=calculer-impot] із трьома параметрами, надісланими методом POST у [marié, enfants, salaire]:
- [marié] має містити значення, задане в [oui, non];
- [enfants, salaire] повинні бути додатними цілими числами або нулем;
- рядки 26–27: перевіряється, чи дійсно в URL є POST з єдиним параметром;
- рядки 28–34: якщо це не так, до головного контролера надсилається повідомлення про помилку;
- рядок 36: помилкові повідомлення накопичуються у масиві [$erreurs];
- рядки 39–41: перевіряється наявність параметра [marié]. Якщо його немає, фіксується помилка;
- рядки 43–49: перевіряється, чи значення [marié] міститься в [oui, non]. Якщо це не так, фіксується помилка;
- рядки 51–54: перевіряється наявність параметра [enfants]. Якщо його немає, фіксується помилка;
- рядки 55–61: перевіряється, чи значення параметра [enfants] є додатним числом або нулем. Якщо це не так, фіксується помилка;
- рядки 63–66: перевіряється наявність параметра [salaire]. Якщо його немає, фіксується помилка;
- рядки 67–72: перевіряється, чи значення параметра [salaire] є додатним числом або нулем. Якщо це не так, фіксується помилка;
- рядки 75–78: якщо масив [$erreurs] не порожній, це означає, що сталися помилки. Масив помилок додається до відповіді, а результат повертається головному контролеру;
- рядок 80: параметри є дійсними. Можна обчислити податок. Для цього потрібно створити шари [dao] та [métier], які виконують цей розрахунок;
- рядки 82–94: створюється клієнт [Redis];
- рядки 88–94: якщо не вдалося підключитися до сервера [Redis], клієнту надсилається код [500 Internal Server Error];
- рядок 98: перевіряється, чи має сервер [Redis] ключ [taxAdminData]. Цей ключ представляє дані податкової адміністрації. Якщо ключ відсутній, то податкові дані потрібно шукати в базі даних;
- рядок 101: формування шару [dao], коли податкові дані потрібно взяти з бази. Клас [ServerDaoWithRedis] описано в параграфі «посилання»;
- рядок 103: дані, отримані з бази даних, заносяться до пам’яті [Redis] із ключем [taxAdminData];
- рядки 104–110: якщо пошук у базі даних завершився невдало, фіксується помилка, повернута рівнем [dao], і вона включається до результату, що повертається головному контролеру;
- рядок 109: повідомлення про помилку, повернуте шаром [PDO], закодовано в [iso-8859-1]. Його кодують у [utf-8];
- рядки 111–117: якщо ключ [taxAdminData] існує в пам’яті [Redis], то податкові дані передаються безпосередньо до конструктора шару [dao];
- рядок 119: створюється шар [métier]. Клас [ServerMetier] описано в розділі «Посилання»;
- рядки 124–126: на основі розрахованої суми податку створюється об’єкт [Simulation]. Клас [Simulation] інкапсулює дані моделювання та описаний у розділі «Посилання»;
- рядки 128–132: щойно створене моделювання потрібно додати до списку вже обчислених моделювань. Цей список зберігається в сесії, за винятком випадків, коли ще не було виконано жодного моделювання;
- рядки 133–136: симуляція додається до списку симуляцій, і цей список повертається до сесії;
- рядки 137–139: результат передається головному контролеру;
23.11.6. Тести [Postman]
Ми проводимо тести [Postman] контролера [CalculerImpotController] у режимі jSON;

Вище:
- у [1-7] надсилається запит [GET] замість [POST];
- у [8-11] — відповідь jSON від сервера;
Тепер скористаємося методом [POST] із параметрами, що передаються методом POST, без них, а також із недійсними параметрами:

Вище:
- ми надсилаємо запит [POST] [2] із недійсними параметрами, надісланими методом POST: [6-11] [marié, enfants, salaire]. Один із цих параметрів можна не надсилати, знявши галочку у відповідному полі в [16]. Це дозволить вам протестувати різні варіанти. На знімку екрана вище всі три параметри присутні, і всі вони є недійсними;
- у [12-15] — відповідь сервера jSON;
Тепер знімемо галочки з двох із трьох надісланих параметрів:

Вище,
- у [5-8] відправляється лише параметр [salaire], до того ж він є недійсним;
- у [9-11] — результат jSON від сервера;
Тепер розрахуємо податок із використанням дійсних параметрів:

Вище:
- у [1118] — запит із дійсними параметрами [6-8];
- у [12-14] — відповідь jSON від сервера;
23.11.7. Дія [lister-simulations]
Дія [lister-simulations] обробляється наступним вторинним контролером [ListerSimulationsController]:
<?php
namespace Application;
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class ListerSimulationsController {
// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// повинен бути лише один параметр GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
$état = 501;
$message = "GET requis, avec l'unique paramètre [action] dans l'URL";
// повертається результат з помилкою головному контролеру
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// отримується список симуляцій у сесії
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// повертається результат з успіхом до головного контролера
$état = 500;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
Коментарі
- запит [GET main.php?action=lister-simulations];
- рядки 24–25: перевіряється наявність запиту GET з одним параметром;
- рядки 26–31: якщо це не так, головному контролеру повертається результат з помилкою;
- рядки 33–37: отримується список симуляцій із сесії, якщо він там є (рядок 36), інакше цей список порожній (рядок 34);
- рядки 39–40: список симуляцій передається головному контролеру;
23.11.8. Тести [Postman]
Ми створимо два тести: один із помилкою та один успішний.

Вище:
- у [1-8] надсилається запит [GET] із зайвим параметром [param1] у URL [3, 7-8];
- на запит [9-12] сервер надіслав відповідь jSON;
Тепер надішлімо правильний запит:

Вище:
- [1-5] — правильний запит;
Результат запиту такий:

- у [3-6] — відповідь jSON від сервера. Перед цим тестом тест [Postman] [calculer-impot-300] було виконано кілька разів для створення симуляцій у веб-сесії сервера;
23.11.9. Дія [supprimer-simulation]
Дія [supprimer-simulation] обробляється наступним вторинним контролером [SupprimerSessionController]:
<?php
namespace Application;
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class SupprimerSimulationController {
/// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// має бути два параметри GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
$état = 600;
if ($erreur) {
$état += 2;
$message = "GET requis, avec les paramètres [action, numéro]";
}
// параметр [numéro] повинен існувати
if (!$erreur) {
$état += 4;
$erreur = !$request->query->has("numéro");
if ($erreur) {
$message = "paramètre [numéro] manquant";
}
}
// параметр [numéro] має бути дійсним
if (!$erreur) {
$état += 8;
$numéro = $request->query->get("numéro");
$erreur = !preg_match("/^\d+$/", $numéro);
if ($erreur) {
$message = "paramètre [$numéro] invalide";
}
}
// параметр [numéro] повинен знаходитися в інтервалі [0,n-1]
// якщо n — кількість симуляцій
if (!$erreur) {
$numéro = (int) $numéro;
$erreur = !$session->has("simulations");
if (!$erreur) {
$simulations = $session->get("simulations");
$erreur = $numéro < 0 || $numéro >= count($simulations);
}
if ($erreur) {
$état += 16;
$message = "la simulation n° [$numéro] n'existe pas";
}
}
// помилка?
if ($erreur) {
// результат передається головному контролеру
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// моделювання видаляється $numéro
unset($simulations[$numéro]);
$simulations = array_values($simulations);
// повертаємо симуляції в сесію
$session->set("simulations", $simulations);
// повертаємо список симуляцій клієнту
$état = 600;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
Коментарі
- запит [GET main.php?action=supprimer-simulation&numéro=x];
- рядки 24–30: перевіряється наявність запиту GET із двома параметрами;
- рядки 32–38: перевіряється наявність параметра [numéro] серед параметрів URL;
- рядки 40–47: перевіряється, чи значення параметра [numéro] є синтаксично правильним;
- рядки 50–61: перевіряється, чи дійсно існує симуляція № [numéro]. Існують два випадки помилок:
- список моделювань не знайдено в сесії (рядок 52);
- номер [numéro] симуляції, яку потрібно видалити, відсутній у списку симуляцій;
- рядки 63–66: у разі помилки головному контролеру повертається результат із помилкою;
- рядок 68: симуляція № [numéro] видалена;
- рядок 69: операція [unset] не змінює індекси [0, n-1] у списку. Щоб їх оновити, запитуються значення масиву [$simulations] для усунення відсутнього моделювання;
- рядок 71: новий масив симуляцій повертається в сесію;
- рядки 73–74: передаємо головному контролеру новий список симуляцій;
23.11.10. Тести [Postman]
Ми проведемо тести на помилки та успішне виконання:

Вище:
- у [1-6] — запит GET без параметра [numéro];
- у [7-10] — відповідь jSON від сервера;
Тепер запит із синтаксично неправильним номером:

Вище:
- у [1-5] — запит GET з недійсним параметром [numéro] [3, 5];
- у [6-9] — відповідь jSON від сервера;
Тепер запит із номером симуляції, який не існує:

Вище:
- у [1-5] — запит із номером симуляції, рівним 100, якого немає у списку симуляцій;
- у [6-9] — відповідь jSON від сервера;
Тепер ми видалимо симуляцію № 0 зі списку, тобто першу симуляцію. Спочатку знову запитаємо цей список за допомогою запиту [lister-simulations-500]:

- У [1] наразі є 2 симуляції;
Видаляємо першу симуляцію (номер 0):

Вище:
- у файлі [1-5] видаляємо симуляцію № 0 [5];
- у [6-9] — відповідь сервера jSON. Бачимо, що симуляція № 0 була видалена;
Повторімо цю операцію:

Вище:
- у [1] у веб-сесії сервера більше не залишилося симуляцій;
23.11.11. Дія [fin-session]
Дія [fin-session] обробляється наступним вторинним контролером [FinSessionController]:
<?php
namespace Application;
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class FinSessionController implements InterfaceController {
// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// повинен бути лише один параметр GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
// помилка?
if ($erreur) {
$état = 401;
// результат на головному контролері
$message = "GET requis avec le seul paramètre [action] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// зберігається тип сеансу
$type = $session->get("type");
// поточна сесія визнається недійсною
$session->invalidate();
// тип сеансу встановлюється у новій сесії
$session->set("type", $type);
// відправлення відповіді
$état = 400;
// результат передається до головного контролера
$content = ["réponse" => "session supprimée"];
return [Response::HTTP_OK, $état, $content, []];
}
}
Коментарі
- запит [GET main.php?action=fin-session];
- рядки 25–33: перевіряється, чи є дія GET з єдиним параметром [fin-action];
- рядок 38: поточна сесія визнається недійсною. Це призводить до видалення даних, записаних у ній, і запускається нова сесія;
- рядок 36: перед закінченням сесії запам’ятовується її тип [json, xml, html];
- рядок 40: тип попередньої сесії переноситься в нову сесію. Врешті-решт починається нова сесія з унікальним ключем [type];
- рядки 44–45: результат передається головному контролеру;
23.11.12. Тести [Postman]
Ми проведемо тест на помилку та тест на успішне виконання:

Вище:
- у [1-5] запитується завершення сесії [5] з POST [2] замість очікуваного GET;
- у [6-9] — відповідь сервера jSON;
А тепер приклад успішного результату. Спочатку розглянемо сесійний файл cookie, обміняний між клієнтом [Postman] та сервером під час останнього проведеного тесту:

Вище:
- [3] — сесійний файл cookie, надісланий клієнтом [Postman] на сервер;
Тепер розглянемо заголовки HTTP, надіслані сервером у своїй відповіді:

Вище:
- у [3-4] сесійний файл cookie відсутній у відповіді сервера. Це нормально. Сервер надсилає його лише один раз: на початку нової веб-сесії;
Тепер виконаємо дійсну дію [fin-session]:

Вище:
- у [1-3] — дійсна дія [fin-session];
- у [4-7] — відповідь jSON від сервера;
Розглянемо заголовки HTTP, надіслані у відповіді сервера:

- у [3] сервер надсилає заголовок [Set-Cookie], тим самим показуючи, що розпочинається нова веб-сесія;
23.12. Типи відповідей сервера
23.12.1. Вступ
Повернемося до загальної архітектури додатка:

Ми розглянемо можливі типи відповідей [3a]. Вони зібрані в папці [Responses] цього проєкту:

Ми вже розглядали клас [JsonResponse] у розділі «Посилання». Він реалізує інтерфейс [InterfaceResponse] і успадковує клас [ParentResponse]. Те саме стосується двох інших класів — [XmlResponse] та [HtmlResponse].
Нагадаємо визначення інтерфейсу [InterfaceResponse]:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Запит $request: запит обробляється
// Сесія $session: сесія веб-додатку
// масив $config: конфігурація додатка
// int statusCode: код статусу відповіді HTTP
// масив $content: відповідь сервера
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
// Logger $logger: логер для запису журналів
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
- рядки 19–27: інтерфейс [InterfaceResponse] має єдиний метод [send] для відправлення відповіді клієнту;
- рядки 11–17: значення різних параметрів методу [send];
- рядки 23–25: параметри [$statusCode, $content, $headers] є стандартною відповіддю вторинних контролерів додатка. Однак відповідь може потребувати додаткової інформації. Тому їй передаються перші три параметри (рядки 20–22), які надають доступ до повної інформації щодо запиту, сесії та конфігурації;
- рядок 26: відповідь потребує параметра [Logger], оскільки вона буде реєструвати відповідь, надіслану клієнту;
Тепер нагадаємо код класу [ParentResponse] — батьківського класу для трьох типів відповідей, який узагальнює те, що їм спільне: фактичне надсилання текстової відповіді клієнту:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode: код HTTP статусу відповіді
// рядок $content: тіло відповіді, що надсилається
// залежно від випадку, це рядок jSON, XML, HTML
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// підготовка текстової відповіді сервера
$response = new Response();
$response->setCharset("utf-8");
// код статусу
$response->setStatusCode($statusCode);
// заголовки
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// відправлення відповіді
$response->setContent($content);
$response->send();
}
}
Коментарі
- рядки 10–13: значення трьох параметрів методу [send];
- рядок 17: слід зауважити, що тіло відповіді має тип [string] і, отже, готове до відправлення (рядок 30);
- рядок 22: відповідь міститиме символи UTF-8;
- рядок 24: код статусу HTTP відповіді;
- рядки 26–28: додавання заголовків HTTP, наданих кодом виклику;
- рядки 30–31: надсилання відповіді клієнту;
Наостанок нагадаємо код головного контролера, який запитує надсилання відповіді клієнту:
// додавання ключів [action, état] до відповіді контролера
$content = ["action" => $action, "état" => $état] + $content;
// створюється екземпляр об'єкта [Response], який відповідає за відправку відповіді клієнту
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// відповідь надіслано — звільняємо ресурси
$logger->close();
exit;
- рядок 4: задаємо ім’я класу [Response], який потрібно інстанціювати;
- рядок 5: створюється екземпляр класу та надсилається відповідь клієнту за допомогою методу [send($request, $session, $config, $statusCode, $content, $headers, $logger)]. Оскільки методи [send] для різних типів відповідей реалізують один і той самий інтерфейс [InterfaceResponse], усі вони мають однаковий сигнатур;
23.12.2. Клас [JsonResponse]
Він уже був представлений у розділі «Посилання». Проте ми наводимо його код ще раз, щоб краще підкреслити однорідність трьох класів відповідей:
Клас [JsonResponse] реалізує інтерфейс [InterfaceResponse] наступним чином:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// Запит $request: запит обробляється
// Сесія $session: сесія веб-додатку
// масив $config: конфігурація додатка
// int statusCode: код статусу відповіді HTTP
// масив $content: відповідь сервера
// масив $headers: заголовки HTTP, які слід додати до відповіді
// Logger $logger: логер для запису журналів
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// підготовка серіалізатора Symfony
$serializer = new Serializer(
[
// необхідний для серіалізації об’єктів
new ObjectNormalizer()],
// кодер jSON
// для параметрів, вкажіть OU між різними параметрами
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// серіалізація jSON
$json = $serializer->serialize($content, 'json');
// заголовки
$headers = array_merge($headers, ["content-type" => "application/json"]);
// відправлення відповіді
parent::sendResponse($statusCode, $json, $headers);
// журнал
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
Коментарі
- рядок 13: клас реалізує інтерфейс [InterfaceResponse];
- рядок 13: клас успадковує клас [ParentResponse]. Усі типи [Response] успадковують цей клас. Саме цей батьківський клас надсилає відповідь клієнту (рядок 46). Оскільки цей код був спільним для всіх типів [Response], його було винесено в батьківський клас;
- рядки 33–40: створення екземпляра серіалізатора [Symfony], який перетворить відповідь сервера [$content] на рядок jSON (рядок 42);
- рядки 34–36: перший параметр конструктора [Serializer] — це масив. У нього поміщається екземпляр класу [ObjectNormalizer], необхідний для серіалізації об’єктів. У цьому додатку такий випадок зустрічається у списку симуляцій, де кожна симуляція є екземпляром класу [Simulation];
- рядок 39: другий параметр конструктора класу [Serializer] також є масивом: туди поміщаються всі кодери, що використовуються під час серіалізації (XML, jSON, CSV…);
- рядок 39: тут буде лише один кодер типу [JsonEncoder]. Конструктора без параметрів могло б вистачити. Тут ми передали конструктору параметр [JsonEncode] виключно для того, щоб передати параметри кодування jSON;
- рядок 39: параметр конструктора [JsonEncode] є масивом опцій. Тут використовується опція [JSON_UNESCAPED_UNICODE], щоб символи UTF-8 у рядку jSON відображалися у вихідному вигляді, а не «ескейпувалися»;
- рядок 42: тіло відповіді HHTP серіалізується у jSON за допомогою попереднього серіалізатора;
- рядок 44: додається заголовок HTTP, який повідомляє клієнту, що йому буде надіслано jSON;
- рядок 46: надсилається запит до батьківського класу на відправку відповіді клієнту;
- рядки 48–50: записуємо відповідь jSON у журнал;
23.12.3. Клас [XmlResponse]
Клас [XmlResponse] реалізує інтерфейс [InterfaceResponse] наступним чином:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class XmlResponse extends ParentResponse implements InterfaceResponse {
// Запит $request: запит обробляється
// Сесія $session: сесія веб-додатку
// масив $config: конфігурація додатка
// int statusCode: код статусу відповіді HTTP
// масив $content: відповідь сервера
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
// Logger $logger: логер для запису журналів
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// підготовка серіалізатора Symfony
$serializer = new Serializer(
// необхідний для серіалізації об’єктів
[new ObjectNormalizer()],
[
// серіалізація XML
new XmlEncoder(
[
XmlEncoder::ROOT_NODE_NAME => 'root',
XmlEncoder::ENCODING => 'utf-8'
]
),
// серіалізація jSON
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// серіалізація XML
$xml = $serializer->serialize($content, 'xml');
// заголовки
$headers = array_merge($headers, ["content-type" => "application/xml"]);
// відправлення відповіді
parent::sendResponse($statusCode, $xml, $headers);
// журнал
if ($logger !== NULL) {
// журнал у форматі jSON
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
Коментарі
- рядки 34–48: створення екземпляра серіалізатора Symfony. Конструктор приймає два параметри типу масив;
- рядок 36: перший масив містить екземпляр типу [ObjectNormalizer], який бере участь у серіалізації об’єктів;
- рядки 37–47: другий масив містить кодери, що використовуються для серіалізації. За допомогою одного й того самого серіалізатора можна передбачити різні типи серіалізації;
- рядки 38–44: кодер XML;
- рядок 41: встановлюється корінь згенерованого коду XML. Він матиме вигляд <root>[autres balises XML]</root>;
- рядок 42: для кодування використовуватимуться символи UTF-8;
- рядок 46; кодер jSON. Він буде використовуватися для запису відповіді у файл [logs.txt], який створюється у форматі jSON;
- рядок 50: тіло відповіді, що надсилається клієнту, серіалізується у форматі XML;
- рядок 52: до заголовків, отриманих як параметр (рядок 30), додається заголовок HTTP, який повідомляє клієнту, що йому надсилається документ XML;
- рядок 54: фактичне надсилання відповіді клієнту батьківським класом;
- рядки 56–60: запис відповіді у файл jSON;
23.12.4. Тести [Postman]
Ми вже провели всі можливі тести на помилки у файлі jSON. У файлі XML нічого більше робити не потрібно. Наводимо два приклади відповідей у файлі XML:

Вище:
- у [1-3] — запит на початок сеансу XML;
- у форматі [4-7] — відповідь сервера XML;
Відтепер усі відповіді сервера будуть у форматі XML. Можна використовувати всі запити, що вже застосовувалися у форматі [Postman], без змін, і для кожного з них ми отримаємо відповідь у форматі XML. Давайте, наприклад, проведемо успішну автентифікацію:

Вище:
- у [1-3] — дійсний запит на автентифікацію;
- у [4-7] — відповідь сервера XML;
23.12.5. Відповідь [HtmlResponse]
Коли тип сеансу — [html], створюється екземпляр об’єкта типу [HtmlResponse] для надсилання відповіді клієнту. Цей об’єкт надсилає клієнту потік HTML, який залежить від коду стану, повернутого вторинним контролером, що обробив дію. Ця відповідність [état=>vue] заноситься до конфігураційного файлу [config.json] наступним чином:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Цю конфігурацію слід інтерпретувати наступним чином: [‘nom de la vue’ => ‘états associés à cette vue’]
- рядок 2: якщо вторинний контролер повернув стан масиву [700, 221, 400], то потрібно відобразити вигляд [vue-authentification.php];
- рядок 3: якщо вторинний контролер повернув стан масиву [200, 300, 341, 350, 800], то слід відобразити вигляд [vue-calcul-impot.php];
- рядок 4: якщо вторинний контролер повернув стан з таблиці [500, 600], то слід відобразити вигляд [vue-liste-simulations.php];
- рядок 6: якщо вторинний контролер повернув стан, якого немає в жодній із попередніх таблиць, то слід відобразити вигляд [vue-erreurs.php];
Види зібрані в папці [Views] проекту:

Код класу [HtmlResponse] такий:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class HtmlResponse extends ParentResponse implements InterfaceResponse {
// Запит $request: запит обробляється
// Сесія $session: сесія веб-додатку
// масив $config: конфігурація додатка
// int statusCode: код статусу відповіді HTTP
// масив $content: відповідь сервера
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
// Logger $logger: логер для запису журналів
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// підготовка серіалізатора Symfony
$serializer = new Serializer(
[
// для серіалізації об’єктів
new ObjectNormalizer()],
[
// для серіалізації jSON журналу відповіді
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// відповідь HTML залежить від коду стану, поверненого контролером
$état = $content["état"];
// кожному стану відповідає певний вигляд — його шукають у конфігурації додатка
// список подань
$vues = array_keys($config["vues"]);
$trouvé = false;
$i = 0;
// проглядається список переглядів
while (!$trouvé && $i < count($vues)) {
// стани, пов’язані з видом № i
$états = $config["vues"][$vues[$i]];
// чи знаходиться шуканий стан серед станів, пов’язаних з переглядом № I?
if (in_array($état, $états)) {
// відображеним буде перегляд № i
$vueRéponse = $vues[$i];
$trouvé = true;
}
// наступний вигляд
$i++;
}
// знайдено?
if (!$trouvé) {
// якщо для поточного стану додатка не існує жодного виду
// відображається вікно помилок
$vueRéponse = $config["vue-erreurs"];
}
// отримуємо вигляд HTML для відображення у вигляді рядка символів
ob_start();
require __DIR__ . "/../Views/$vueRéponse";
$html = ob_get_clean();
// у заголовках вказується, що буде надіслано HTML
$headers = array_merge($headers, ["content-type" => "text/html"]);
// батьківський клас відповідає за фактичне надсилання відповіді
parent::sendResponse($statusCode, $html, $headers);
// запис у форматі jSON відповіді без HTML
if ($logger !== NULL) {
// запис у форматі jSON відповіді вторинного контролера, який обробив дію
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
Коментарі
- рядки 32–41: створюється екземпляр серіалізатора Symfony. Він необхідний для запису в журнал jSON відповіді контролера, який обробив дію (рядки 72–82);
- рядки 42–57: у конфігурації додатка шукається вигляд, який має бути відображений. Він залежить від коду стану, повернутого контролером, який обробив дію. Цей код міститься в [$content[‘état’]] (рядок 43);
- рядки 42–61: шукається вигляд, що відповідає цьому стану;
- рядки 62–67: якщо жодного виду не знайдено, то маємо ситуацію з аномальним кодом стану для додатка HTML. Поняття аномальних станів буде детальніше пояснено далі. У цьому випадку відображається вікно помилки;
- рядки 68–70: інтерпретується код PHP вибраного виду, а результат записується у змінну [$html] (рядок 71);
- цей код потребує деяких пояснень. Уявімо, що вибраним поданням є [vue-authentification.php], яке містить веб-форму автентифікації:
- рядок 69: функція [ob_start] запускає те, що в документації називається затримкою виводу. Усе, що записується операціями print, require… і що зазвичай негайно надсилається клієнту, потрапляє у буфер виводу (ob=output buffer), не надсилаючись клієнту;
- рядок 70: завантажується представлення [vue-authentification.php], яке є динамічним представленням HTML, що містить код PHP. Після цього відбуваються дві події:
- код PHP з подання [vue-authentification.php] завантажується та інтерпретується. Результатом є представлення, яке ми назвемо [vue-authentification.html], яке містить лише код HTML, а також CSS і JavaScript, але вже не містить PHP;
- цей код HTML зазвичай надсилається клієнту. Так відбувається з будь-яким текстом, який зустрічає інтерпретатор PHP і який не є кодом PHP. Через затримку виведення цей код HTML поміщається у буфер виведення, не надсилаючись клієнту;
- рядок 71: функція [ob_get_clean] виконує дві дії:
- вона записує у змінну [$html] вміст буфера виводу, тобто сторінку [vue-authentification.html], яку туди було записано;
- вона очищає вихідний буфер. Щодо нього все відбувається так, ніби нічого не сталося. Крім того, клієнт досі нічого не отримав;
- рядок 70: тут відбувається виконання класу [HtmlResponse], який знаходиться в папці [Responses]. Щоб знайти цей вигляд, потрібно піднятися на один рівень вище — до [..], а потім перейти до папки [Views]. [__DIR__] — це абсолютний шлях до папки, у якій знаходиться скрипт, що виконується; у нашому прикладі це папка [C:/myprograms/laragon-lite/www/php7/scripts-web/impots/13/Responses];
- рядок 73: до заголовків HTTP, отриманих як параметр (рядок 29), додаємо заголовок, який повідомляє клієнту, що йому буде надіслано HTML;
- рядок 75: класу-батьку доручається здійснити фактичну відправку відповіді клієнту;
- рядки 77–81: у файлі jSON реєструється відповідь [$content], надана вторинним контролером, який обробив поточну дію;
23.12.6. Тестування [Postman]
Щоб по-справжньому протестувати режим HTML сесії, нам потрібно було б переглянути всі подання. Ми зробимо це пізніше. Зараз проведемо такий тест:
Переглянемо список подання у файлі конфігурації:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Контекст, що генерує деякі з наведених вище кодів стану, можна визначити, переглянувши виконані тести [Postman]:

Бачимо, що код стану [700] відповідає успішній дії [init-session] [2]. Вище наведено відповідь jSON, але вона може бути типу XML або HTML. Саме останній випадок буде перевірено. Згідно з файлом конфігурації, саме вигляд [vue-authentification.php] становить відповідь HTML. Перевіримо.

Вище:
- у [1-3] ініціалізується сесія HTML. Отже, очікується відповідь HTML;
- у [4-8] — відповідь HTML від сервера;
- вкладка [8] дозволяє переглянути попередній перегляд отриманого коду HTML;

- у [8-9] — попередній перегляд сторінки HTML;
23.13. Веб-додаток HTML
23.13.1. Огляд переглядів
Веб-додаток HTML використовуватиме чотири екрани:
Екран автентифікації:

Вигляд розрахунку податку:

Вигляд зі списком моделювань:

Вигляд несподіваних помилок:

Ми опишемо ці екрани по черзі.
23.13.2. Вікно автентифікації
23.13.2.1. Опис екрану
Вигляд сторінки автентифікації такий:

Вигляд складається з двох елементів, які ми назвемо фрагментами:
- фрагмент [1] генерується скриптом [v-bandeau.php];
- фрагмент [2] генерується скриптом [v-authentification.php];
Сторінка автентифікації генерується наступною сторінкою [vue-authentification.php]:
<?php
// тестові дані сторінки
// дані сторінки інкапсулюються в $page
…
?>
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- головний блок на 1 рядок і 12 стовпців -->
<?php require "v-bandeau.php"; ?>
<!-- форма входу з 9 стовпцями -->
<div class="row">
<div class="col-md-9">
<?php require "v-authentification.php" ?>
</div>
</div>
<?php
// у разі помилки — відображається повідомлення про помилку
if ($modèle->error) {
print <<<EOT
<div class="row">
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
Les erreurs suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT;
}
?>
</div>
</body>
</html>
Коментарі
- рядок 7: документ HTML починається з цього рядка;
- рядки 8–44: сторінка HTML вкладена в теги <html> </html>;
- рядки 9–16: заголовок (head) документа HTML;
- рядок 11: тег <meta charset> вказує, що документ закодовано у форматі UTF-8;
- рядок 12: тег <meta name=’viewport’> визначає початкове відображення вікна перегляду: на всю ширину екрана, на якому воно відображається (width), у його початковому розмірі (initial-scale) без зміни розміру для пристосування до меншого розміру екрана (shrink-to-fit);
- рядок 14: тег <link rel=’stylesheet’> вказує на файл CSS, який визначає зовнішній вигляд сторінки. Тут ми використовуємо фреймворк CSS Bootstrap 4.1.3 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
- рядок 15: тег <title> визначає заголовок сторінки:

- рядки 17–43: основний вміст веб-сторінки розміщений у тегах <body></body>;
- рядки 18–42: тег <div> окреслює розділ відображуваної сторінки. Атрибути [class], що використовуються у цьому виді, всі пов’язані з фреймворком CSS Bootstrap. Тег <div class=’container’> окреслює контейнер Bootstrap;
- рядок 20: включено скрипт [v-bandeau.php]. Цей скрипт генерує верхній блок [1] сторінки. Ми опишемо його незабаром;
- рядки 22–26: тег <div class=’row’> окреслює рядок Bootstrap. Ці рядки складаються з 12 стовпців;
- рядок 23: тег <div class=’col-md-9’> окреслює секцію з 9 стовпців;
- рядок 24: включено скрипт [v-authentification.php], який відображає форму автентифікації [2] на сторінці. Ми опишемо її незабаром;
- рядок 27: тег <?php вставляє код PHP всередину сторінки HTML. Цей код виконується перед відображенням сторінки HTML і може її змінювати;
- рядок 29: усі динамічні дані відображуваного виду будуть інкапсульовані в об’єкт [$modèle] типу [stdClass]. Це довільний вибір. Замість цього можна було б вибрати асоціативний масив, що дало б той самий результат;
- рядок 29: автентифікація завершується невдало, якщо користувач вводить неправильні ідентифікаційні дані. У цьому випадку вікно автентифікації відображається знову з повідомленням про помилку. Атрибут [$modèle→error] вказує, чи слід відображати це повідомлення про помилку;
- рядки 30–39: цей синтаксис записує весь текст, розміщений між символами PHP <<<EOT (рядок 30 — замість EOT=End Of Text можна вказати будь-що) та символом EOT у рядку 39 (повинен бути ідентичним символу, використаному в рядку 30). Символ має бути вказаний у першому стовпці рядка 39. Змінні PHP, розташовані в тексті між двома символами EOT, інтерпретуються;
- рядки 33–36: обмежують зону з рожевим фоном (class="alert alert-danger") (рядок 33);

- рядок 34: текст;
- рядок 35: тег HTML <ul> (неупорядкований список) відображає список з маркованими пунктами. Кожен елемент списку повинен мати синтаксис <li>елемент</li>;
З цього коду слід запам’ятати динамічні елементи, які потрібно визначити:
- [$modèle→error]: для відображення повідомлення про помилку;
- [$modèle→erreurs]: список (у значенні HTML цього терміна) повідомлень про помилки;
23.13.2.2. Фрагмент [v-bandeau.php]
Фрагмент [v-bandeau.php] відображає верхню панель усіх сторінок веб-додатку:

Код фрагмента [v-bandeau.php] такий:
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
Коментарі
- рядки 2–13: верхня панель вкладена в секцію Bootstrap типу Jumbotron [<div class="jumbotron">]. Цей клас Bootstrap застосовує особливе стильове оформлення до відображуваного вмісту, щоб виділити його;
- рядки 3–12: рядок Bootstrap;
- рядки 4–6: зображення [img] розміщено у перших чотирьох стовпцях рядка;
- рядок 5: синтаксис [<?= $logo ?>] є еквівалентним синтаксису [<?php print $logo ?>]. Іншими словами, значення атрибута [src] буде дорівнювати значенню змінної PHP [$logo];
- рядки 7–11: інші 8 стовпців рядка (нагадаємо, що їх загалом 12) слугуватимуть для розміщення тексту (рядок 9) великими літерами (<h1>, рядки 8–10);
Динамічні елементи:
- [$logo]: URL зображення, що відображається у банері;
23.13.2.3. Фрагмент [v-authentification.php]
Фрагмент [v-authentification .php] відображає форму входу у веб-додаток:

Код фрагмента [v-authentification.php] такий:
<!-- форма HTML — значення надсилаються за допомогою дії [authentifier-utilisateur] -->
<form method="post" action="main.php?action=authentifier-utilisateur">
<!-- заголовок -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
<!-- форма Bootstrap -->
<fieldset class="form-group">
<!-- 1-й рядок -->
<div class="form-group row">
<!-- назва -->
<label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
<div class="col-md-4">
<!-- поле для введення тексту -->
<input type="text" class="form-control" id="user" name="user"
placeholder="Nom d'utilisateur" value="<?= $modèle->login ?>">
</div>
</div>
<!-- 2-й рядок -->
<div class="form-group row">
<!-- назва -->
<label for="password" class="col-md-3 col-form-label">Mot de passe</label>
<!-- поле введення тексту -->
<div class="col-md-4">
<input type="password" class="form-control" id="password" name="password"
placeholder="Mot de passe">
</div>
</div>
<!-- кнопка типу [submit] у 3-му рядку-->
<div class="form-group row">
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
Коментарі
- рядки 2–39: тег <form> обмежує форму HTML. Зазвичай вона має такі характеристики:
- він визначає поля введення даних (теги <input> у рядках 17 та 27);
- має кнопку типу [submit] (рядок 34), яка надсилає введені значення до URL, вказаного в атрибуті [action] тегу [form] (рядок 2). Метод HTTP, що використовується для звернення до цього URL, вказано в атрибуті [method] тегу [form] (рядок 2);
- тут, коли користувач натисне кнопку [Valider] (рядок 34), браузер відправить (рядок 2) значення, введені у форму, до URL [main.php?action=authentifier-utilisateur] (рядок 2);
- передані значення — це значення, введені користувачем у поля введення рядків 17 та 27. Вони будуть передані у форматі [user=xx&password=yy]. Назви параметрів [user, password] відповідають атрибутам [name] полів введення даних у рядках 17 та 27;
- рядки 5–7: розділ Bootstrap для відображення заголовка на синьому тлі:

- рядки 10–37: форма Bootstrap. Усі елементи форми будуть оформлені певним чином;
- рядки 12–20: визначають перший рядок форми:
![]()
- рядок 14 визначає напис [1] у трьох стовпцях. Атрибут [for] тегу [label] пов’язує напис з атрибутом [id] поля введення даних у рядку 17;
- рядки 15–19: розміщує поле введення у групу з чотирьох стовпців;
- рядок 17: тег HTML [input] описує поле введення. Він має кілька параметрів:
- [type=’text’]: це текстове поле введення. У нього можна вводити будь-що;
- [class=’form-control’]: стиль Bootstrap для поля введення;
- [id=’user’]: ідентифікатор поля введення. Цей ідентифікатор зазвичай використовується CSS та кодом JavaScript;
- [name=’user’]: назва поля введення. Саме під цією назвою значення, введене користувачем, буде відправлене браузером [user=xx];
- [placeholder=’invite’]: текст, що відображається у полі введення, коли користувач ще нічого не ввів;
![]()
- [value=’valeur’]: текст «значення» відображатиметься у полі введення, щойно воно з’явиться на екрані, тобто до того, як користувач введе будь-що інше. Цей механізм використовується у разі помилки для відображення даних, що спричинили помилку. У цьому випадку цим значенням буде значення змінної PHP [$modèle→login];
- рядки 21–30: аналогічний код для введення пароля;
- рядок 27: [type=’password’] створює поле для введення тексту (можна вводити що завгодно), але введені символи приховані:
![]()
- рядки 32–36: третій рядок для кнопки [Valider];
- рядок 34: оскільки він має атрибут [type=submit], натискання на цю кнопку спричиняє надсилання браузером на сервер введених значень, як було пояснено раніше. Атрибут CSS [class="btn btn-primary"] відображає синю кнопку:

Залишилося пояснити ще одну річ. У рядку 2 атрибут [action="main.php?action=authentifier-utilisateur"] визначає неповну URL (вона не починається з http://machine:port/chemin). У нашому прикладі всі URL у додатку мають вигляд [http://localhost/php7/scripts-web/impots/version-12/main.php?action=xx]. Екран автентифікації буде відображатися з різними URL:
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html];
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur]
Ці URL вказують на документ [main.php] у шляху [http://localhost/php7/scripts-web/impots/version-12]. Це стосуватиметься всіх URL цього додатка. Параметр [action="main.php?action=authentifier-utilisateur"] отримає цей шлях як префікс під час надсилання введених значень. Отже, ці значення будуть надіслані до URL [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur].
23.13.2.4. Візуальні тести
Тестування візуалізацій можна проводити задовго до їх інтеграції в додаток. Йдеться про перевірку їхнього візуального вигляду. Ми зберемо всі тестові візуалізації в папці [Tests] проекту:

Щоб протестувати вікно [vue-authentification.php], нам потрібно створити модель даних, яку воно буде відображати:
<?php
// тестові дані сторінки
//
// розраховується шаблон подання
$modèle = getModelForThisView();
function getModelForThisView(): object {
// дані сторінки інкапсулюються в $modèle
$modèle = new \stdClass();
// ідентифікатор користувача
$modèle->login = "albert";
// список помилок
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// створюється список HTML помилок
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// зображення банера
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// відтворюємо шаблон
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
…
</head>
<body>
….
</body>
</html>
Коментарі
- рядки 1–5: вигляд автентифікації має динамічні частини, що керуються об’єктом [$modèle]. Цей об’єкт називають моделлю подання. Згідно з одним із двох визначень, наведених для абревіатури MVC, тут маємо M від MVC;
- рядок 5: модель подання обчислюється функцією [getModelForThisView];
- рядок 9: модель подання буде інкапсульована в тип [stdClass];
- рядки 10–22: визначаються тестові значення для динамічних елементів вікна автентифікації;
Візуальне тестування можна виконати за допомогою NetBeans:

Продовжуємо ці візуальні тести, доки не будемо задоволені результатом.
23.13.2.5. Розрахунок моделі подання
Після визначення візуального вигляду екрану можна приступити до розрахунку моделі екрану в реальних умовах. Нагадаємо коди станів, які ведуть до цього екрану. Їх можна знайти у файлі конфігурації:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Отже, саме коди стану [700, 221, 400] забезпечують відображення екрану автентифікації. Щоб з’ясувати значення цих кодів, можна скористатися тестами [Postman], виконаними на додатку jSON:
- [init-session-json-700]: 700 — це код стану після успішного виконання дії [init-session]: у цьому випадку відображається порожня форма автентифікації;
- [authentifier-utilisateur-221]: 221 — це код стану після невдалої операції [authentifier-utilisateur] (нерозпізнані облікові дані): у цьому випадку відображається форма автентифікації для внесення виправлень;
- [fin-session-400]: 400 — це код стану після успішного виконання дії [fin-session]: у цьому випадку відображається порожня форма автентифікації;
Тепер, коли ми знаємо, в яких випадках має відображатися форма автентифікації, можна обчислити її шаблон у [vue-authentification.php]:

Код для обчислення шаблону подання [vue-authentification.php] є таким:
<?php
// успадковуються такі змінні
// Запит $request: поточний запит
// Сесія $session: сесія додатка
// масив $config: конфігурація додатка
// масив $content: відповідь контролера
//
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// розраховується модель подання
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// інкапсуляція даних сторінки в $modèle
$modèle = new stdClass();
// стан додатка
$état = $content["état"];
// модель залежить від стану
switch ($état) {
case 700:
case 400:
// випадок відображення порожньої форми
$modèle->login = "";
// помилок для відображення немає
$modèle->error = FALSE;
break;
case 221:
// помилка автентифікації
// повторно відображається користувач, введений спочатку
$modèle->login = $request->request->get("user");
// є помилка, яку потрібно відобразити
$modèle->error = TRUE;
// список HTML повідомлень про помилки — тут лише одне
$modèle->erreurs = "<li>Echec de l'authentification</li>";
}
// результат
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Коментарі
- рядки 3–6: викликаються змінні, успадковані від класу [HtmlResponse], який за допомогою [require] відображає вигляд [vue-authentification.php];
- рядки 9–10: класи Symfony, що використовуються в коді подання;
- рядки 15–40: функція [getModelForThisView] відповідає за обчислення моделі подання;
- рядок 19: отримується код стану, повернутий контролером, який обробив поточну дію;
- рядки 21–37: шаблон залежить від цього коду стану;
- рядки 22–28: випадок, коли потрібно відобразити порожню форму автентифікації;
- рядки 29–37: випадок помилкової автентифікації: відображається введений користувачем ідентифікатор та повідомлення про помилку. Користувач може тоді за допомогою клавіатури повторити спробу автентифікації;
Для банера [v-bandeau.php] було написано окремий шаблон:
<?php
// логотип
$scheme = $request->server->get('REQUEST_SCHEME'); // http
$host = $request->server->get('SERVER_NAME'); // localhost
$port = $request->server->get('SERVER_PORT'); // 80
$uri = $request->server->get('REQUEST_URI'); // /php7/scripts-web/impots/version-12/main.php?action=xxx
$champs = [];
preg_match("/(.+)\/.+?$/", $uri, $champs);
$root = $champs[1]; // /php7/scripts-web/impots/version-12
$modèle->logo = "$scheme://$host:$port$root/Views/logo.jpg"; // http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg
?>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $modèle->logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
Коментарі
- у рядку 16 використовується змінна [$modèle→logo], яка є значенням URL для логотипу банера. Замість того, щоб обчислювати цю змінну чотири рази для чотирьох вікон додатка, це обчислення винесено у фрагмент [v-bandeau.php];
- у рядках 1–11 показано, як побудувати URL та [http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg] на основі інформації, знайденої в серверному середовищі [$request→server];
23.13.2.6. Тести [Postman]
Ми вже створили запити, що генерують коди [700, 221, 400], які відображають вікно автентифікації. Нагадаємо їх:
- [init-session-html-700]: 700 — це код стану після успішного виконання дії [init-session]: у цьому випадку відображається порожня форма автентифікації;
- [authentifier-utilisateur-221]: 221 — це код стану після невдалої дії [authentifier-utilisateur] (нерозпізнані облікові дані): у цьому випадку відображається форма автентифікації для внесення виправлень;
- [fin-session-400]: 400 — це код стану після успішного виконання дії [fin-session]: у цьому випадку відображається порожня форма автентифікації;
Достатньо повторно використати їх і перевірити, чи правильно відображається сторінка автентифікації. Тут ми покажемо лише два тести:
- [init-session-html-700]: початок сеансу HTML;

- [authentifier-utilisateur-221]: автентифікація користувача [x, x];

Вище:
- запит надіслав рядок [user=x&password=x];
- у [4] відображається повідомлення про помилку;
- у [3] знову відображено неправильного користувача;
23.13.2.7. Висновок
Нам вдалося протестувати представлення [vue-authentification.php], не написавши інших представлень. Це стало можливим завдяки тому, що:
- усі контролери вже написані;
- [Postman] дозволяє нам надсилати запити на сервер без використання представлень. Під час написання контролерів слід пам’ятати, що це може зробити будь-хто. Тому слід бути готовим обробляти запити, які жоден вигляд не підтримує. Вони створюються вручну у [Postman]. Ніколи не слід заздалегідь вважати, що «цей запит неможливий». Потрібно перевірити;
23.13.3. Вид для розрахунку податку
23.13.3.1. Опис подання
Вигляд розрахунку податку виглядає так:

Вигляд складається з трьох частин:
- 1: верхня панель генерується фрагментом [v-bandeau.php], який вже було представлено;
- 2: форма розрахунку податку, що генерується фрагментом [v-calcul-impot.php];
- 3: меню з двома посиланнями, яке генерується фрагментом [v-menu.php];
Сторінка розрахунку податку генерується за допомогою наступного скрипта [vue-calcul-impot.php]:

<?php
// успадковуються такі змінні
// Запит $request: поточний запит
// Сесія $session: сесія додатка
// масив $config: конфігурація додатка
// масив $content: відповідь контролера, який обробив дію
//
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// розраховується модель подання
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// інкапсулюються дані сторінки в $modèle
$modèle = new \stdClass();
…
// здійснюється рендеринг моделі
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- головний банер -->
<?php require "v-bandeau.php"; ?>
<!-- двоколонковий макет -->
<div class="row">
<!-- меню -->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- форма розрахунку -->
<div class="col-md-9">
<?php require "v-calcul-impot.php" ?>
</div>
</div>
<!-- у разі успіху -->
<?php
if ($modèle->success) {
// виводиться повідомлення про успішне виконання
print <<<EOT1
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-success" role="alert">
$modèle->impôt</br>
$modèle->décôte</br>\n
$modèle->réduction</br>\n
$modèle->surcôte</br>\n
$modèle->taux</br>\n
</div>
</div>
</div>
EOT1;
}
?>
<?php
if ($modèle->error) {
// список помилок у 9 стовпцях
print <<<EOT2
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
L'erreur suivante s'est produite :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT2;
}
?>
</div>
</body>
</html>
Коментарі
- ми коментуємо лише ті нововведення, з якими ще не стикалися;
- рядок 37: включення верхнього банера подання до першого рядка Bootstrap подання;
- рядки 41–43: вставлення меню, яке займатиме три стовпці другого рядка Bootstrap у вікні;
- рядки 45–47: вставлення форми розрахунку податку, яка займатиме дев’ять стовпців другого рядка Bootstrap у вікні;
- рядки 51–69: якщо розрахунок податку завершився успішно ([$modèle→success=TRUE]), то результат розрахунку податку відображається у зеленому блоці (рядки 59–65). Ця рамка знаходиться у третьому рядку Bootstrap у вигляді (рядок 54) і займає дев’ять стовпців (рядок 58) праворуч від трьох порожніх стовпців (рядки 55–57). Отже, ця рамка буде розташована безпосередньо під формою розрахунку податку;
- рядки 71–87: якщо розрахунок податку за кодом [$modèle→error=TRUE] завершився невдало, то повідомлення про помилку відображається у рожевій рамці (рядки 80–83). Ця рамка знаходиться у третьому рядку Bootstrap цього виду (рядок 75) і займає дев’ять стовпців (рядок 79) праворуч від трьох порожніх стовпців (рядки 76–78). Отже, ця рамка буде розташована безпосередньо під формою розрахунку податку;
23.13.3.2. Фрагмент [v-calcul-impot.php]
Фрагмент [v-calcul-impot.php] відображає форму входу у веб-додаток:

Код фрагмента [v-calcul-impot.php] такий:
<!-- форма HTML відправлено -->
<form method="post" action="main.php?action=calculer-impot">
<!-- повідомлення у 12 стовпцях на синьому тлі -->
<div class="col-md-12">
<div class="alert alert-primary" role="alert">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</div>
</div>
<!-- елементи форми -->
<fieldset class="form-group">
<!-- перший рядок у 9 стовпців -->
<div class="row">
<!-- напис у 4 стовпцях -->
<legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
<!-- перемикачі на 5 стовпців-->
<div class="col-md-5">
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios1" value="oui" <?= $modèle->checkedOui ?>>
<label class="form-check-label" for="gridRadios1">
Oui
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios2" value="non" <?= $modèle->checkedNon ?>>
<label class="form-check-label" for="gridRadios2">
Non
</label>
</div>
</div>
</div>
<!-- другий рядок із 9 стовпців -->
<div class="form-group row">
<!-- напис у 4 стовпцях -->
<label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
<!-- поле для введення кількості дітей у 5 стовпців -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="<?= $modèle->enfants ?>">
</div>
</div>
<!-- третій рядок у 9 стовпців -->
<div class="form-group row">
<!-- напис у 4 стовпцях -->
<label for="salaire" class="col-md-4 col-form-label">Salaire annuel</label>
<!-- поле для введення суми заробітної плати, що складається з 5 стовпців -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="salaire" name="salaire" placeholder="Salaire annuel" aria-describedby="salaireHelp" value="<?= $modèle->salaire ?>">
<small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
</div>
</div>
<!-- четвертий рядок, кнопка [submit] у 5 стовпців -->
<div class="form-group row">
<div class="col-md-5">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
Коментарі
- рядок 2: форма HTML буде відправлена (атрибут [method]) на URL [main.php?action=calculer-impot] (атрибут [action]). Передані значення будуть відповідати значенням полів введення:
- значення позначеної кнопки вибору у вигляді:
- [marié=oui], якщо відмічено перемикач [Oui] (рядки 16–22). [marié] — це значення атрибута [name] у рядку 18, [oui] — значення атрибута [value] у рядку 18;
- [marié=non], якщо встановлено прапорець [Non] (рядки 23–28). [marié] — це значення атрибута [name] у рядку 24, [non] — значення атрибута [value] у рядку 24;
- значення поля введення чисел у рядку 37 у вигляді [enfants=xx], де [enfants] — це значення атрибута [name] у рядку 37, а [xx] — значення, введене користувачем за допомогою клавіатури;
- значення поля введення чисел у рядку 46 у вигляді [salaire=xx], де [salaire] — це значення атрибута [name] у рядку 46, а [xx] — значення, введене користувачем за допомогою клавіатури;
- значення позначеної кнопки вибору у вигляді:
Зрештою, значення, що буде відправлено, матиме вигляд [marié=xx&enfants=yy&salaire=zz].
- введені значення будуть відправлені, коли користувач натисне кнопку типу [submit] у рядку 53;
- рядки 16–30: дві кнопки-перемикачі:
![]()
Обидві кнопки-перемикачі входять до однієї групи, оскільки мають однаковий атрибут [name] (рядки 18, 24). Браузер гарантує, що в групі кнопок-перемикачів у будь-який момент часу може бути позначена лише одна. Отже, натискання на одну з них знімає позначку з тієї, що була позначена раніше;
- це перемикачі завдяки атрибуту [type="radio"] (рядки 18, 24);
- під час відображення форми (до введення даних) одна з радіо-кнопок має бути відмічена: для цього достатньо додати атрибут [checked=’checked’] до відповідного тегу <input type="radio">. Це здійснюється за допомогою динамічних змінних:
- [<?= $modèle->checkedOui ?>] у рядку 18;
- [<?= $modèle->checkedNon ?>] у рядку 24;
Ці змінні стануть частиною шаблону подання.
- рядок 37: поле для введення чисел [type="number"] із мінімальним значенням 0 [min="0"]. У сучасних браузерах це означає, що користувач зможе ввести лише число >=0. У цих самих сучасних браузерах введення можна здійснювати за допомогою повзунка, який можна пересувати вгору або вниз. Атрибут [step="1"] у рядку 37 вказує, що повзунок буде змінювати значення з кроком у 1 одиницю. Це означає, що повзунок прийматиме лише цілі числа від 0 до n з кроком у 1. Для ручного введення це означає, що числа з десятковою крапкою не будуть прийняті;
![]()
- рядок 37: у деяких випадках поле введення даних про дітей має бути попередньо заповнене останнім значенням, введеним у це поле. Для цього використовується атрибут [value], який визначає значення, що відображатиметься у полі введення. Це значення буде динамічним і генеруватиметься змінною [$modèle→enfants];
- рядок 46: пояснення щодо введення заробітної плати такі самі, як і для введення даних про дітей;
- рядок 53: кнопка типу [submit], яка запускає POST для значень, введених у URL та [main.php?action=calculer-impot];

23.13.3.3. Фрагмент [v-menu.php]
Цей фрагмент відображає меню ліворуч від форми розрахунку податку:

Код цього фрагмента такий:
<!-- меню Bootstrap -->
<nav class="nav flex-column">
<?php
// відображення списку посилань HTML
foreach($modèle->optionsMenu as $texte=>$url){
print <<<EOT3
<a class="nav-link" href="$url">$texte</a>
EOT3;
}
?>
</nav>
Коментарі
- рядки 2–11: тег HTML [nav] обрамлює частину документа HTML, що містить навігаційні посилання на інші документи;
- рядок 7: тег HTML [a] вводить навігаційне посилання:
- [$url]: це URL, до якого відбувається перехід при натисканні на посилання [$texte]. У цьому випадку браузер виконує операцію [GET $url]. Якщо [$url] є відносною URL, то перед нею ставиться префікс у вигляді кореня URL, яка наразі відображається в адресному рядку браузера. Отже, щоб отримати посилання [1], коли поточне посилання URL у браузері має тип [http://chemin/main.php?paramètres], буде створено таке посилання:
- рядок 5: шаблон [$modèle→optionsMenu] фрагмента буде масивом такого вигляду:
[‘ Liste des simulations’=>’main.php?action=liste-simulations’,
‘ Fin de session’=>’main.php?action=fin-session’]
- рядки 2, 7: класи CSS та [nav, flex-column, nav-link] — це класи Bootstrap, які визначають зовнішній вигляд меню;
23.13.3.4. Візуальне тестування
Ми об’єднуємо ці різні елементи у папці [Tests] і створюємо тестовий шаблон для подання [vue-calcul-impot.php]:

Модель даних для подання [vue-calcul-impot] буде такою:
<?php
// тестові дані сторінки
//
// розраховуємо шаблон подання
$modèle = getModelForThisView();
function getModelForThisView(): object {
// інкапсуляція даних сторінки в $modèle
$modèle = new \stdClass();
// форма
$modèle->checkedOui = "";
$modèle->checkedNon = 'checked="checked"';
$modèle->enfants = 2;
$modèle->salaire = 300000;
// повідомлення про успішне виконання
$modèle->success = TRUE;
$modèle->impôt = "Montant de l'impôt : 1000 euros";
$modèle->décôte = "Décôte : 15 euros";
$modèle->réduction = "Réduction : 20 euros";
$modèle->surcôte = "Surcôte : 0 euros";
$modèle->taux = "Taux d'imposition : 14 %";
// повідомлення про помилку
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// створюється список HTML помилок
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// меню
$modèle->optionsMenu = [
'Список симуляцій' => 'main.php?action=список-симуляцій',
'«Завершення сеансу» => 'main.php?action=fin-session'];
// зображення банера
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// відображення шаблону
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Коментарі
- рядки 7–39: ініціалізуємо всі динамічні частини подання [vue-calcul-impot.php] та фрагментів [v-calcul-impot.php] і [v-menu.php];
Перевіряємо вигляд [vue-calcul-impot.php]:

Отримуємо такий результат:

Працюємо над цим видом, доки отриманий візуальний результат нас не задовольнить. Після цього можна перейти до інтеграції виду у веб-додаток, що зараз розробляється.
23.13.3.5. Обчислення моделі подання

Після визначення візуального вигляду подання можна приступити до розрахунку моделі подання в реальних умовах. Нагадаємо коди станів, які ведуть до цього подання. Їх можна знайти у файлі конфігурації:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Отже, саме коди стану [200, 300, 341, 350, 800] викликають відображення вікна автентифікації. Щоб з’ясувати значення цих кодів, можна скористатися тестами [Postman], проведеними на додатку jSON:
- [authentifier-utilisateur-200]: 200 — це код стану після успішного виконання дії [authentifier-itilisateur]: у цьому випадку відображається порожня форма розрахунку податку;
- [calculer-impot-300]: 300 — це код стану після успішного виконання дії [calculer-impot]. У цьому випадку відображається форма розрахунку з введеними даними та сумою податку. Користувач може виконати ще один розрахунок;
- [fin-session-400]: 400 — це код стану після успішного виконання дії [fin-session]: після цього відображається порожня форма автентифікації;
- код стану [341] отримується у разі правильного розрахунку податку, але відсутність з’єднання з SGBD спричиняє помилку;
- код стану [350] отримується у разі правильного розрахунку податку, але відсутність з’єднання з сервером [Redis] спричиняє помилку;
- код стану [800] буде представлено пізніше. Ми ще не стикалися з ним;
- тут ми виходимо з припущення, що користувач використовує сучасний браузер. Отже, у розглянутій формі неможливо вводити від’ємні числа, нечислові символьні рядки та числа з десятковою крапкою у поля введення [enfants, salaire]. У старіших браузерах це було б можливо. Ми будемо розглядати ці помилки як несподівані та відображатимемо вікно [vue-erreurs];
Тепер, коли ми знаємо, в яких випадках має відображатися форма розрахунку податку, можна розробити її шаблон у [vue-calcul-impot.php]:
<?php
// успадковуються такі змінні
// Запит $request: поточний запит
// Сесія $session: сесія додатка
// масив $config: конфігурація додатка
// масив $content: відповідь контролера, який обробив дію
//
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// розраховується модель подання
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// інкапсулюються дані сторінки в $modèle
$modèle = new \stdClass();
// стан додатка
$état = $content["état"];
// модель залежить від стану
switch ($état) {
case 200 :
case 800:
// початкове відображення порожньої форми
$modèle->success = FALSE; $modèle->errror = FALSE;
$modèle->checkedNon = 'checked="checked"';
$modèle->checkedOui = "";
$modèle->enfants = "";
$modèle->salaire = "";
break;
case 300:
// успішне виконання обчислення — відображення результату
$modèle->success = TRUE;
$modèle->error = FALSE;
$modèle->impôt = "Montant de l'impôt : {$content["réponse"]["impôt"]} euros";
$modèle->décôte = "Décôte : {$content["réponse"]["décôte"]} euros";
$modèle->réduction = "Réduction : {$content["réponse"]["réduction"]} euros";
$modèle->surcôte = "Surcôte : {$content["réponse"]["surcôte"]} euros";
$modèle->taux = "Taux d'imposition : " . ($content["réponse"]["taux"] * 100) . " %";
// форма відновлена із введеними значеннями
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
break;
case 341:
// база даних HS
case 350:
// сервер Redis HS
// форма відновлена із введеними значеннями
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
// помилка
$modèle->success = FALSE;
$modèle->error = TRUE;
$modèle->erreurs = "<li>{$content["réponse"]}</li>";
break;
}
//меню
$modèle->optionsMenu = [
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session"];
// повертаємо шаблон
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
…
<title>Application impots</title>
</head>
<body>
…
</body>
</html>
Коментарі
- рядки 22–30: відображення порожньої форми;
- рядки 31–45: випадок успішного розрахунку податку. Повторно відображаються введені значення, а також сума податку;
- рядки 46–59: випадок невдалого розрахунку податку через недоступність одного з серверів [Redis] або [MySQL];
- рядки 62–64: розрахунок двох опцій меню;
23.13.3.6. Тести [Postman]
Тест [calculer-impot-300] дозволяє отримати код стану 300. Він відповідає успішному розрахунку податку:

- у [3] — значення, що призвели до результату [2];
Давайте розглянемо випадок помилки: помилка [350], спричинена недоступністю сервера [Redis]:

23.13.4. Вигляд списку симуляцій
23.13.4.1. Опис вікна
Вигляд, що відображає список симуляцій, виглядає наступним чином:

Вигляд, згенерований скриптом [vue-liste-simulations], складається з трьох частин:
- 1: верхня панель генерується фрагментом [v-bandeau.php], який вже було представлено;
- 2: таблиця симуляцій, згенерована фрагментом [v-liste-simulations.php];
- 3: меню з двома посиланнями, яке генерується фрагментом [v-menu.php];
Вигляд симуляцій генерується наступним скриптом [vue-liste-simulations.php]:

<?php
// розраховується шаблон подання
$modèle = getModelForThisView();
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// інкапсулюються дані сторінки в $modèle
$modèle = new \stdClass();
…
// виводиться шаблон
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- головний банер -->
<?php require "v-bandeau.php"; ?>
<!-- двоколонковий макет -->
<div class="row">
<!-- триколонне меню-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- список симуляцій у 9 стовпців-->
<div class="col-md-9">
<?php require "v-liste-simulations.php" ?>
</div>
</div>
</div>
</body>
</html>
Коментарі
- рядок 28: включення верхнього банера додатка [1];
- рядок 33: додавання меню [2]. Воно відображатиметься у трьох стовпцях під банером;
- рядок 37: включення таблиці моделювань [3]. Вона відображатиметься у дев’яти стовпцях під банером та праворуч від меню;
Ми вже прокоментували два з трьох фрагментів цього виду:
- [v-bandeau.php]: у розділі «Посилання»;
- [v-menu.php]: у параграфі з посиланням;
Фрагмент [v-liste-simulations.php] виглядає так:
<!-- повідомлення на синьому тлі -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- таблиця моделювань -->
<table class="table table-sm table-hover table-striped">
<!-- заголовки шести стовпців таблиці -->
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Marié</th>
<th scope="col">Nombre d'enfants</th>
<th scope="col">Salaire annuel</th>
<th scope="col">Montant impôt</th>
<th scope="col">Surcôte</th>
<th scope="col">Décôte</th>
<th scope="col">Réduction</th>
<th scope="col">Taux</th>
<th scope="col"></th>
</tr>
</thead>
<!-- тіло таблиці (відображені дані) -->
<tbody>
<?php
$i = 0;
// кожна симуляція відображається під час перегляду таблиці симуляцій
foreach ($modèle->simulations as $simulation) {
// відображення рядка таблиці з 6 стовпцями — тег <tr>
// стовпець 1: заголовок рядка (№ симуляції) — тег <th scope='row'>
// стовпець 2: значення параметра [marié] — тег <td>
// стовпець 3: значення параметра [enfants] — тег <td>
// стовпець 4: значення параметра [salaire] — тег <td>
// стовпець 5: значення параметра [impôt] (податку) — тег <td>
// стовпець 6: значення параметра [surcôte] — тег <td>
// стовпець 7: значення параметра [décôte] — тег <td>
// стовпець 8: значення параметра [réduction] — тег <td>
// стовпець 9: значення параметра [taux] (податку) — тег <td>
// стовпець 10: посилання на скасування моделювання — тег <td>
print <<<EOT
<tr>
<th scope="row">$i</th>
<td>{$simulation["marié"]}</td>
<td>{$simulation["enfants"]}</td>
<td>{$simulation["salaire"]}</td>
<td>{$simulation["impôt"]}</td>
<td>{$simulation["surcôte"]}</td>
<td>{$simulation["décôte"]}</td>
<td>{$simulation["réduction"]}</td>
<td>{$simulation["taux"]}</td>
<td><a href="main.php?action=supprimer-simulation&numéro=$i">Supprimer</a></td>
</tr>
EOT;
$i++;
}
?>
</tr>
</tbody>
</table>
Коментарі
- таблиця HTML створена за допомогою тегу <table> (рядки 6 та 58);
- заголовки стовпців таблиці розміщуються всередині тегу <thead> (table head, рядки 8, 21). Тег <tr> (table row, рядки 9 та 20) окреслює рядок. У рядках 10–15 тег <th> (table header) визначає заголовок стовпця. Отже, їх десять. [scope="col"] вказує, що заголовок стосується стовпця. [scope="row"] вказує, що заголовок стосується рядка;
- рядки 23–57: тег <tbody> обрамлює дані, що відображаються в таблиці;
- рядки 40–51: тег <tr> обмежує рядок таблиці;
- рядок 41: тег <th scope=’row’> визначає заголовок рядка;
- рядки 42–50: кожен тег <td> визначає стовпець рядка;
- рядок 27: список симуляцій міститься у шаблоні [$modèle→simulations], який є асоціативною таблицею;
- рядок 50: посилання для видалення симуляції. Модель URL використовує номер, що відображається в першому стовпці таблиці (рядок 41);
23.13.4.2. Візуальна перевірка
Ми об’єднуємо ці різні елементи у папці [Tests] і створюємо тестову модель для подання [vue-liste-simulations.php]:

Модель даних подання [vue-liste-simulations] буде такою:
<?php
// розраховується шаблон подання
$modèle = getModelForThisView();
function getModelForThisView(): object {
// дані сторінки інкапсулюються в $modèle
$modèle = new \stdClass();
// симуляції перетворюються у формат, який очікує сторінка
$modèle->simulations = [
[
"marié" => "oui",
"enfants" => 2,
"salaire" => 60000,
"impôt" => 448,
"décôte" => 100,
"réduction" => 20,
"surcôte" => 0,
"taux" => 0.14
],
[
"marié" => "non",
"enfants" => 2,
"salaire" => 200000,
"impôt" => 25600,
"décôte" => 0,
"réduction" => 0,
"surcôte" => 8400,
"taux" => 0.45
]
];
// параметри меню
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// зображення банера
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// створюється шаблон
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Коментарі
- рядки 9–30: масив симуляцій, що відображаються таблицею HTML;
- рядки 32–34: таблиця опцій меню;
Відобразимо цей вигляд:

Отримуємо такий результат:

Працюємо над цим видом, доки візуальний результат нас не задовольнить. Після цього можна перейти до інтеграції виду у веб-додаток, який зараз створюється.
23.13.4.3. Обчислення моделі виду

Після визначення візуального вигляду подання можна приступити до розрахунку моделі подання в реальних умовах. Нагадаємо коди станів, які ведуть до цього подання. Їх можна знайти у файлі конфігурації:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Отже, саме коди стану [500, 600] забезпечують відображення виду симуляцій. Щоб з’ясувати значення цих кодів, можна скористатися тестами [Postman], виконаними на додатку jSON:
- [lister-simulations-500]: 500 — це код стану після успішного виконання дії [lister-simulations]: у цьому випадку відображається список симуляцій, виконаних користувачем;
- [supprimer-simulation-600]: 600 — це код стану після успішного виконання дії [supprimer-simulation]. Після цього відображається новий список симуляцій, отриманий після цього видалення;
Тепер, коли ми знаємо, в яких випадках має відображатися список симуляцій, можна обчислити його модель у [vue-liste-simulations.php]:
<?php
// успадковуються такі змінні
// Запит $request: поточний запит
// Сесія $session: сесія додатка
// масив $config: конфігурація додатка
// масив $content: відповідь контролера
// помилок немає
// масив $content: відповідь контролера
//
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// розраховується модель подання
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// інкапсулюємо дані сторінки в $modèle
$modèle = new \stdClass();
// симуляції перетворюються у формат, очікуваний сторінкою
// вони містяться у відповіді контролера, який виконав дію
// у вигляді масиву об’єктів типу [Simulation]
$objetsSimulation = $content["réponse"];
// кожен об’єкт типу [Simulation] буде перетворено на асоціативний масив
$modèle->simulations = [];
foreach ($objetsSimulation as $objetSimulation) {
$modèle->simulations[] = [
"marié" => $objetSimulation->getMarié(),
"enfants" => $objetSimulation->getEnfants(),
"salaire" => $objetSimulation->getSalaire(),
"impôt" => $objetSimulation->getImpôt(),
"surcôte" => $objetSimulation->getSurcôte(),
"décôte" => $objetSimulation->getdécôte(),
"réduction" => $objetSimulation->getRéduction(),
"taux" => $objetSimulation->getTaux()
];
}
// опції меню
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// створюємо шаблон
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Коментарі
- рядки 26–36: обчислення моделі [$modèle→simulations], що використовується фрагментом [v-liste-simulations.php];
- рядки 39–41: обчислення моделі [$modèle→optionsMenu], що використовується фрагментом [v-menu.php];
23.13.4.4. Тести [Postman]
Тест [lister-simulations-500] дозволяє отримати код стану 500. Він відповідає запиту на перегляд симуляцій:

Тест [supprimer-simulation-600] дозволяє отримати код стану 600. Він відповідає успішному видаленню симуляції № 0. Повернутий результат — це список симуляцій, у якому на одну симуляцію менше:

23.13.5. Перегляд несподіваних помилок
Під несподіваною помилкою тут розуміється помилка, яка не мала б трапитися за умов нормального використання веб-додатку.
Візьмемо для прикладу тест [Postman] [calculer-impot-3xx], який визначено так:

- у [1-3] — запит POST з дією [calculer-impot];
- у [4-6]: тут можна задати будь-які значення для трьох параметрів POST:
- [4]: параметр [marié] відсутній;
- [5-6]: параметри [enfants, salaire] присутні, але є недійсними;
- у [9] ці три помилки позначаються кодом стану 338;
Однак у формі HTML веб-додатку такий випадок не може трапитися:
- усі параметри присутні;
- параметр [marié], значення якого береться з атрибутів [value] двох перемикачів, обов’язково має одне з значень [oui] або [non];
- у сучасних браузерах атрибути <input type=’number’ min=’0’ step=’1’ …> гарантують, що значення для дітей та зарплати обов’язково є цілими числами >=0;
Однак ніщо не заважає користувачеві ввести [Postman] і надіслати на наш сервер тестовий запит [calcul-impot-3xx], наведений вище. Ми бачили, що наш веб-додаток вміє правильно обробляти цей запит. Несподіваною помилкою ми назвемо помилку, яка не повинна виникати в рамках роботи додатка HTML. Якщо вона виникає, це, ймовірно, означає, що хтось намагається «зламати» додаток. З навчальною метою ми вирішили відображати сторінку помилок для таких випадків. Насправді можна було б повторно відобразити останню сторінку, надіслану клієнту. Для цього достатньо зберегти в сесії останню надіслану відповідь HTML. У разі несподіваної помилки ми повертаємо цю відповідь. Таким чином, у користувача складеться враження, що сервер не реагує на його помилки, оскільки відображена сторінка не змінюється.
23.13.5.1. Представлення подання
Вигляд, що відображає несподівані помилки, є таким:

Вигляд, згенерований скриптом [vue-erreurs.php], складається з трьох частин:
- 1: верхній банер генерується фрагментом [v-bandeau.php], який вже було представлено;
- 2: несподівана помилка або помилки;
- 3: меню з трьома посиланнями, згенероване фрагментом [v-menu.php];
Вигляд несподіваних помилок генерується наступним скриптом [vue-erreurs.php]:

<?php
// розраховується шаблон подання
$modèle = getModelForThisView();
function getModelForThisView(): object {
// інкапсулюються дані сторінки в $modèle
$modèle = new \stdClass();
…
// повертається шаблон
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Необхідні метатеги -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- головний блок на 12 колонок -->
<?php require "v-bandeau.php"; ?>
<!-- двоколонковий рядок -->
<div class="row">
<!-- меню на 3 колонки-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- список помилок -->
<div class="col-md-9">
<?php
print <<<EOT
<div class="alert alert-danger" role="alert">
Les erreurs inattendues suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
EOT;
?>
</div>
</div>
</div>
</body>
</html>
Коментарі
- рядок 27: включення банера додатка [1];
- рядок 32: включення меню [2]. Воно відображатиметься у трьох стовпцях під заголовком;
- рядки 34–44: відображення області помилок у дев’яти стовпцях;
- рядки 37–44: операція [print], яка відображає несподівані помилки;
- рядок 38: це відображення відбуватиметься у рамці Bootstrap на рожевому тлі;
- рядок 39: вступний текст;
- рядок 40: тег <ul> обрамлює маркований список. Цей маркований список надається шаблоном [$modèle->erreurs];
Ми вже прокоментували обидва фрагменти цього виду:
- [v-bandeau.php]: у абзаці-посиланні;
- [v-menu.php]: у абзаці-посиланні;
23.13.5.2. Візуальне тестування
Ми об’єднуємо ці різні елементи у папці [Tests] і створюємо тестовий шаблон для подання [vue-erreurs.php]:

Модель даних подання [vue-erreurs.php] буде такою:
<?php
// розраховується шаблон подання
$modèle = getModelForThisView();
function getModelForThisView(): object {
// дані сторінки інкапсулюються в $modèle
$modèle = new \stdClass();
// таблиця несподіваних помилок
$erreurs = ["erreur1", "erreur2"];
// складається список HTML помилок
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// опції меню
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// зображення банера
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// повертається шаблон
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Коментарі
- рядки 9–15: формування списку помилок HTML;
- рядки 17–20: таблиця опцій меню;
Відобразимо цей вигляд:

Отримуємо такий результат:

Працюємо над цим видом, доки візуальний результат нас не задовольнить. Після цього можна перейти до інтеграції виду у веб-додаток, який зараз створюється.
23.13.5.3. Обчислення моделі виду

Після визначення візуального вигляду подання можна приступити до розрахунку моделі подання в реальних умовах. Нагадаємо коди станів, які ведуть до цього подання. Їх можна знайти у файлі конфігурації:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Отже, саме коди станів, яких немає в рядках [2-4], викликають відображення вікна несподіваних помилок.
Код розрахунку моделі подання [vue-erreurs.php] має такий вигляд:
<?php
// успадковуються такі змінні
// Запит $request: поточний запит
// Сесія $session: сесія додатка
// масив $config: конфігурація додатка
// масив $content: відповідь контролера
//
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// розраховується модель подання
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// інкапсулюємо дані сторінки в $modèle
$modèle = new \stdClass();
// отримано помилки у відповіді контролера
$réponse = $content["réponse"];
if (!is_array($réponse)) {
// один повідомлення про помилку
$erreurs = [$réponse];
} else {
// кілька повідомлень про помилки
$erreurs = $réponse;
}
// складається список HTML помилок
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// опції меню
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// повертається шаблон
return $modèle;
}
?>
<!-- документ HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Коментарі
- рядки 19–32: обчислення шаблону [$modèle→erreurs], що використовується поданням [vue-erreurs.php];
- рядки 34–37: розрахунок шаблону [$modèle→optionsMenu], що використовується фрагментом [v-menu.php];
23.13.5.4. Тести [Postman]
Тест [calculer-impot-3xx] дає нам код стану 338, який не є очікуваним кодом стану. Відповідь HTML тоді виглядає так:

23.13.6. Реалізація дій меню додатка
Тут ми розглянемо реалізацію дій меню. Нагадаємо значення посилань, з якими ми вже стикалися
Вигляд | Посилання | Ціль | Роль |
Розрахунок податку | [Liste des simulations] | [main.php?action=lister-simulations] | Запросити список моделювань |
[Fin de session] | [main.php?action=fin-session] | ||
Список симуляцій | [Calcul de l’impôt] | [main.php?action=afficher-calcul-impot] | Переглянути розрахунок податку |
[Fin de session] | [main.php?action=fin-session] | ||
Несподівані помилки | [Calcul de l’impôt] | [main.php?action=afficher-calcul-impot] | Переглянути розрахунок податку |
[Liste des simulations] | [main.php?action=lister-simulations] | ||
[Fin de session] | [main.php?action=fin-session] |
Слід нагадати, що клік на посилання викликає GET до цілі посилання. Дії [lister-simulations, fin-session] були реалізовані за допомогою операції GET, що дозволяє нам використовувати їх як цілі посилань. Коли дія виконується за допомогою POST, використання посилання стає неможливим, якщо тільки не пов’язати його з JavaScript.
З наведених вище дій випливає, що дія [afficher-calcul-impot] ще не була реалізована. Це операція переходу між двома видами: сервери jSON або XML не мають жодних підстав для її реалізації, оскільки в них відсутнє поняття «вид». Саме сервер HTML вводить це поняття.
Отже, нам потрібно реалізувати дію [afficher-calcul-impot]. Це дасть нам змогу переглянути порядок реалізації дії на сервері.
Насамперед нам потрібно додати новий вторинний контролер. Ми назвемо його [AfficherCalculImpotController]:

Цей контролер потрібно додати до файлу конфігурації [config.json]:
{
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
…
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
…
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
…
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
- рядок 15: новий контролер;
- рядок 30: нова дія та її контролер;
- рядок 35: новий контролер поверне код стану 800. При зміні перегляду помилки бути не може;
Контролер [AfficherCalculImpotController.php] матиме такий вигляд:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Response;
class AfficherCalculImpotController implements InterfaceController {
// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
//— зміна виду — потрібно лише встановити код стану
return [Response::HTTP_OK, 800, ["réponse" => ""], []];
}
}
Коментарі
- рядок 10: як і інші вторинні контролери, новий контролер реалізує інтерфейс [InterfaceController];
- зміни переглядів легко реалізувати: достатньо встановити код стану, пов’язаний із цільовим переглядом, у даному випадку код 800, як було зазначено вище;
23.13.7. Тестування в реальних умовах
Код написано, і кожну дію перевірено за допомогою [Postman]. Залишилося перевірити послідовність переходів між видами в реальних умовах. Нам потрібен спосіб ініціалізації сесії HTML. Ми знаємо, що потрібно надіслати на сервер параметри [action=init-session&type=html]. Щоб не вводити їх вручну в адресний рядок браузера, додамо до нашого додатка скрипт [index.php]:

Скрипт [index.php] матиме такий вигляд:
<?php
// перенаправлення на [main.php] у режимі [html]
header('Location: main.php?action=init-session&type=html');
- рядок 4: [header] — це функція PHP, яка додає заголовок HTTP до відповіді. Заголовок HTTP [Location: main.php?action=init-session&type=html] вимагає від клієнтського браузера перенаправлення на ціль URL, вказану в [Location]. Скрипт [index.php] запитується разом із URL та [http://localhost/php7/scripts-web/impots/version-12/index.php]. Коли браузер клієнта отримає перенаправлення на відносну адресу URL з [main.php?action=init-session&type=html], він запитає абсолютний URL щодо [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html], і сесія HTML розпочнеться;
Початковий URL можна спростити до [http://localhost/php7/scripts-web/impots/version-12/]. У разі, якщо в URL не вказано жодної сторінки, за замовчуванням використовуються сторінки [index.html, index.php]. Отже, тут буде використано скрипт [index.php];
Почнемо: тепер ми розглянемо кілька послідовностей переходів між сторінками.
У браузері вмикаємо відстеження запитів (F12 у Firefox) і запитуємо стартовий URL [https://localhost/php7/scripts-web/impots/version-12/]:

- у [4] перша відповідь сервера — перенаправлення 302:
- на [5] надсилається новий запит до URL [http://localhost/php7/scripts-web/impots/13/main.php?action=init-session&type=html];
Давайте детальніше розглянемо перенаправлення 302:

- на [8], код HTTP [302] є кодом перенаправлення: клієнтському браузеру повідомляється, що запитуваний URL був переміщений. Нова сторінка URL вказана як [9]. Браузер виконає це перенаправлення, надіславши новий запит GET:

- на [12-13] — новий запит, зроблений браузером;
Заповнимо отриману форму;

Тоді давайте проведемо кілька симуляцій:


Запитуємо список моделювань:

Видаляємо першу симуляцію:

Завершимо сесію:

Читачеві пропонується провести інші тести.
23.14. Клієнт веб-сервісу jSON
23.14.1. Архітектура «клієнт-сервер»

Тепер розглянемо клієнт jSON [A] веб-сервісу [B]. Клієнт [A], як і веб-сервіс [B], має багаторівневу структуру:

Ця архітектура відображається у такій організації коду:

Більшість класів ми вже розглядали та пояснювали:
абзац-посилання. | |
абзац-посилання. | |
абзац-посилання. | |
абзац-посилання. | |
абзац-посилання. | |
абзац-посилання. |
23.14.2. Шар [dao]

23.14.2.1. Інтерфейс
Інтерфейс шару [dao] буде таким: [InterfaceClientDao.php]:
<?php
// простір імен
namespace Application;
interface InterfaceClientDao {
// читання даних платника податків
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// розрахунок податків платника податків
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
// запис результатів
public function saveResults(string $resultsFilename, array $simulations): void;
// аутентифікація
public function authentifierUtilisateur(String $user, string $password): void;
// перелік моделювань
public function listerSimulations(): array;
// видалення моделювання
public function supprimerSimulation(int $numéro): array;
// початок сеансу
public function initSession(string $type = 'json'): void;
// завершення сеансу
public function finSession(): void;
}
Коментарі
- рядок 9: метод [getTaxPayersData] дозволяє обробляти файл jSON із даними платників податків. Цей метод реалізовано за допомогою функції [TraitDao], про яку вже йшлося (абзац «посилання»);
- рядок 15: метод [saveResults] дозволяє зберегти результати декількох розрахунків податку у файлі jSON. Тут також цей метод реалізовано за допомогою функції [TraitDao], про яку вже йшлося (див. абзац за посиланням);
- рядки 12, 18, 21, 27, 30: для кожної з дій, що підтримуються веб-сервісом, створено окремий метод;
23.14.2.2. Реалізація
Інтерфейс [InterfaceClientDao] реалізовано за допомогою наступного класу [ClientDao]:
<?php
namespace Application;
// залежності
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\CurlResponse;
class ClientDao implements InterfaceClientDao {
// використання Trait
use TraitDao;
// атрибути
private $urlServer;
private $sessionCookie;
private $verbose;
// конструктор
public function __construct(string $urlServer, bool $verbose = TRUE) {
$this->urlServer = $urlServer;
$this->verbose = $verbose;
}
…
}
Коментарі
- рядки 18–21: конструктор отримує два параметри:
- URL [$urlServer] веб-сервісу jSON;
- булеве значення [$verbose], яке в TRUE вказує, що клас повинен виводити відповіді сервера на консоль;
- рядок 14: сесійний файл cookie. Його роль було описано у версії 09 клієнта (параграф «посилання»);
- рядок 11: клас використовує рис [TraitDao], який реалізує два методи інтерфейсу:
- [getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array];
- [function calculerImpot(string $marié, int $enfants, int $salaire): Simulation];
23.14.2.2.1. Метод [initSession]
Метод [initSession] реалізовано наступним чином:
public function initSession(string $type = 'json'): void {
// створення клієнта HTTP
$httpClient = HttpClient::create();
// надсилання запиту на сервер без автентифікації
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "init-session",
"type" => $type
],
"verify_peer" => false
]);
// отримуємо відповідь
$this->getResponse($response);
// отримуємо сесійний файл cookie
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// сесійний файл cookie?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
Оскільки дія [init-session] має бути першою дією, що запитується у веб-сервісі, метод [initSession] буде першим методом рівня [dao], який буде викликано.
Коментарі
- рядок 1: тип бажаної сесії передається як параметр. У разі відсутності параметра буде запущено сесію jSON;
- рядки 5–11: до веб-сервісу надсилається запит GET;
- рядки 7–8: два параметри запиту GET;
- рядок 10: у разі захищеного обміну даними (протокол https) сертифікат безпеки, надісланий веб-сервісом, не перевірятиметься;
- рядок 13: метод [getResponse] отримує відповідь від сервера. Він повертає її у вигляді масиву. У цьому випадку результат методу не використовується. Метод [getResponse] генерує виняток, якщо код HTTP у відповіді веб-сервісу відрізняється від 200 OK;
- рядки 14–25: оскільки метод [initSession] є першим методом шару [dao], що виконується, отримується сесійний файл cookie, щоб наступні методи могли надіслати його назад до веб-сервісу. Цей код уже було прокоментовано у версії 09;
23.14.2.2.2. Метод [getResponse]
Метод [getResponse] відповідає за обробку відповіді веб-сервісу:
private function getResponse(CurlResponse $response) {
// отримуємо відповідь
$json = $response->getContent(false);
// журнали
if ($this->verbose) {
print "$json\n";
}
// отримуємо статус відповіді
$statusCode = $response->getStatusCode();
// помилка?
if ($statusCode !== 200) {
// виникла помилка
throw new ExceptionImpots($json);
}
// надається відповідь
$array = json_decode($json, true);
return $array["réponse"];
}
Коментарі
- рядок 1: метод є приватним;
- рядок 1: параметром методу є відповідь веб-сервісу типу [Symfony\Component\HttpClient\Response\CurlResponse] — тип відповіді Symfony, коли [HttpClient] реалізується методом [CurlClient], тобто бібліотекою [curl];
- рядок 3: отримуємо відповідь jSON від сервера. Нагадаємо, що параметр [false] призначений для того, щоб запобігти виклику Symfony винятку, коли статус відповіді HTTP від сервера знаходиться в діапазоні [3xx, 4xx, 5xx];
- рядки 5–7: якщо ми перебуваємо в режимі [$verbose], то виводимо відповідь сервера на консоль;
- рядки 9–14: якщо статус відповіді сервера HTTP відрізняється від 200, то генерується виняток із повідомленням про помилку, що відповідає відповіді сервера jSON;
- рядок 16: рядок jSON декодується в масив;
- рядок 17: корисна інформація міститься в [$array["réponse"]];
23.14.2.2.3. Метод [authentifierUtilisateur]
Метод [authentifierUtilisateur] виглядає наступним чином:
public function authentifierUtilisateur(string $user, string $password): void {
// створюється клієнт HTTP
$httpClient = HttpClient::create();
// надсилаємо запит на сервер з автентифікацією
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "authentifier-utilisateur"
],
"body" => [
"user" => $user,
"password" => $password
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// отримуємо відповідь
$this->getResponse($response);
}
Коментарі
- рядок 5: запит клієнта — це POST;
- рядки 6–8: параметри у файлі URL;
- рядки 9–12: параметри POST;
- рядок 14: сесійний файл cookie;
- рядок 17: зчитуємо відповідь. Відомо, що в разі помилки (код HTTP, відмінний від 200) метод [getResponse] сам генерує виняток;
23.14.2.2.4. Метод [calculerImpot]
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
// створюється клієнт HTTP
$httpClient = HttpClient::create();
// надсилається запит до сервера без автентифікації, але з сесійним файлом cookie
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "calculer-impot"],
"body" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// отримуємо відповідь
$array = $this->getResponse($response);
return (new Simulation())->setFromArrayOfAttributes($array);
}
Коментарі
- рядки 6–7: єдиний параметр методу URL;
- рядки 8–12: три параметри методу POST (рядок 5);
- рядок 17: відповідь обробляється;
- рядок 18: якщо ми дійшли до цього місця, значить, метод [getResponse] не викликав винятку. Повертається об’єкт [Simulation], ініціалізований масивом, повернутим методом [getResponse];
23.14.2.2.5. Метод [listerSimulations]
public function listerSimulations(): array {
// створюється клієнт HTTP
$httpClient = HttpClient::create();
// надсилається запит до сервера без автентифікації, але з сесійним файлом cookie
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "lister-simulations"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// отримуємо відповідь
return $this->getSimulations($response);
}
Коментарі
- рядок 5: метод GET;
- рядки 6–8: єдиний параметр методу GET;
- рядок 13: отримання результатів моделювання доручено приватному методу [getSimulations];
23.14.2.2.6. Метод [getSimulations]
private function getSimulations(CurlResponse $response): array {
// отримуємо відповідь JSON
$array = $this->getResponse($response);
// отримано масив асоціативних об’єктів
// перетворимо його на масив об’єктів Simulation
$simulations = [];
foreach ($array as $simulation) {
$simulations [] = (new Simulation())->setFromArrayOfAttributes($simulation);
}
// повертаємо список об’єктів «Simulation»
return $simulations;
}
Коментарі
- рядок 3: отримуємо масив із відповіді. Це масив масивів, кожен з яких має всі атрибути об’єкта [Simulation];
- рядок 6: якщо ми дійшли до цього місця, це означає, що метод [getResponse] не викликав винятку;
- рядки 6–9: використовуємо відповідь для побудови масиву об’єктів [Simulation];
- рядок 11: повертаємо цей масив;
23.14.2.2.7. Метод [SupprimerSimulation]
public function supprimerSimulation(int $numéro): array {
// створюємо клієнта HTTP
$httpClient = HttpClient::create();
// надсилаємо запит на сервер без автентифікації, але з сесійним кукі
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "supprimer-simulation",
"numéro" => $numéro
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// отримуємо відповідь
return $this->getSimulations($response);
}
Коментарі
- рядок 5: виконується запит GET;
- рядки 6–9: два параметри URL;
- рядок 14: після видалення сервер повертає новий масив симуляцій. Цей масив повертається;
23.14.2.2.8. Метод [finSession]
Сесія роботи з веб-сервісом зазвичай завершується викликом методу [finSession]:
public function finSession(): void {
// створюємо клієнт HTTP
$httpClient = HttpClient::create();
// надсилається запит до сервера без автентифікації, але з сесійним файлом cookie
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "fin-session"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// отримуємо відповідь
$this->getResponse($response);
}
Коментарі
- рядок 5: надсилається запит GET;
- рядки 6–8: єдиний параметр URL;
- рядок 13: зчитується відповідь. Якщо код HTTP у відповіді відрізняється від 200, генерується виняток;
23.14.3. Рівень [métier]

23.14.3.1. Інтерфейс
Інтерфейс шару [métier] має такий вигляд: [InterfaceClientMetier.php]:
<?php
// простір імен
namespace Application;
interface InterfaceClientMetier {
// розрахунок податків платника податків
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
// розрахунок податків у пакетному режимі
public function executeBatchImpots(string $taxPayersFileName, string $resultsFilename, string $errorsFileName): void;
// аутентифікація
public function authentifierUtilisateur(String $user, string $password): void;
// перелік моделювань
public function listerSimulations(): array;
// запис результатів
public function saveResults(string $resultsFilename, array $simulations): void;
// видалення моделювання
public function supprimerSimulation(int $numéro): array;
// початок сеансу
public function initSession(string $type = 'json'): void;
// завершення сеансу
public function finSession(): void;
}
Коментарі
- лише метод [executeBatchImpots] у рядку 12 є специфічним для шару [métier]. Усі інші належать до шару [dao], який їх реалізує;
23.14.3.2. Клас [ClientMetier]
Клас, що реалізує шар [métier], є таким:
<?php
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// атрибут
private $clientDao;
// виробник
public function __construct(InterfaceClientDao $clientDao) {
$this->clientDao = $clientDao;
}
// розрахунок податку
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// розрахунок податків у пакетному режимі
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// дозволяється передавати винятки, що походять з рівня [dao]
// отримання даних про платників податків
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// таблиця результатів
$simulations = [];
// здійснюється їх обробка
foreach ($taxPayersData as $taxPayerData) {
// розраховується податок
$simulations [] = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
}
// запис результатів
if ($resultsFileName !== NULL) {
$this->clientDao->saveResults($resultsFileName, $simulations);
}
}
public function authentifierUtilisateur(String $user, string $password): void {
$this->clientDao->authentifierUtilisateur($user, $password);
}
public function listerSimulations(): array {
return $this->clientDao->listerSimulations();
}
public function saveResults(string $resultsFilename, array $simulations): void {
$this->clientDao->saveResults($resultsFilename, $simulations);
}
public function supprimerSimulation(int $numéro): array {
return $this->clientDao->supprimerSimulation($numéro);
}
public function finSession(): void {
$this->clientDao->finSession();
}
public function initSession(string $type = 'json'): void {
$this->clientDao->initSession($type);
}
}
Коментарі
- рядки 10–12: для створення шару [métier] потрібне посилання на шар [dao];
- рядки 20–38: лише метод [executeBatchImpots] є специфічним для шару [métier]. Реалізація інших методів делегує роботу, яку потрібно виконати, методам з такими самими іменами у шарі [dao];
- рядок 23: звертаємося до шару [dao], щоб отримати в масиві об’єктів типу [TaxPayerData] дані про платників податків;
- рядок 25: у масиві [$simulations] підсумовуються різні розраховані моделювання;
- рядки 27–33: обчислюється податок для кожного платника податків з масиву [$taxPayersData];
- рядки 35–37: результати, отримані в таблиці [$simulations], зберігаються у файлі jSON;
Примітка: Шар [métier] практично нічого не робить. Можна вирішити видалити його та об’єднати все в шарі [dao].
23.14.4. Головний скрипт

Головний скрипт налаштовується за допомогою такого файлу [config.json]:
{
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12",
"dependencies": [
"/Entities/BaseEntity.php",
"/Entities/TaxPayerData.php",
"/Entities/Simulation.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Utilitaires.php",
"/Model/InterfaceClientDao.php",
"/Model/TraitDao.php",
"/Model/ClientDao.php",
"/Model/InterfaceClientMetier.php",
"/Model/ClientMetier.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php"
],
"user": {
"login": "admin",
"passwd": "admin"
},
"urlServer": "https://localhost:443/php7/scripts-web/impots/version-12/main.php"
}
Основний скрипт [main.php] має такий вигляд:
<?php
// суворе дотримання заявлених типів параметрів функцій
declare(strict_types = 1);
// простір імен
namespace Application;
// обробка помилок за допомогою PHP
// ini_set("display_errors", "0");
//
// шлях до файлу конфігурації
define("CONFIG_FILENAME", "../Data/config.json");
// завантажуємо конфігурацію
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// включення необхідних для скрипта залежностей
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// абсолютні залежності (сторонні бібліотеки)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// визначення констант
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// залежності Symfony
use Symfony\Component\HttpClient\HttpClient;
// створення шару [dao]
$clientDao = new ClientDao($config["urlServer"]);
// створення шару [métier]
$clientMetier = new ClientMetier($clientDao);
// розрахунок податків у пакетному режимі
try {
// ініціалізація сеансу
$clientMetier->initSession('json');
// аутентифікація
$clientMetier->authentifierUtilisateur($config["user"]["login"], $config["user"]["passwd"]);
// розрахунок податків без збереження результатів
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, NULL, ERRORS_FILENAME);
// перелік моделювань
$clientMetier->listerSimulations();
// видалення моделювання
$simulations = $clientMetier->supprimerSimulation(1);
// збереження результатів
$clientMetier->saveResults(RESULTS_FILENAME, $simulations);
// завершення сеансу
$clientMetier->finSession();
// дія без авторизації — має призвести до збою
$clientMetier->listerSimulations();
} catch (ExceptionImpots $ex) {
// виводиться повідомлення про помилку
print "Une erreur s'est produite : " . $ex->getMessage() . "\n";
}
// кінець
print "Terminé\n";
exit();
Коментарі
- рядки 12–16: використання файлу конфігурації [config.json];
- рядки 18–26: завантаження всіх залежностей;
- рядки 28–34: визначення констант та псевдонімів;
- рядки 36–39: побудова шарів [dao] та [métier];
- рядок 44: ініціалізація сеансу jSON;
- рядок 46: авторизація на сервері;
- рядок 48: обчислення податку для низки платників податків. Результати не зберігаються (2-й параметр NULL);
- рядок 50: запит результатів усіх цих розрахунків;
- рядок 52: видаляється симуляція № 1 (друга у списку);
- рядок 54: збереження решти симуляцій;
- рядок 56: завершується сесія. Це означає, що сесійний файл cookie знищується;
- рядок 58: запитується список симуляцій. Оскільки сесійний файл cookie було знищено, автентифікацію потрібно виконати заново. Тому має з’явитися виняток, що повідомляє про відсутність автентифікації;
Файл [taxpayersdata.json] має такий вигляд:
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
Є 12 платників податків, з яких 1 є неправильним. Отже, загалом 11 симуляцій. Одну з них буде видалено. Має залишитися 10.
Після виконання головного скрипта файл jSON [results.json] має такий вигляд:
[
{
"marié": "oui",
"enfants": "2",
"salaire": "55555",
"impôt": 2814,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "oui",
"enfants": "3",
"salaire": "50000",
"impôt": 0,
"surcôte": 0,
"décôte": 720,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "2",
"salaire": "100000",
"impôt": 19884,
"surcôte": 4480,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "non",
"enfants": "3",
"salaire": "100000",
"impôt": 16782,
"surcôte": 7176,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "3",
"salaire": "100000",
"impôt": 9200,
"surcôte": 2180,
"décôte": 0,
"réduction": 0,
"taux": 0.3
},
{
"marié": "oui",
"enfants": "5",
"salaire": "100000",
"impôt": 4230,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "0",
"salaire": "100000",
"impôt": 22986,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "2",
"salaire": "30000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
},
{
"marié": "non",
"enfants": "0",
"salaire": "200000",
"impôt": 64210,
"surcôte": 7498,
"décôte": 0,
"réduction": 0,
"taux": 0.45
},
{
"marié": "oui",
"enfants": "3",
"salaire": "20000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
}
]
Дійсно, симуляцій 10.
Файл jSON [errors.json] має такий вміст:
{
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
Результати виведення на консоль такі (у режимі verbose відповіді сервера jSON відображаються на консолі):
{"action":"init-session","état":700,"réponse":"session démarrée avec type [json]"}
{"action":"authentifier-utilisateur","état":200,"réponse":"Authentification réussie [admin, admin]"}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"lister-simulations","état":500,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"supprimer-simulation","état":600,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"fin-session","état":400,"réponse":"session supprimée"}
{"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Une erreur s'est produite : {"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Terminé
23.14.5. Тести [Codeception]
Як і у випадку з попередніми клієнтами, клієнт версії 12 може підлягати тестуванню [Codeception]:

Код тестового класу шару [métier] клієнта аналогічний коду тестових класів попередніх клієнтів:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// визначення констант
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12");
// шлях до файлу конфігурації
define("CONFIG_FILENAME", ROOT . "/Data/config.json");
// отримання конфігурації
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// включення необхідних для скрипта залежностей
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// абсолютні залежності (сторонні бібліотеки)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// залежності Symfony
use Symfony\Component\HttpClient\HttpClient;
// клас тестування
class ClientDaoTest extends \Codeception\Test\Unit {
// DAO-шар
private $clientDao;
public function __construct() {
parent::__construct();
// отримання конфігурації
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// створення шару [dao]
$clientDao = new ClientDao($config["urlServer"]);
// створення шару [métier]
$this->métier = new ClientMetier($clientDao);
// ініціалізація сеансу
$this->métier->initSession("json");
// аутентифікація
$this->métier->authentifierUtilisateur("admin", "admin");
}
// тестування
public function test1() {
$simulation = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $simulation->getImpôt(), 1);
$this->assertEqualsWithDelta(0, $simulation->getSurcôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getDécôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getRéduction(), 1);
$this->assertEquals(0.14, $simulation->getTaux());
}
public function test2() {
….
}
…
public function test11() {
…
}
}
Коментарі
- рядки 34–46: нагадаємо, що конструктор тестового класу виконується перед кожним тестом;
- рядки 38–41: створення шарів [dao] та [métier];
- рядки 42–45: тестові методи [test1…, test11] перевіряють метод [calculerImpot]. Щоб це стало можливим, спочатку потрібно ініціалізувати сесію jSON та пройти автентифікацію;
Результати тестування такі:

Слід провести ще багато інших тестів:
- перевірити різні методи рівня [dao];
- перевірити статуси, що повертаються веб-сервером. Ці статуси є важливими, оскільки від їхнього значення залежить, яку сторінку HTML слід відобразити;