18. Практичне завдання — версія 8
Ми повернемося до прикладу програми – версія 5 (параграф «посилання») і перетворимо її на клієнт-серверну програму.
18.1. Introduction
Архітектура версії 5 була такою:

- шар під назвою [dao] (Data Access Objects) відповідає за взаємодію з базою даних MySQL та локальною файловою системою;
- шар під назвою [métier] здійснює розрахунок податку;
- головний скрипт є «диригентом»: він створює екземпляри шарів [dao] та [métier], а потім взаємодіє з шаром [métier] для виконання необхідних операцій;
Ми перейдемо від цієї архітектури до наступної архітектури «клієнт/сервер»:

- У [2] ми перенесемо рівень [dao] з версії 5, видаливши з нього методи доступу до локальної файлової системи. Ці методи будуть перенесені до шару [dao] клієнта [6, 7];
- у [3], шар [métier] залишиться таким самим, як у версії 5, без методів [executeBatchImpôts, saveResults], які переносяться до шару [dao] [7] клієнта;
- у [4] слід написати серверний скрипт, який повинен:
- створити шари [métier], [dao] та [3, 2];
- взаємодіяти зі скриптом клієнта [5, 7];
- у [7] слід написати клієнтський шар [dao]:
- вона буде клієнтом HTTP серверного скрипта [4, 5];
- вона перейме методи доступу до локальної файлової системи з рівня [dao] версії 5;
- у [8] клієнтський рівень [métier] відповідатиме інтерфейсу [InterfaceMetier] версії 5. Однак його реалізація буде іншою. У версії 5 розрахунок податку виконував рівень [métier]. Тут цей розрахунок виконує рівень [métier] сервера. Отже, рівень [métier] звернеться до рівнів [dao] та [7], щоб встановити зв’язок із сервером і попросити його розрахувати податок;
- у [9] консольний скрипт повинен буде створити екземпляр клієнтського шару [dao, métier] та запустити його виконання;
18.2. Сервер
Нас цікавить серверна частина додатка.

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

18.2.1. Об’єкти, що обмінюються між рівнями

Об'єкти, що обмінюються між рівнями, відповідають версії 5, описаній у розділі «Посилання».
18.2.2. Рівень [dao]

Рівень [dao] реалізує наступний інтерфейс [InterfaceServerDao]:
<?php
// простір імен
namespace Application;
interface InterfaceServerDao {
// читання даних податкової служби
public function getTaxAdminData(): TaxAdminData;
}
- рядок 9: метод [getTaxAdminData] отримує дані податкової адміністрації з бази даних;
Інтерфейс [InterfaceServerDao] реалізовано наступним класом [ServerDao]:
<?php
// простір імен
namespace Application;
// визначення класу ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
// об’єкт типу TaxAdminData, що містить дані про податкові шкали
private $taxAdminData;
// об’єкт типу [Database], що містить характеристики BD
private $database;
// конструктор
public function __construct(string $databaseFilename) {
// зберігається конфігурація JSON бази даних
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// готується атрибут
$this->taxAdminData = new TaxAdminData();
try {
// відкриваємо з'єднання з базою даних
$connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// потрібно, щоб при кожній помилці SGBD генерувалося виключення
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// запускаємо транзакцію
$connexion->beginTransaction();
// заповнюємо таблицю податкових шкал
$this->getTranches($connexion);
// заповнюється таблиця констант
$this->getConstantes($connexion);
// транзакція успішно завершується
$connexion->commit();
} catch (\PDOException $ex) {
// чи є транзакція, що виконується?
if (isset($connexion) && $connexion->inTransaction()) {
// завершення транзакції з помилкою
$connexion->rollBack();
}
// видається виняток коду, що викликав
throw new ExceptionImpots($ex->getMessage());
} finally {
// закрито з'єднання
$connexion = NULL;
}
}
// читання даних із бази
private function getTranches($connexion): void {
…
}
// читання таблиці констант
private function getConstantes($connexion): void {
…
}
// повертає дані, необхідні для розрахунку податку
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Цей код було наведено у розділі «Посилання».
18.2.3. Шар [métier]


Шар [métier] реалізує наступний інтерфейс [InterfaceServerMetier]:
<?php
// простір імен
namespace Application;
interface InterfaceServerMetier {
// розрахунок податків платника податків
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
Інтерфейс [InterfaceServerMetier] реалізовано наступним класом [ServerMetier]:
<?php
// простір імен
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// рівень Dao
private $dao;
// дані податкової адміністрації
private $taxAdminData;
//---------------------------------------------
// встановлення шару [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $dao) {
// зберігається посилання на рівень [dao]
$this->dao = $dao;
// отримуються дані, необхідні для розрахунку податку
// метод [getTaxAdminData] може викликати виняток ExceptionImpots
// у цьому випадку виняток передається до коду, що викликає
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// розрахунок податку
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// результат
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
…
// результат
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=річна зарплата-податкова пільга
// податкова пільга має мінімальний та максимальний розмір
private function getRevenuImposable(float $salaire): float {
…
// результат
return floor($revenuImposable);
}
// розраховує можливе зниження
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// результат
return ceil($décôte);
}
// розраховує можливе зниження
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
..
// результат
return ceil($réduction);
}
}
Цей код вже розглядався та коментувався у розділі «Посилання» у версії 1. Його об’єктна версія з базою даних була представлена у розділі «Посилання».
18.2.4. Серверний скрипт


Серверний скрипт реалізує рівень [web] [4]. Скрипт [impots-server] налаштовується за допомогою такого файлу: jSON [config-server.json]:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
"databaseFilename": "Data/database.json",
"taxAdminDataFileName": "Data/taxadmindata.json",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/ExceptionImpots.php",
"/Entities/TaxAdminData.php",
"/Entities/Database.php",
"/Dao/InterfaceServerDao.php",
"/Dao/ServerDao.php",
"/Métier/InterfaceServerMetier.php",
"/Métier/ServerMetier.php"
],
"absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
"users": [
{
"login": "admin",
"passwd": "admin"
}
]
}
- рядок 1: коренева папка, від якої відраховуються шляхи до файлів;
- рядок 2: файл jSON з налаштуваннями бази даних MySQL;
- рядок 3: файл jSON з даними податкової адміністрації;
- рядки 5–14: файли додатка;
- рядок 15: необхідна залежність від сторонніх бібліотек, у даному випадку Symfony;
- рядки 16–20: таблиця користувачів, уповноважених користуватися додатком;
Файли jSON та [database.json, taxadmindata.json] належать до версії 5, описаної у розділі «Посилання».
Скрипт [impots-server] реалізує рівень [web] наступним чином:
<?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";
}
// визначення констант
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// підготовка відповіді JSON від сервера
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// отримання поточного запиту
$request = Request::createFromGlobals();
// аутентифікація
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// чи існує користувач?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// встановлення коду статусу відповіді
if (!$trouvé) {
// не знайдено — код 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// повідомлення про помилку
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// кінець
exit;
}
// користувач дійсний — перевіряємо отримані параметри
$erreurs = [];
// повинно бути три параметри GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// помилка?
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// отримуємо сімейний стан
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// помилка?
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// отримуємо кількість дітей
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// кількість дітей має бути цілим числом >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// помилка?
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// отримано річну заробітну плату
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// зарплата має бути цілим числом >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// помилка?
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// інші параметри у запиті?
foreach (\array_keys($request->query->all()) as $key) {
// параметр дійсний?
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// помилки?
if ($erreurs) {
// клієнту надсилається код помилки 400
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// у нас є все необхідне для роботи
// створення архітектури сервера
$msgErreur = "";
try {
// створення шару [dao]
$dao = new ServerDao($config["databaseFilename"]);
// створення шару [métier]
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// зафіксовано помилку
$msgErreur = utf8_encode($ex->getMessage());
}
// помилка?
if ($msgErreur) {
// клієнту надсилається код помилки 500
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// розрахунок податку
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// повертається відповідь
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
Коментарі
- рядок 16: використовується файл конфігурації;
- рядки 18–26: завантажуються всі залежності;
- рядок 29: ім’я файлу [database.json];
- рядки 32–33: оголошуються класи сторонніх бібліотек, які будуть використовуватися;
- рядки 36–38: готується відповідь jSON;
- рядки 40–52: перевіряється, чи користувач, який надсилає запит, дійсно входить до числа авторизованих користувачів;
- рядки 54–63: якщо це не так, надсилається код HTTP 401, що вказує на відмову в доступі. Після отримання цього коду та заголовка HTTP [WWW-Authenticate => Basic realm=] більшість браузерів відображають вікно автентифікації, яке пропонує користувачеві пройти автентифікацію;
- рядок 59: відповідь сервера jSON пояснює причину помилки. Усі відповіді сервера будуть складатися з рядка jSON з таблиці [‘réponse’=>’qq chose’];
- рядки 64–117: перевіряється правильність запиту:
- запит GET, що містить саме три параметри;
- параметр [marié], значення якого має бути «так» або «ні»;
- параметр [enfants], значення якого має бути цілим числом >=0;
- параметр [salaire], значення якого має бути цілим числом >=0;
- рядок 65: щоразу, коли виявляється помилка, до масиву [$erreurs] додається повідомлення про помилку;
- рядки 120–126: якщо є помилка, то клієнту надсилається код HTTP [400 Bad Request] (рядок 122);
- рядок 123: відповідь сервера jSON пояснює причину помилки;
- починаючи з рядка 132, все перевірено. Можна створити екземпляр шарів [dao, métier]. Створення екземпляра має певну вартість, тому його слід виконувати лише в тому випадку, якщо ви впевнені, що запит є дійсним;
- рядки 130–138: створюється архітектура сервера. Під час побудови шару [dao] може виникнути виняток типу [ExceptionImpots]. Якщо цей виняток виникає, помилку фіксують;
- рядки 135–138: якщо виняток стався, то клієнту надсилається код HTTP 500. Цей код означає, що на сервері сталася помилка;
- рядок 143: у відповіді пояснюється причина помилки;
- рядок 148 : розрахунок податку делегується на рівень [métier];
- рядки 150–151: надсилання відповіді;
Перевіримо цей скрипт у браузері. Запросимо захищений URL [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

- у [1], запитаний захищений URL;
- на [2] — три параметри [marié, enfants, salaire];
- у [3] сервер Apache від Laragon надіслав самопідписаний сертифікат SSL. Браузер це помітив і відображає попередження про безпеку: він вважає, що сайт сервера не є надійним;
- у [4] ми продовжуємо;

- у [6] продовжуємо;

- у [7] браузер відображає вікно, щоб користувач міг пройти аутентифікацію;
- у [9,10] вводимо [admin] та [admin];

- у [13] — відповідь jSON від сервера;
Проведемо кілька тестів на помилки:
Запитуємо URL та [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]
Отримуємо такий результат:

Виключаємо SGBD MySQL і надсилаємо запит на URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

18.2.5. Тестування [Codeception]
Кожного разу, коли ми створюватимемо нову версію сервера, ми тестуватимемо шари [métier] та [dao], як це робилося починаючи з версії 04 (див. абзаци за посиланням та посиланням).
Спочатку ми пов’язуємо проєкт [scripts-web] із тестами [Codeception]. Для цього слід дотримуватися тієї самої процедури, що й для проєкту [scripts-console], описаної в параграфі посилання. Ми отримуємо проект [scripts-web] із папкою [Test Files]:

Ми створимо тест для шару [dao] та тест для шару [métier].
18.2.5.1. Тести шару [dao]

Тест [ServerDaoTest] буде таким:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// визначення констант
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// шлях до файлу конфігурації
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 ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// тестування
public function testTaxAdminData() {
…
}
}
Коментарі
- рядки 9–24: створюється те саме робоче середовище, що й для сервера [impots-server.php]. Це здійснюється в рядках 9–12 шляхом визначення двох констант, від яких залежить це середовище;
- рядки 32–40: створюється екземпляр шару [dao] для тестування, як це було зроблено у серверному скрипті [impots-server.php];
- відтепер ми перебуваємо в тих самих умовах, що й у серверному скрипті [impots-server.php]: можна розпочинати тестування;
- рядки 43–45: метод [testTaxAdminData] є тим, що описано в розділі «посилання»;
Результати тесту такі:

18.2.5.2. Тестування рівня [métier]

Тест [ServerMetierTest] буде таким:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// визначення констант
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// шлях до файлу конфігурації
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 ServerMetierTest extends \Codeception\Test\Unit {
// бізнес-шар
private $métier;
public function __construct() {
parent::__construct();
// отримуємо конфігурацію
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// створення шару [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// створення шару [métier]
$this->métier = new ServerMetier($dao);
}
// тестування
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
Коментарі
- рядки 9–24: створюється те саме робоче середовище, що й для сервера [impots-server.php]. Це здійснюється в рядках 9–12 за допомогою визначення двох констант, від яких залежить середовище;
- рядки 30–38: створюється екземпляр шару [métier] для тестування, як це було зроблено у серверному скрипті [impots-server.php];
- відтепер ми перебуваємо в тих самих умовах, що й у серверному скрипті [impots-server.php]: можна розпочинати тестування;
- рядки 40–53: методи [test1, test2…, test11] відповідають тим, що описані в розділі «Посилання»;
Результати тесту такі:

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

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

18.3.1. Об’єкти, що обмінюються між рівнями

Усі вищезазначені сутності були описані та вже використовувалися:
- [BaseEntity] у розділі «Посилання»;
- [ExceptionImpots] у розділі «Посилання»;
- [TaxPayerData] у параграфі «посилання»;
18.3.2. Шар [dao]

Шар [dao] реалізує наступний інтерфейс [InterfaceClientDao]:
<?php
// простір імен
namespace Application;
interface InterfaceClientDao {
// читання даних платників податків
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// розрахунок податків платника податків
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// запис результатів
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- рядок 9: функція [getTaxPayersData] завантажує в пам’ять дані про платників податків із файлу [$taxPayersFilename]. Якщо трапляються помилки, вони фіксуються у файлі [$errorsFilename];
- рядок 12: функція [calculerImpots] обчислює податок платника податків;
- рядок 15: функція [saveResults] зберігає у файлі [$resultsFilename] дані масиву [$taxPayersData], які відображають результати кількох розрахунків податку;
Інтерфейс [InterfaceClientDao] реалізовано за допомогою наступного класу [ClientDao]:
<?php
namespace Application;
// залежності
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// використання функції «Trait»
use TraitDao;
// атрибути
private $urlServer;
private $user;
// конструктор
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// розрахунок податку
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// створення клієнта HTTP
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// надсилання запиту на сервер
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// отримання відповіді
$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);
}
// повертаємо відповідь
return $réponse;
}
}
Коментарі
- рядок 10: вставляється [TraitDao] (див. параграф «посилання»), який реалізує методи [getTaxPayersData] та [saveResults]. Отже, залишається реалізувати лише метод [calculerImpots]. Він реалізований у рядках 22–49;
- рядки 16–19: конструктор класу [ClientDao] приймає два параметри:
- URL та [$urlServer] сервера розрахунку податків;
- масив [$user] ключів «login» та «passwd», що визначає користувача, який надсилає запит;
- рядок 22: метод [calculerImpots] отримує три параметри, які потрібно надіслати на сервер розрахунку податків;
- рядки 24–27: створюється клієнт HTTP із:
- рядок 25: ідентифікаційні дані користувача, який надсилає запит;
- рядок 26: опція, завдяки якій клієнт HTTP не перевірятиме дійсність сертифіката SSL, надісланого сервером;
- рядки 29–34: до сервера надсилається запит із трьома параметрами, яких він очікує;
- рядок 36: отримується відповідь jSON від сервера. Якщо не передати параметр [false] до методу [Response::getContent], то у разі, якщо статус відповіді сервера знаходиться в діапазоні [3xx-5xx] (випадок помилки), об’єкт [Response] генерує виняток, щойно з’являється спроба отримати вміст відповіді [Response::getContent] або її заголовки HTTP та [Response::getHeaders]. У цьому випадку, незалежно від статусу відповіді HTTP, ми хочемо мати доступ до її вмісту, хоча б для того, щоб записати його в журнал (рядок 40);
- рядки 37–38: відповідь сервера — це рядок jSON з масиву [‘réponse’=>qqChose]. Ми витягуємо [qqChose];
- рядок 40: записуємо відповідь jSON у режимі розробки;
- рядок 42: отримуємо код статусу відповіді;
- рядки 44–49: якщо код статусу HTTP не дорівнює 200, це означає, що на нашому сервері виникла проблема. Тоді генерується виняток типу [ExceptionImpots] із повідомленням, що складається з відповіді jSON від сервера, доповненої кодом HTTP з відповіді;
- рядок 51: повертається результат у вигляді асоціативного масиву з ключами [impôt, surcôte, décôte, réduction, taux];
18.3.3. Рівень [métier]


Рівень [métier] [8] реалізує наступний інтерфейс [InterfaceClientMetier]:
<?php
// простір імен
namespace Application;
interface InterfaceClientMetier {
// розрахунок податків платника податків
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// розрахунок податків у пакетному режимі
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- рядок 9: функція [calculerImpots] обчислює податок;
- рядок 12: функція [executeBatchImpots] обчислює податок для платників податків, дані про яких містяться у файлі [$taxPayersFileName], записує отримані результати у файл [$resultsFileName], а виявлені помилки — у файл [$errorsFileName];
Інтерфейс [InterfaceClientMetier] реалізовано за допомогою наступного класу [ClientMetier]:
<?php
// простір імен
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// атрибут
private $clientDao;
// конструктор
public function __construct(InterfaceClientDao $clientDao) {
// зберігаємо посилання на рівні [dao]
$this->clientDao = $clientDao;
}
// розрахунок податку
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// розрахунок податків у пакетному режимі
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// дозволяється передавати винятки, що походять з рівня [dao]
// отримання даних про платників податків
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// таблиця результатів
$results = [];
// здійснюється їх обробка
foreach ($taxPayersData as $taxPayerData) {
// розраховується податок
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// заповнюємо [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// заносимо результат у таблицю результатів
$results [] = $taxPayerData;
}
// запис результатів
$this->clientDao->saveResults($resultsFileName, $results);
}
}
Коментарі
- рядки 11–14: конструктор класу [ClientMetier] отримує як параметр посилання на шар [dao];
- рядки 17–19: обчислення податку делегується шару [dao];
- рядки 20–38: функція [executeBatchImpots] описана в розділі «Посилання»;
18.3.4. Головний скрипт


Клієнтський скрипт [MainImpotsClient.php] реалізує рівні [console] та [9]. Він налаштовується за допомогою такого файлу: jSON та [conf-client.json]:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": [
"Entities/BaseEntity.php",
"Entities/TaxPayerData.php",
"Entities/ExceptionImpots.php",
"Utilities/Utilitaires.php",
"Dao/InterfaceClientDao.php",
"Dao/TraitDao.php",
"Dao/ClientDao.php",
"Métier/InterfaceClientMetier.php",
"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-08/impots-server.php"
}
- рядок 1: коренева папка клієнта;
- рядок 2: файл jSON з даними платників податків;
- рядок 3: файл jSON з результатами;
- рядок 4: файл jSON з помилками;
- рядки 6–19: різні залежності проекту клієнта;
- рядки 20–23: користувач, який надсилає запити до сервера розрахунку податків;
- рядок 24: захищений файл URL сервера розрахунку податків;
Код скрипта [MainImpotsClient.php] такий:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// обробка помилок за допомогою PHP
//ini_set("display_errors", "0");
//
// шлях до файлу конфігурації
define("CONFIG_FILENAME", "../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";
}
// визначення констант
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"], $config["user"]);
// створення шару [métier]
$clientMetier = new ClientMetier($clientDao);
// розрахунок податків у пакетному режимі
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// виведення повідомлення про помилку
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// кінець
print "Terminé\n";
exit;
Коментарі
- рядок 13: шлях до файлу конфігурації;
- рядок 16: обробка конфігураційного файлу;
- рядки 18–26: завантаження залежностей;
- рядок 37: створення шару [dao]. Конструктору шару передаються дві необхідні йому відомості:
- URL сервера розрахунку податків;
- ідентифікатори користувача, який буде надсилати запити;
- рядок 39: створення шару [métier]. Конструктору шару передається посилання на щойно створений шар [dao];
- рядок 43: шару [métier] задається завдання:
- розрахувати податки для всіх платників податків із файлу $config["taxPayerDataFileName"];
- записати результати у файл $config["resultsFileName"];
- записати помилки у файл $config["errorsFileName"];
- у рядку 43 можуть виникати винятки;
- рядок 46: виведення повідомлення про помилку, пов’язану з винятком;
Виконання клієнта дає ті самі результати, що й попередні версії. Перевірте такі файли:
- [Data/taxpayersdata.json]: дані платників податків, для яких розраховується сума податку;
- [Data/results.json]: результати для різних платників податків із файлу [Data/taxpayersdata.json];
- [Data/errors.json]: помилки, які могли виникнути під час обробки файлу [Data/taxpayersdata.json];
Розглянемо можливі випадки помилок. Спочатку зупинимо сервер Laragon. Результати в консолі клієнта будуть такими:
Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé
Тепер запустимо лише сервер Apache, а не SGBD 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é
Тепер запустимо MySQL, а потім у [config-client] змінимо ім’я користувача, що входить у систему:
Результати в консолі клієнта будуть такими:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
18.3.5. Тестування [Codeception]
Як і для попередніх версій, ми напишемо тести [Codeception] для версії 08.

18.3.5.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-08");
// шлях до файлу конфігурації
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";
}
//
// клас тестування
class ClientMetierTest extends \Codeception\Test\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 test11() {
…
}
}
Коментарі
- рядки 10–26: визначення середовища тестування. Ми використовуємо те саме середовище, що й у головному скрипті [MainImpotsClient], описаному в розділі «Посилання»;
- рядки 33–41: побудова шарів [dao] та [métier];
- рядок 40: атрибут [$this→métier] посилається на шар [métier];
- рядки 44–51: методи [test1, test2…, test11] — це ті, що описані в розділі «посилання»;
Результати тесту такі:
