Skip to content

20. Практичне завдання — версія 10

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

20.1. Redis

Пам’ять з ідентифікатором [Application] буде реалізована за допомогою сервера Redis. Скрипти PHP, яким потрібна ця пам’ять додатка, будуть клієнтами цього сервера:

Image

20.2. Встановлення Redis

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

Image

  • у [3] увімкніть сервер [Redis];
  • у [4] залиште порт [6379], який клієнти Redis використовують за замовчуванням;

Служби Laragon автоматично перезапускаються після активації Redis:

Image

20.3. Клієнт Redis у командному режимі

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

Image

  • у [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:

Image

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

Image

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

Image

Файл конфігурації [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. Код клієнта

Image

Версія 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. Давайте просто перевіримо новий випадок помилки:

Image

Результат у консолі такий:


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] на стороні клієнта

Image

Тестовий клас [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]:

Image

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

Image

Тепер переглянемо журнали [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] можна керувати в командному режимі. Ним також можна керувати за допомогою веб-інтерфейсу:

Image

  • у [4] — адміністративний сервер URL;
  • [5] — ключі, збережені на сервері;
  • [6] — поточний стан сервера;

Натиснувши на [5], можна отримати інформацію про ключ [taxAdminData]:

Image

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