19. Практичне завдання — версія 9
У цій версії ми вдосконалимо сервер таким чином:
- наразі при кожному запиті дані податкової адміністрації шукаються в базі даних. Ми будемо використовувати сесію:
- під час першого запиту користувача дані податкової служби шукаються в базі даних і зберігаються в сесії;
- під час наступних запитів того самого користувача дані податкової служби шукаються в сесії. Можна сподіватися на незначне скорочення часу виконання, оскільки запити до бази даних є ресурсоємними;
- сервер буде записувати в текстовий файл такі важливі моменти:
- успішну або невдалу автентифікацію;
- дійсність або недійсність параметрів, надісланих клієнтом;
- результат розрахунку податку;
- різні випадки помилок;
- у разі фатальної помилки адміністратору додатка буде надіслано електронний лист;
Клієнт також повинен бути модифікований для обробки сесійного файлу cookie, який ми йому надішлемо.
19.1. Сервер
Ми розглядаємо серверну частину додатка.

Ця архітектура буде реалізована за допомогою таких скриптів:

19.1.1. Утиліти

19.1.1.1. Клас [Logger]
Клас [Logger] використовуватиметься для запису журналів у текстовий файл:
<?php
namespace Application;
class Logger {
// атрибут
private $resource;
// конструктор
public function __construct(string $logsFilename) {
// відкриття файлу
$this->resource = fopen($logsFilename, "a");
if (!$this->resource) {
throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
}
}
// запис повідомлення в журнали
public function write(string $message) {
fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
}
// закриття файлу журналів
public function close() {
fclose($this->resource);
}
}
Коментарі
- рядок 7: ресурс файлу журналів;
- рядок 10: конструктор класу отримує як параметр ім’я файлу журналу;
- рядок 12: текстовий файл відкривається в режимі додавання (a+): файл буде відкрито, його вміст збережено. Записи будуть додаватися після поточного вмісту;
- рядки 13–15: якщо відкрити файл не вдалося, генерується виняток;
- рядки 19–21: метод [write] дозволяє записати повідомлення [$message] у файл журналу, додавши перед ним дату та час;
- рядки 24–16: метод [close] дозволяє закрити файл журналу;
Примітка: серверний додаток може обслуговувати декількох клієнтів одночасно. При цьому для всіх них використовується єдиний файл журналу. Отже, існує ризик паралельного доступу для запису в цей файл. Тому необхідно синхронізувати записи, щоб уникнути їхнього змішування. Для цього PHP використовує семафори [https://www.php.net/manual/fr/book.sem.php]. У цьому прикладі ми не будемо розглядати синхронізацію записів, але слід пам’ятати про цю проблему.
19.1.1.2. Клас [SendAdminMail]
Клас [SendAdminMail] дозволяє надіслати електронний лист адміністратору додатка у разі його збою:
<?php
namespace Application;
class SendAdminMail {
// атрибути
private $config;
private $logger;
// конструктор
public function __construct(array $config, Logger $logger = NULL) {
$this->config = $config;
$this->logger = $logger;
}
public function send() {
// надсилає $this->config['message'] на SMTP-сервер $this->config['smtp-server'] на порт $infos[smt-port]
// якщо $this->config['tls'] має значення «true», буде використано підтримку TLS
// лист надсилається від імені $this->config['from']
// для одержувача $this->config['to']
// тема повідомлення: $this->config['subject']
// до листа додаються вкладення з $this->config['attachments']
// результат застосування методу
try {
// створення повідомлення
$message = (new \Swift_Message())
// тема повідомлення
->setSubject($this->config["subject"])
// відправник
->setFrom($this->config["from"])
// адресати з використанням словника (setTo/setCc/setBcc)
->setTo($this->config["to"])
// текст повідомлення
->setBody($this->config["message"])
;
// вкладення
foreach ($this->config["attachments"] as $attachment) {
// шлях до вкладення
$fileName = __DIR__ . $attachment;
// перевіряється наявність файлу
if (file_exists($fileName)) {
// додаємо документ до повідомлення
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
if ($this->logger !== NULL) {
// помилка
$this->logger->write("L'attachement [$fileName] n'existe pas\n");
}
}
}
// протокол TLS?
if ($this->config["tls"] === "TRUE") {
// TLS
$transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
->setUsername($this->config["user"])
->setPassword($this->config["password"]);
} else {
// немає TLS
$transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
}
// менеджер відправлення
$mailer = new \Swift_Mailer($transport);
// відправлення повідомлення
$mailer->send($message);
// кінець
if ($this->logger !== NULL) {
$this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
}
} catch (\Throwable $ex) {
// помилка
if ($this->logger !== NULL) {
$this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
}
}
}
}
Коментарі
- рядок 11: конструктор отримує два параметри:
- [$config]: асоціативний масив, що містить усю необхідну інформацію для надсилання електронного листа;
- [$logger]: логер, що дозволяє реєструвати важливі моменти під час надсилання електронного листа;
Асоціативний масив матиме такий вигляд:
- рядки 16–76: метод [send] дозволяє надіслати електронний лист. Цей код було представлено та описано у розділі за посиланням;
19.1.2. Рівень [dao]

Скрипт [ServeurDaoWithSession.php] має такий вигляд:
<?php
// простір імен
namespace Application;
// визначення класу ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {
// конструктор
public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
// найпростіший випадок
if ($taxAdminData !== NULL) {
$this->taxAdminData = $taxAdminData;
} else {
// передача управління батьківському класу
parent::__construct($databaseFilename);
}
}
}
Коментарі
- рядок 7: клас [ServerDaoWithSession] версії 09 розширює клас [ServerDao] версії 08. Адже клас [ServerDao] вміє працювати з базою даних. Залишається лише передбачити випадок, коли дані податкової адміністрації вже отримано:
- рядок 10: конструктор тепер отримує два параметри:
- [string $databaseFilename]: ім’я файлу, що містить інформацію для підключення до бази даних, якщо дані податкової служби ще не отримано, NULL — в іншому випадку;
- [TaxAdminData $taxAdminData]: дані податкової служби, якщо вони вже отримані; NULL — в іншому випадку;
Під час запуску веб-сесії шар [dao] буде побудовано з об’єктом [$databaseFilename], що не є NULL, та об’єктом [taxAdminData] NULL. Дані податкової служби будуть потім витягнуті з бази даних і збережені в сесії. Під час наступних запитів у рамках тієї самої сесії шар [dao] буде побудовано з об’єктом [databaseFilename], NULL та об’єктом [taxAdminData], отриманими з сесії, а не з NULL. Отже, пошук у базі даних не відбуватиметься.
19.1.3. Серверний скрипт
Серверний скрипт [impots-server.php] налаштовується за допомогою такого файлу: jSON [config-server.json]:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
"databaseFilename": "Data/database.json",
"relativeDependencies": [
"/../version-08/Entities/BaseEntity.php",
"/../version-08/Entities/ExceptionImpots.php",
"/../version-08/Entities/TaxAdminData.php",
"/../version-08/Entities/Database.php",
"/../version-08/Dao/InterfaceServerDao.php",
"/../version-08/Dao/ServerDao.php",
"/Dao/ServerDaoWithSession.php",
"/../version-08/Métier/InterfaceServerMetier.php",
"/../version-08/Métier/ServerMetier.php",
"/Utilities/Logger.php",
"/Utilities/SendAdminMail.php"
],
"absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/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": "Data/logs.txt"
}
Серверний скрипт [impots-server.php] змінюється наступним чином:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// обробка помилок за допомогою PHP
ini_set("display_errors", "0");
//
// шлях до файлу конфігурації
define("CONFIG_FILENAME", "Data/config-server.json");
// отримання конфігурації
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// включення необхідних для скрипта залежностей
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// абсолютні залежності (сторонні бібліотеки)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// сесія
$session = new Session();
$session->start();
// підготовка відповіді JSON від сервера
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// створення файлу журналу
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// внутрішня помилка сервера
doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
// завершено
exit;
}
// перший запис у журналі
$logger->write("\n---nouvelle requête\n");
// отримано поточний запит
$request = Request::createFromGlobals();
// аутентифікація лише під час першого входу
if (!$session->has("user")) {
// журнал
$logger->write("Autentification en cours…\n");
// аутентифікація
…
}
// чи знайдено користувача?
if (!$trouvé) {
// не знайдено — код 401 HTTP_UNAUTHORIZED
sendResponse(
$response,
["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
Response::HTTP_UNAUTHORIZED,
["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
$logger
);
// завершено
exit;
} else {
// у сесії зазначається, що користувач пройшов автентифікацію
$session->set("user", TRUE);
// журнал
$logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
}
} else {
// журнал
$logger->write("Authentification prise en session…\n");
}
// користувач дійсний — перевіряються отримані параметри
$erreurs = [];
// повинно бути три параметри GET
…
// помилки?
if ($erreurs) {
// клієнту надсилається код помилки 400 HTTP_BAD_REQUEST
sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
// завершено
exit;
} else {
// журнали
$logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// у нас є все необхідне для роботи
// створення шару [dao]
if (!$session->has("taxAdminData")) {
// дані беруться з бази даних
$logger->write("données fiscales prises en base de données\n");
try {
// побудова шару [dao]
$dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
// дані заносяться в сесію
$session->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// фіксується помилка
doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
// завершено
exit;
}
} else {
// дані отримано з сеансу
$dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
// журнали
$logger->write("données fiscales prises en session\n");
}
// створення шару [métier]
$métier = new ServerMetier($dao);
// розрахунок податку
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// надається відповідь
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// кінець
exit;
function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
// надсилання електронного листа адміністратору
// SendAdminMail перехоплює всі винятки та самостійно записує їх у журнал
$infos['message'] = $message;
$sendAdminMail = new SendAdminMail($infos, $logger);
$sendAdminMail->send();
// відправляється код помилки 500 клієнту
sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}
// функція відправлення відповіді HTTP клієнту
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
// $response: відповідь HTTP
// $result: таблиця результатів
// $statusCode: статус HTTP відповіді
// $headers: заголовки HTTP, які слід включити у відповідь
// $logger: модуль реєстрації додатка
//
// статус HTTTP
$response->setStatusCode($statusCode);
// тіло повідомлення
$body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$response->setContent($body);
// заголовки
$response->headers->add($headers);
// відправлення
$response->send();
// журнал
if ($logger != NULL) {
$logger->write("$body\n");
$logger->close();
}
}
Коментарі
- рядки 34–35: запускається сесія;
- рядки 38–40: готується відповідь jSON;
- рядки 42–50: здійснюється спроба створення файлу журналу. У разі виникнення винятку викликається метод [doInternalServer] (рядки 132–140);
- рядок 132: метод [doInternalServer] приймає чотири параметри:
- [$message]: повідомлення, яке потрібно занести до журналу. Повинно бути закодовано у форматі UTF-8;
- [$response]: об’єкт [Response], який інкапсулює відповідь сервера своєму клієнту;
- [$logger]: об’єкт [Logger], що дозволяє створювати журнали;
- [$infos]: інформація, що дозволяє надіслати електронний лист адміністратору додатка;
- рядки 135–137: надсилання електронного листа адміністратору додатка;
- рядок 139: надсилання відповіді клієнту:
- $response: відповідь HTTP;
- $result: сервер надсилає рядок jSON із масиву [‘réponse’=>["erreur" => $message]];
- $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], код 500;
- $headers: [], заголовки HTTP, які слід додати до відповіді, відсутні;
- $logger: модуль реєстрації додатка;
- рядок 58: завдяки створеній сесії автентифікація клієнта відбуватиметься лише один раз:
- після автентифікації клієнта в сесію буде додано ключ [user] (рядок 78);
- під час наступного запиту від того самого клієнта рядок 58 дозволяє уникнути аутентифікації, яка стала непотрібною;
- рядок 103: завдяки створеній сесії пошук даних у базі даних відбуватиметься лише один раз:
- під час першого запиту буде виконано пошук у базі даних (рядок 108). Отримані дані потім додаються до сесії (рядок 110) у поєднанні з ключем [taxAdminData];
- під час наступних запитів ключ [taxAdminData] буде знайдено в сесії (рядок 103), і тоді дані з диска будуть безпосередньо передані на рівень [dao] (рядок 119);
- рядки 111–116: пошук податкових даних у базі може завершитися невдачею. У цьому випадку клієнту надсилається код [500 Internal Server Error];
- рядок 113: повідомлення про помилку винятку драйвера MySQL кодується як ISO 8859-1. Його перетворюють на UTF-8 для правильного запису в журнал;
- решта коду майже ідентична коду попередньої версії;
- рядки 143–164: функція [sendResponse] надсилає всі відповіді клієнту;
- рядки 144–148: значення параметрів;
- рядок 153: відповідь завжди є рядком jSON з масиву [‘résultat’=>qqChose];
- рядок 156: іноді до відповіді потрібно додати заголовки HTTP. Це відбувається в рядку 71;
- рядок 158: відповідь надіслано;
- рядки 160–163: відповідь записується в журнал, а журнал закривається;
19.1.4. Тести [Codeception]

Ми будемо тестувати лише рівень [dao], який є єдиним, що зазнав змін.
Код тесту [ServerDaoTest] такий:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// визначення констант
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// шлях до файлу конфігурації
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// отримання конфігурації
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// включення необхідних для скрипта залежностей
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// абсолютні залежності (сторонні бібліотеки)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// тест -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// батьківський
parent::__construct();
// отримання конфігурації
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// створення шару [dao]
$dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// тестування
public function testTaxAdminData() {
…
}
}
- рядки 9–24: створюється середовище виконання, ідентичне до того, що використовується у серверному скрипті [impots-server];
- рядок 38: для побудови шару [dao] створюється екземпляр класу [ServerDaoWithSession];
Результат тестування такий:

19.2. Клієнт
Ми розглянемо клієнтську частину додатка.

Ця архітектура буде реалізована за допомогою таких скриптів:

У новій версії змінюються лише:
- файл конфігурації [config-client.json];
- рівень [dao] клієнта;
19.2.1. шар [dao]
Рівень [Dao] змінюється наступним чином:
<?php
namespace Application;
// залежності
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// використання Trait
use TraitDao;
// атрибути
private $urlServer;
private $user;
private $sessionCookie;
// конструктор
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// розрахунок податку
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// сесійний файл cookie?
if (!$this->sessionCookie) {
// створення клієнта HTTP
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// надсилається запит на сервер без сесійного файлу cookie
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]
]);
} else {
// надсилається запит на сервер із сесійним файлом cookie
// створюється клієнт HTTP
$httpClient = HttpClient::create([
"verify_peer" => false
]);
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"headers" => ["Cookie" => $this->sessionCookie]
]);
}
// отримується відповідь
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// журнали
print "$json=json\n";
// отримуємо статус відповіді
$statusCode = $response->getStatusCode();
// помилка?
if ($statusCode !== 200) {
// виникла помилка — генерується виняток
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// отримуємо сесійний файл 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];
}
}
}
}
// повертаємо відповідь
return $réponse;
}
}
Коментарі
Зміна шару [dao] полягає в тому, що тепер здійснюється управління сеансом:
- рядок 14: сесійний файл cookie;
- рядки 25–39: під час першого запиту цей файл cookie відсутній: тоді надсилається запит до сервера з даними автентифікації (рядок 28);
- рядки 40–53: під час наступних запитів, як правило, є файл cookie сеансу. Тоді дані для автентифікації не надсилаються (рядки 42–44);
- рядки 69–82: відповідь сервера на перший запит міститиме сесійний файл cookie. Його отримуємо. Цей код уже використовувався та пояснювався у розділі за посиланням;
- рядок 78: отриманий сесійний файл cookie зберігається в атрибуті класу [$sessionCookie];
Примітка: можна було б залишити стару версію шару [dao] і здійснювати автентифікацію при кожному запиті, оскільки її вартість є незначною. З навчальною метою ми вирішили нагадати, як клієнт HTTP може керувати сесією.
19.2.2. Файл конфігурації
Файл конфігурації jSON змінюється наступним чином:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": [
"/../version-08/Entities/BaseEntity.php",
"/../version-08/Entities/TaxPayerData.php",
"/../version-08/Entities/ExceptionImpots.php",
"/../version-08/Utilities/Utilitaires.php",
"/../version-08/Dao/InterfaceClientDao.php",
"/../version-08/Dao/TraitDao.php",
"/Dao/ClientDao.php",
"/../version-08/Métier/InterfaceClientMetier.php",
"/../version-08/Métier/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-09/impots-server.php"
}
Змінюється лише URL у рядку 24.
19.3. Кілька тестів
19.3.1. Тест 1
Спочатку запускаємо клієнт у середовищі без помилок. Результати залишаються такими ж, як і в попередніх версіях. Але тепер на стороні сервера з’являється файл журналу [logs.txt]:
04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours…
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session…
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session…
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête
…
- рядки 3–7: під час першого запиту відбувається автентифікація та пошук даних у базі;
- рядки 9–14: під час наступного запиту автентифікація більше не відбувається, а дані беруться із сесії. Це повторюється під час наступних запитів (рядки 15 і далі);
19.3.2. Тест 2
Тепер відключимо базу даних MySQL. На стороні клієнта ми отримуємо такий результат у консолі:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}
Terminé
На стороні сервера ми бачимо такі журнали [logs.txt]:
04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête
…
Щоб отримати електронний лист, який отримав адміністратор додатка, використовуємо скрипт [imap-03.php] з розділу, пов’язаного з таким файлом конфігурації [config-imap-01.json]:
Отримуємо такий результат:

Файл [message_1.txt] містить такий текст:
return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable
SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
19.3.3. Тест 3
Тепер зробимо так, щоб файл [logs.txt] не міг бути створений. Для цього достатньо створити папку [logs.txt]:

Зробивши це, запустимо клієнт.
На стороні клієнта ми бачимо такі результати у консолі:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé
На стороні сервера журналів немає, але адміністратор отримує такий лист:
return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable
Echec lors de la création du fichier de logs [Data/logs.txt]
19.3.4. Тест 4
Цього разу в конфігураційному файлі клієнта вкажемо неправильні облікові дані для клієнта, який підключається.
Клієнт виводить на консоль такі результати:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
На стороні сервера з’являються такі журнали:
---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}
19.3.5. Тест 5
Повернемо правильний ідентифікатор користувача [admin, admin] у файл конфігурації клієнта.
Тепер звернемося до сервера безпосередньо з браузера, не передаючи жодних параметрів:
У файлі журналу [logs.txt] сервера містяться такі рядки:
---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}
19.4. Тести [Codeception]
Як і для попередніх версій, ми напишемо тести [Codeception] для версії 09.

19.4.0.1. Тест рівня [métier]
Тест [ClientMetierTest.php] виглядає наступним чином:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// визначення констант
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09");
// шлях до файлу конфігурації
define("CONFIG_FILENAME", ROOT . "/Data/config-client.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";
}
//
// використовує
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;
// клас тестування
class ClientMetierTest extends Unit {
…
}
Коментарі
- порівняно з тестовим класом версії 08, змінюється лише рядок 10, який визначає кореневу папку клієнта, що тестується;
Результати тесту такі:

Цікаво переглянути журнали сервера [logs.txt]:
04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête
…
---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}
Можна помітити, що дані податкової адміністрації завжди беруться з бази даних, а ніколи — із сеансу. Повернемося до коду виконаного тесту:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
…
// тестовий клас
class ClientMetierTest extends Unit {
// бізнес-шар
private $métier;
public function __construct() {
parent::__construct();
// отримання конфігурації
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// створення шару [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// створення шару [métier]
$this->métier = new ClientMetier($clientDao);
}
// тестування
public function test1() {
…
}
public function test2() {
…
}
public function test3() {
…
}
…
}
У класі тесту [Codeception] конструктор виконується для кожного тесту.
- рядок 21: отже, для кожного тесту створюється новий об’єкт [ClientDao] із сесійним файлом cookie NULL. Це пояснює, чому цей клієнт не використовує жодної сесії;
Цей приклад показує, що сесія — не найкраще місце для зберігання даних податкової адміністрації. Адже ці дані є спільними для всіх користувачів додатка. Однак тут вони дублюються в кожній із їхніх сесій.
У веб-програмуванні розрізняють три типи видимості спільних даних:
- дані, спільні для всіх користувачів веб-додатку. Зазвичай це дані, доступні лише для читання. PHP не має вбудованої пам’яті для таких даних;
- дані, спільні для запитів одного й того самого клієнта. Ці дані зберігаються у сесії. У цьому випадку термін «сесія клієнта» використовується для позначення пам’яті клієнта. Усі запити клієнта мають доступ до цієї сесії. Вони можуть зберігати та зчитувати інформацію з неї. У наведених вище скриптах ця сесія реалізована об’єктом Symfony [HttpFoundation\Session\Session];
- пам'ять запиту, або контекст запиту. Запит користувача може оброблятися кількома послідовними діями. Контекст запиту дозволяє дії 1 передавати інформацію дії 2. У наведених вище скриптах запит реалізовано за допомогою об’єкта Symfony [HttpFoundation\Request], а його пам’ять — за допомогою атрибута [HttpFoundation\Request::attributes];

Існують сторонні бібліотеки, які надають PHP пам’ять додатка. У новій версії практичного завдання показано використання однієї з них.