20. Практичне завдання — версія 10
У попередній версії було показано, що податкові дані, спільні для всіх користувачів додатка, слід зберігати в пам’яті з областю дії [Application]. Ми використаємо сервер Redis [https://redis.io] для реалізації цього.
20.1. Redis
Пам’ять з ідентифікатором [Application] буде реалізована за допомогою сервера Redis. Скрипти PHP, яким потрібна ця пам’ять додатка, будуть клієнтами цього сервера:

20.2. Встановлення Redis
У Laragon сервер Redis за замовчуванням не активовано. Тому спочатку його потрібно активувати:

- у [3] увімкніть сервер [Redis];
- у [4] залиште порт [6379], який клієнти Redis використовують за замовчуванням;
Служби Laragon автоматично перезапускаються після активації Redis:

20.3. Клієнт Redis у командному режимі
Сервер Redis можна запитувати в командному режимі. Відкрийте термінал Laragon (див. параграф «Посилання»):

- у [1] команда [redis-cli] запускає клієнт у командному режимі сервера Redis;
У липні 2019 року клієнт Redis міг використовувати 172 команди для взаємодії з сервером [https://redis.io/commands#list]. Одна з них — [command count] [2] — відображає це число [3].
Ми розглянемо лише ті команди, які знадобляться нам у нашому додатку PHP. Ми будемо використовувати Redis лише для однієї мети: збереження масиву [‘attribut’=>’valeur’] у пам’яті Redis. Це робиться за допомогою команди Redis [set attribut valeur] [4]. Потім значення можна отримати за допомогою команди [get attribut] [5]. Це все, що нам знадобиться.
Може знадобитися очищення пам’яті Redis. Це робиться за допомогою команди [flushdb] [6]. Далі, якщо запитати значення атрибута [titre] [7], ми отримаємо посилання [nil] [8], яке вказує, що атрибут не знайдено. Також можна використати команду [exists] [9-10], щоб перевірити наявність атрибута.
Щоб вийти з клієнта Redis, введіть команду [quit] [11].
20.4. Встановлення клієнта Redis для PHP
Тепер нам потрібно встановити клієнт Redis для PHP:

Існує кілька бібліотек, що реалізують клієнт Redis. Ми будемо використовувати бібліотеку [Predis] [https://github.com/nrk/predis] (липень 2019 року). Ця бібліотека, як і попередні, встановлюється за допомогою [composer] у терміналі Laragon:

20.5. Код сервера

Файл конфігурації [config-server.json] змінюється наступним чином:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-10",
"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",
"/../version-09/Dao/ServerDaoWithSession.php",
"/../version-08/Métier/InterfaceServerMetier.php",
"/../version-08/Métier/ServerMetier.php",
"/../version-09/Utilities/Logger.php",
"/../version-09/Utilities/SendAdminMail.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": "Data/logs.txt"
}
Коментарі
- рядки 5–15: версія 10 не містить нічого нового, окрім скрипта [impots-server.php]. Вона використовує елементи версій 08 та 09;
- рядок 19: необхідна залежність від бібліотеки [predis], яку щойно було встановлено;
Код сервера [impots-server.php] змінюється наступним чином:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// обробка помилок за допомогою PHP
ini_set("display_errors", "0");
//
// шлях до файлу конфігурації
define("CONFIG_FILENAME", "Data/config-server.json");
// псевдонім класу
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
// сесія
$session = new Session();
$session->start();
…
…
// перший запис у журналі
$logger->write("\n---nouvelle requête\n");
// отримано поточний запит
$request = Request::createFromGlobals();
// аутентифікація тільки під час першого входу
if (!$session->has("user")) {
…
} else {
// журнал
$logger->write("Authentification prise en session…\n");
}
// користувач дійсний — перевіряємо отримані параметри
$erreurs = [];
// має бути три параметри GET
$method = strtolower($request->getMethod());
…
// помилки?
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");
}
// у нас є все необхідне для роботи
// Redis
\Predis\Autoloader::register();
try {
// клієнт [predis]
$redis = new \Predis\Client();
// підключаємося до сервера, щоб перевірити, чи він працює
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// внутрішня помилка сервера
doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
// завершено
exit;
}
// створення шару [dao]
if (!$redis->get("taxAdminData")) {
// фіскальні дані витягуються з бази даних
$logger->write("données fiscales prises en base de données\n");
try {
// побудова шару [dao]
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// податкові дані заносяться в пам'ять області дії [application]
// метод [TaxAdminData]->__toString буде викликано неявно
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// фіксується помилка
doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
// завершено
exit;
}
} else {
// фіскальні дані беруться з пам’яті області дії [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// інстанціювання шару [dao]
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
// журнали
$logger->write("données fiscales prises dans redis\n");
}
// створення шару [métier]
$métier = new ServerMetier($dao);
// розрахунок податку
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// надається відповідь
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// кінець
exit;
function doInternalServerError(string $message, Response $response, array $infos,
Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $message: повідомлення про помилку
// $response: відповідь HTTP
// $infos: таблиця інформації для надсилання електронного листа
// $result: таблиця результатів
// $logger: журнал додатка
// $predisClient: клієнт [predis]
//
// надсилається електронний лист адміністратору
// SendAdminMail перехоплює всі винятки та самостійно записує їх у журнал
$infos['message'] = $message;
$sendAdminMail = new SendAdminMail($infos, $logger);
$sendAdminMail->send();
// відправляється код помилки 500 клієнту
sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}
// функція відправлення відповіді HTTP клієнту
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response: відповідь HTTP
// $result: таблиця результатів
// $statusCode: статус HTTP відповіді
// $headers: заголовки HTTP, які слід включити у відповідь
// $logger: модуль реєстрації додатка
// $predisClient: клієнт [predis]
//
// статус 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();
}
// закриття з'єднання [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
Коментарі
- рядок 15: класу [\Application\ServerDaoWithSession] присвоюється псевдонім [ServerDaoWithRedis], щоб відобразити зміну реалізації серверного скрипта;
- рядки 18–19: сесія зберігається. Тут слід запам’ятати дві речі:
- факт того, що користувач успішно пройшов автентифікацію. Ця інформація має область дії [session]: вона пов’язана з конкретним користувачем і не діє для інших користувачів;
- дані податкової адміністрації. Ця інформація має область дії [application]: вона не пов’язана з конкретним користувачем, але є дійсною для всіх користувачів;
- рядки 54–64: створення клієнта [redis], який буде взаємодіяти з сервером [redis]. Цей клієнт буде взаємодіяти з портом сервера за замовчуванням. Якби сервер не працював на своєму порту за замовчуванням або не знаходився на машині [localhost], цю інформацію потрібно було б передати конструктору класу [\Predis\Client];
- рядок 59: клієнт одразу підключається до сервера, щоб перевірити, чи той відповідає;
- рядки 60–65: якщо з’єднання з сервером Redis не вдається, клієнту надсилається відповідь про помилку, а адміністратору додатка — електронний лист;
- рядок 67: надсилається запит до сервера [redis] щодо ключа [taxAdminData]. Якщо його не знайдено, то податкові дані беруться з бази даних (рядок 72);
- рядок 75: ключ [taxAdminData] розміщується в пам’яті [redis], пов’язаній із рядком jSON змінної [$taxAdminData], яка є об’єктом типу [TaxAdminData]. Метод [$redis→set] очікує на рядок символів як значення ключа. Тому він спробує перетворити об’єкт типу [TaxAdminData] на тип [string]. Тоді неявним чином буде викликано метод [TaxAdminData->__toString]. Він формує рядок jSON для об’єкта [TaxAdminData];
- рядок 84: ключ [taxAdminData] знаходиться в пам’яті [redis], тому отримуємо його значення. Відомо, що це рядок jSON об’єкта [TaxAdminData]. Потім його декодують, щоб отримати масив атрибутів;
- рядок 85: на основі цього масиву створюється новий екземпляр об’єкта [TaxAdminData];
- рядок 87: створюється екземпляр шару [dao];
20.6. Код клієнта

Версія 10 клієнта ідентична версії 9. Змінюється лише файл конфігурації [config-client.json]:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10",
"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",
"/../version-09/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-10/impots-server.php"
}
Змінюється лише рядок 24, URL сервера.
Результати такі самі, як і у версії 09. Давайте просто перевіримо новий випадок помилки:

Результат у консолі такий:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"[redis], Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée. [tcp:\/\/127.0.0.1:6379]"}
Terminé
20.7. Тести [Codeception] на стороні клієнта

Тестовий клас [ClientMetierTest] у версії 10 ідентичний класу у версії 09, за винятком одного:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// визначення констант
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");
…
}
- рядок 10: тестове середовище відповідає клієнту версії 10;
Перш ніж розпочати тестування, видалимо за допомогою клієнта [redis-cli] ключ [taxAdminData] із пам’яті сервера [redis]:

Тепер запустимо тест:

Тепер переглянемо журнали [logs.txt] сервера:
05/07/19 08:52:16:396 :
---nouvelle requête
05/07/19 08:52:16:403 : Autentification en cours…
05/07/19 08:52:16:403 : Authentification réussie [admin, admin]
05/07/19 08:52:16:403 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
05/07/19 08:52:16:407 : données fiscales prises en base de données
05/07/19 08:52:16:420 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:546 :
---nouvelle requête
05/07/19 08:52:16:555 : Autentification en cours…
05/07/19 08:52:16:555 : Authentification réussie [admin, admin]
05/07/19 08:52:16:556 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
05/07/19 08:52:16:559 : données fiscales prises dans redis
05/07/19 08:52:16:559 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
05/07/19 08:52:16:668 :
---nouvelle requête
05/07/19 08:52:16:675 : Autentification en cours…
05/07/19 08:52:16:675 : Authentification réussie [admin, admin]
05/07/19 08:52:16:675 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
05/07/19 08:52:16:678 : données fiscales prises dans redis
05/07/19 08:52:16:678 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:776 :
---nouvelle requête
…
Ми вже зазначали, що під час кожного тесту конструктор тестового класу виконується заново, тому тестований клас [ClientDao] під час кожного тесту інстанціюється з неіснуючим сесійним файлом cookie. Отже, все відбувається так, ніби 11 тестів представляють 11 різних користувачів з 11 різними сесіями.
- рядок 6: податкові дані беруться з бази даних;
- рядки 13, 20: податкові дані беруться з пам’яті [redis]. Отже, тут дійсно маємо пам’ять з областю дії [application], яку спільно використовують усі користувачі додатка;
20.8. Веб-інтерфейс сервера [Redis]
Ми бачили, що сервером [Redis] можна керувати в командному режимі. Ним також можна керувати за допомогою веб-інтерфейсу:

- у [4] — адміністративний сервер URL;
- [5] — ключі, збережені на сервері;
- [6] — поточний стан сервера;
Натиснувши на [5], можна отримати інформацію про ключ [taxAdminData]:

- у [7] — URL, що надає доступ до інформації про ключ [taxAdminData] [8];
- у [9] — статус ключа;
- у [10] — його значення: можна розпізнати рядок jSON об’єкта типу [TaxAdminData];
- у [11] можна видалити ключ;
- у [12] можна додати інший;