Skip to content

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

Ми повернемося до прикладу програми – версія 5 (параграф «посилання») і перетворимо її на клієнт-серверну програму.

18.1. Introduction

Архітектура версії 5 була такою:

Image

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

Ми перейдемо від цієї архітектури до наступної архітектури «клієнт/сервер»:

Image

  • У [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. Сервер

Нас цікавить серверна частина додатка.

Image

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

Image

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

Image

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

18.2.2. Рівень [dao]

Image

Рівень [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]

Image

Image

Шар [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. Серверний скрипт

Image

Image

Серверний скрипт реалізує рівень [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]:

Image

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

Image

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

Image

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

Image

  • у [13] — відповідь jSON від сервера;

Проведемо кілька тестів на помилки:

Запитуємо URL та [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]

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

Image

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

Image

18.2.5. Тестування [Codeception]

Кожного разу, коли ми створюватимемо нову версію сервера, ми тестуватимемо шари [métier] та [dao], як це робилося починаючи з версії 04 (див. абзаци за посиланням та посиланням).

Спочатку ми пов’язуємо проєкт [scripts-web] із тестами [Codeception]. Для цього слід дотримуватися тієї самої процедури, що й для проєкту [scripts-console], описаної в параграфі посилання. Ми отримуємо проект [scripts-web] із папкою [Test Files]:

Image

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

18.2.5.1. Тести шару [dao]

Image

Тест [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] є тим, що описано в розділі «посилання»;

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

Image

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

Image

Тест [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] відповідають тим, що описані в розділі «Посилання»;

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

Image

18.3. Клієнт

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

Image

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

Image

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

Image

Усі вищезазначені сутності були описані та вже використовувалися:

18.3.2. Шар [dao]

Image

Шар [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]

Image

Image

Рівень [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. Головний скрипт

Image

Image

Клієнтський скрипт [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:

Image

Результати у консолі клієнта будуть такими:


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] змінимо ім’я користувача, що входить у систему:

1
2
3
4
    "user": {
        "login": "x",
        "passwd": "x"
},

Результати в консолі клієнта будуть такими:


L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé

18.3.5. Тестування [Codeception]

Як і для попередніх версій, ми напишемо тести [Codeception] для версії 08.

Image

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] — це ті, що описані в розділі «посилання»;

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

Image