Skip to content

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

У цій версії ми вдосконалимо сервер таким чином:

  • наразі при кожному запиті дані податкової адміністрації шукаються в базі даних. Ми будемо використовувати сесію:
    • під час першого запиту користувача дані податкової служби шукаються в базі даних і зберігаються в сесії;
    • під час наступних запитів того самого користувача дані податкової служби шукаються в сесії. Можна сподіватися на незначне скорочення часу виконання, оскільки запити до бази даних є ресурсоємними;
  • сервер буде записувати в текстовий файл такі важливі моменти:
    • успішну або невдалу автентифікацію;
    • дійсність або недійсність параметрів, надісланих клієнтом;
    • результат розрахунку податку;
    • різні випадки помилок;
  • у разі фатальної помилки адміністратору додатка буде надіслано електронний лист;

Клієнт також повинен бути модифікований для обробки сесійного файлу cookie, який ми йому надішлемо.

19.1. Сервер

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

Image

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

Image

19.1.1. Утиліти

Image

19.1.1.1. Клас [Logger]

Клас [Logger] використовуватиметься для запису журналів у текстовий файл:


<?php

namespace Application;

class Logger {
  // атрибут
  private $resource;

  // конструктор
  public function __construct(string $logsFilename) {
    // відкриття файлу
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  // запис повідомлення в журнали
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }

  // закриття файлу журналів
  public function close() {
    fclose($this->resource);
  }

}

Коментарі

  • рядок 7: ресурс файлу журналів;
  • рядок 10: конструктор класу отримує як параметр ім’я файлу журналу;
  • рядок 12: текстовий файл відкривається в режимі додавання (a+): файл буде відкрито, його вміст збережено. Записи будуть додаватися після поточного вмісту;
  • рядки 13–15: якщо відкрити файл не вдалося, генерується виняток;
  • рядки 19–21: метод [write] дозволяє записати повідомлення [$message] у файл журналу, додавши перед ним дату та час;
  • рядки 24–16: метод [close] дозволяє закрити файл журналу;

Примітка: серверний додаток може обслуговувати декількох клієнтів одночасно. При цьому для всіх них використовується єдиний файл журналу. Отже, існує ризик паралельного доступу для запису в цей файл. Тому необхідно синхронізувати записи, щоб уникнути їхнього змішування. Для цього PHP використовує семафори [https://www.php.net/manual/fr/book.sem.php]. У цьому прикладі ми не будемо розглядати синхронізацію записів, але слід пам’ятати про цю проблему.

19.1.1.2. Клас [SendAdminMail]

Клас [SendAdminMail] дозволяє надіслати електронний лист адміністратору додатка у разі його збою:


<?php

namespace Application;

class SendAdminMail {
  // атрибути
  private $config;
  private $logger;

  // конструктор
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }

  public function send() {
    // надсилає $this->config['message'] на SMTP-сервер $this->config['smtp-server'] на порт $infos[smt-port]
    // якщо $this->config['tls'] має значення «true», буде використано підтримку TLS
    // лист надсилається від імені $this->config['from']
    // для одержувача $this->config['to']
    // тема повідомлення: $this->config['subject']
    // до листа додаються вкладення з $this->config['attachments']
    // результат застосування методу
    try {
      // створення повідомлення
      $message = (new \Swift_Message())
        // тема повідомлення
        ->setSubject($this->config["subject"])
        // відправник
        ->setFrom($this->config["from"])
        // адресати з використанням словника (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // текст повідомлення
        ->setBody($this->config["message"])
      ;
      // вкладення
      foreach ($this->config["attachments"] as $attachment) {
        // шлях до вкладення
        $fileName = __DIR__ . $attachment;
        // перевіряється наявність файлу
        if (file_exists($fileName)) {
          // додаємо документ до повідомлення
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // помилка
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // протокол TLS?
      if ($this->config["tls"] === "TRUE") {
        // TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
          ->setUsername($this->config["user"])
          ->setPassword($this->config["password"]);
      } else {
        // немає TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // менеджер відправлення
      $mailer = new \Swift_Mailer($transport);
      // відправлення повідомлення
      $mailer->send($message);
      // кінець
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // помилка
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }

}

Коментарі

  • рядок 11: конструктор отримує два параметри:
    • [$config]: асоціативний масив, що містить усю необхідну інформацію для надсилання електронного листа;
    • [$logger]: логер, що дозволяє реєструвати важливі моменти під час надсилання електронного листа;

Асоціативний масив матиме такий вигляд:

1
2
3
4
5
6
7
8
9
{
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
}
  • рядки 16–76: метод [send] дозволяє надіслати електронний лист. Цей код було представлено та описано у розділі за посиланням;

19.1.2. Рівень [dao]

Image

Скрипт [ServeurDaoWithSession.php] має такий вигляд:


<?php

// простір імен
namespace Application;

// визначення класу ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {

  // конструктор
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // найпростіший випадок
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // передача управління батьківському класу
      parent::__construct($databaseFilename);
    }
  }

}

Коментарі

  • рядок 7: клас [ServerDaoWithSession] версії 09 розширює клас [ServerDao] версії 08. Адже клас [ServerDao] вміє працювати з базою даних. Залишається лише передбачити випадок, коли дані податкової адміністрації вже отримано:
  • рядок 10: конструктор тепер отримує два параметри:
    • [string $databaseFilename]: ім’я файлу, що містить інформацію для підключення до бази даних, якщо дані податкової служби ще не отримано, NULL — в іншому випадку;
    • [TaxAdminData $taxAdminData]: дані податкової служби, якщо вони вже отримані; NULL — в іншому випадку;

Під час запуску веб-сесії шар [dao] буде побудовано з об’єктом [$databaseFilename], що не є NULL, та об’єктом [taxAdminData] NULL. Дані податкової служби будуть потім витягнуті з бази даних і збережені в сесії. Під час наступних запитів у рамках тієї самої сесії шар [dao] буде побудовано з об’єктом [databaseFilename], NULL та об’єктом [taxAdminData], отриманими з сесії, а не з NULL. Отже, пошук у базі даних не відбуватиметься.

19.1.3. Серверний скрипт

Серверний скрипт [impots-server.php] налаштовується за допомогою такого файлу: jSON [config-server.json]:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
    "databaseFilename": "Data/database.json",
    "relativeDependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Entities/TaxAdminData.php",
        "/../version-08/Entities/Database.php",
        "/../version-08/Dao/InterfaceServerDao.php",
        "/../version-08/Dao/ServerDao.php",
        "/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Data/logs.txt"
}

Серверний скрипт [impots-server.php] змінюється наступним чином:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// обробка помилок за допомогою PHP
ini_set("display_errors", "0");
//
// шлях до файлу конфігурації
define("CONFIG_FILENAME", "Data/config-server.json");

// отримання конфігурації
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// включення необхідних для скрипта залежностей
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// абсолютні залежності (сторонні бібліотеки)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

// сесія
$session = new Session();
$session->start();

// підготовка відповіді JSON від сервера
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// створення файлу журналу
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // внутрішня помилка сервера
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // завершено
  exit;
}

// перший запис у журналі
$logger->write("\n---nouvelle requête\n");

// отримано поточний запит
$request = Request::createFromGlobals();
// аутентифікація лише під час першого входу
if (!$session->has("user")) {
  // журнал
  $logger->write("Autentification en cours…\n");
  // аутентифікація

  }
  // чи знайдено користувача?
  if (!$trouvé) {
    // не знайдено — код 401 HTTP_UNAUTHORIZED
    sendResponse(
      $response,
      ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
      Response::HTTP_UNAUTHORIZED,
      ["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
      $logger
    );
    // завершено
    exit;
  } else {
    // у сесії зазначається, що користувач пройшов автентифікацію
    $session->set("user", TRUE);
    // журнал
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // журнал
  $logger->write("Authentification prise en session…\n");
}
// користувач дійсний — перевіряються отримані параметри
$erreurs = [];
// повинно бути три параметри GET


// помилки?
if ($erreurs) {
// клієнту надсилається код помилки 400 HTTP_BAD_REQUEST
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // завершено
  exit;
} else {
  // журнали
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// у нас є все необхідне для роботи
// створення шару [dao]
if (!$session->has("taxAdminData")) {
  // дані беруться з бази даних
  $logger->write("données fiscales prises en base de données\n");
  try {
    // побудова шару [dao]
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    // дані заносяться в сесію
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // фіксується помилка
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // завершено
    exit;
  }
} else {
  // дані отримано з сеансу
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // журнали
  $logger->write("données fiscales prises en session\n");
}
// створення шару [métier]
$métier = new ServerMetier($dao);
// розрахунок податку
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// надається відповідь
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// кінець
exit;

function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // надсилання електронного листа адміністратору
  // SendAdminMail перехоплює всі винятки та самостійно записує їх у журнал
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // відправляється код помилки 500 клієнту
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}

// функція відправлення відповіді HTTP клієнту
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $response: відповідь HTTP
  // $result: таблиця результатів
  // $statusCode: статус HTTP відповіді
  // $headers: заголовки HTTP, які слід включити у відповідь
  // $logger: модуль реєстрації додатка
  //
  // статус HTTTP
  $response->setStatusCode($statusCode);
  // тіло повідомлення
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // заголовки
  $response->headers->add($headers);
  // відправлення
  $response->send();
  // журнал
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
}

Коментарі

  • рядки 34–35: запускається сесія;
  • рядки 38–40: готується відповідь jSON;
  • рядки 42–50: здійснюється спроба створення файлу журналу. У разі виникнення винятку викликається метод [doInternalServer] (рядки 132–140);
  • рядок 132: метод [doInternalServer] приймає чотири параметри:
    • [$message]: повідомлення, яке потрібно занести до журналу. Повинно бути закодовано у форматі UTF-8;
    • [$response]: об’єкт [Response], який інкапсулює відповідь сервера своєму клієнту;
    • [$logger]: об’єкт [Logger], що дозволяє створювати журнали;
    • [$infos]: інформація, що дозволяє надіслати електронний лист адміністратору додатка;
  • рядки 135–137: надсилання електронного листа адміністратору додатка;
  • рядок 139: надсилання відповіді клієнту:
    • $response: відповідь HTTP;
    • $result: сервер надсилає рядок jSON із масиву [‘réponse’=>["erreur" => $message]];
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], код 500;
    • $headers: [], заголовки HTTP, які слід додати до відповіді, відсутні;
    • $logger: модуль реєстрації додатка;
  • рядок 58: завдяки створеній сесії автентифікація клієнта відбуватиметься лише один раз:
    • після автентифікації клієнта в сесію буде додано ключ [user] (рядок 78);
    • під час наступного запиту від того самого клієнта рядок 58 дозволяє уникнути аутентифікації, яка стала непотрібною;
  • рядок 103: завдяки створеній сесії пошук даних у базі даних відбуватиметься лише один раз:
    • під час першого запиту буде виконано пошук у базі даних (рядок 108). Отримані дані потім додаються до сесії (рядок 110) у поєднанні з ключем [taxAdminData];
    • під час наступних запитів ключ [taxAdminData] буде знайдено в сесії (рядок 103), і тоді дані з диска будуть безпосередньо передані на рівень [dao] (рядок 119);
  • рядки 111–116: пошук податкових даних у базі може завершитися невдачею. У цьому випадку клієнту надсилається код [500 Internal Server Error];
  • рядок 113: повідомлення про помилку винятку драйвера MySQL кодується як ISO 8859-1. Його перетворюють на UTF-8 для правильного запису в журнал;
  • решта коду майже ідентична коду попередньої версії;
  • рядки 143–164: функція [sendResponse] надсилає всі відповіді клієнту;
  • рядки 144–148: значення параметрів;
  • рядок 153: відповідь завжди є рядком jSON з масиву [‘résultat’=>qqChose];
  • рядок 156: іноді до відповіді потрібно додати заголовки HTTP. Це відбувається в рядку 71;
  • рядок 158: відповідь надіслано;
  • рядки 160–163: відповідь записується в журнал, а журнал закривається;

19.1.4. Тести [Codeception]

Image

Ми будемо тестувати лише рівень [dao], який є єдиним, що зазнав змін.

Код тесту [ServerDaoTest] такий:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// визначення констант
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// шлях до файлу конфігурації
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");

// отримання конфігурації
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// включення необхідних для скрипта залежностей
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// абсолютні залежності (сторонні бібліотеки)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// тест -----------------------------------------------------

class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    // батьківський
    parent::__construct();
    // отримання конфігурації
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // створення шару [dao]
    $dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // тестування
  public function testTaxAdminData() {

  }

}
  • рядки 9–24: створюється середовище виконання, ідентичне до того, що використовується у серверному скрипті [impots-server];
  • рядок 38: для побудови шару [dao] створюється екземпляр класу [ServerDaoWithSession];

Результат тестування такий:

Image

19.2. Клієнт

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

Image

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

Image

У новій версії змінюються лише:

  • файл конфігурації [config-client.json];
  • рівень [dao] клієнта;

19.2.1. шар [dao]

Рівень [Dao] змінюється наступним чином:


<?php

namespace Application;

// залежності
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // використання Trait
  use TraitDao;
  // атрибути
  private $urlServer;
  private $user;
  private $sessionCookie;

  // конструктор
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }

  // розрахунок податку
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // сесійний файл cookie?
    if (!$this->sessionCookie) {
      // створення клієнта HTTP
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // надсилається запит на сервер без сесійного файлу cookie
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // надсилається запит на сервер із сесійним файлом cookie
      // створюється клієнт HTTP
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // отримується відповідь
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // журнали
    print "$json=json\n";
    // отримуємо статус відповіді
    $statusCode = $response->getStatusCode();
    // помилка?
    if ($statusCode !== 200) {
      // виникла помилка — генерується виняток
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    if (!$this->sessionCookie) {
      // отримуємо сесійний файл cookie
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // файл cookie сеансу?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // повертаємо відповідь
    return $réponse;
  }

}

Коментарі

Зміна шару [dao] полягає в тому, що тепер здійснюється управління сеансом:

  • рядок 14: сесійний файл cookie;
  • рядки 25–39: під час першого запиту цей файл cookie відсутній: тоді надсилається запит до сервера з даними автентифікації (рядок 28);
  • рядки 40–53: під час наступних запитів, як правило, є файл cookie сеансу. Тоді дані для автентифікації не надсилаються (рядки 42–44);
  • рядки 69–82: відповідь сервера на перший запит міститиме сесійний файл cookie. Його отримуємо. Цей код уже використовувався та пояснювався у розділі за посиланням;
  • рядок 78: отриманий сесійний файл cookie зберігається в атрибуті класу [$sessionCookie];

Примітка: можна було б залишити стару версію шару [dao] і здійснювати автентифікацію при кожному запиті, оскільки її вартість є незначною. З навчальною метою ми вирішили нагадати, як клієнт HTTP може керувати сесією.

19.2.2. Файл конфігурації

Файл конфігурації jSON змінюється наступним чином:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/TaxPayerData.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Utilities/Utilitaires.php",
        "/../version-08/Dao/InterfaceClientDao.php",
        "/../version-08/Dao/TraitDao.php",
        "/Dao/ClientDao.php",
        "/../version-08/Métier/InterfaceClientMetier.php",
        "/../version-08/Métier/ClientMetier.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php"
    ],
    "user": {
        "login": "admin",
        "passwd": "admin"
    },
    "urlServer": "https://localhost:443/php7/scripts-web/impots/version-09/impots-server.php"
}

Змінюється лише URL у рядку 24.

19.3. Кілька тестів

19.3.1. Тест 1

Спочатку запускаємо клієнт у середовищі без помилок. Результати залишаються такими ж, як і в попередніх версіях. Але тепер на стороні сервера з’являється файл журналу [logs.txt]:


04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête

  • рядки 3–7: під час першого запиту відбувається автентифікація та пошук даних у базі;
  • рядки 9–14: під час наступного запиту автентифікація більше не відбувається, а дані беруться із сесії. Це повторюється під час наступних запитів (рядки 15 і далі);

19.3.2. Тест 2

Тепер відключимо базу даних MySQL. На стороні клієнта ми отримуємо такий результат у консолі:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}
Terminé

На стороні сервера ми бачимо такі журнали [logs.txt]:


04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête

Щоб отримати електронний лист, який отримав адміністратор додатка, використовуємо скрипт [imap-03.php] з розділу, пов’язаного з таким файлом конфігурації [config-imap-01.json]:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

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

Image

Файл [message_1.txt] містить такий текст:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.

19.3.3. Тест 3

Тепер зробимо так, щоб файл [logs.txt] не міг бути створений. Для цього достатньо створити папку [logs.txt]:

Image

Зробивши це, запустимо клієнт.

На стороні клієнта ми бачимо такі результати у консолі:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé

На стороні сервера журналів немає, але адміністратор отримує такий лист:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

Echec lors de la création du fichier de logs [Data/logs.txt]

19.3.4. Тест 4

Цього разу в конфігураційному файлі клієнта вкажемо неправильні облікові дані для клієнта, який підключається.

Клієнт виводить на консоль такі результати:


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

На стороні сервера з’являються такі журнали:


---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}

19.3.5. Тест 5

Повернемо правильний ідентифікатор користувача [admin, admin] у файл конфігурації клієнта.

Тепер звернемося до сервера безпосередньо з браузера, не передаючи жодних параметрів:

У файлі журналу [logs.txt] сервера містяться такі рядки:


---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}

19.4. Тести [Codeception]

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

Image

19.4.0.1. Тест рівня [métier]

Тест [ClientMetierTest.php] виглядає наступним чином:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;

// визначення констант
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09");

// шлях до файлу конфігурації
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");

// отримання конфігурації
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// включення необхідних для скрипта залежностей
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// абсолютні залежності (сторонні бібліотеки)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// використовує
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;

// клас тестування
class ClientMetierTest extends Unit {
  
}

Коментарі

  • порівняно з тестовим класом версії 08, змінюється лише рядок 10, який визначає кореневу папку клієнта, що тестується;

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

Image

Цікаво переглянути журнали сервера [logs.txt]:


04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête

---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}

Можна помітити, що дані податкової адміністрації завжди беруться з бази даних, а ніколи — із сеансу. Повернемося до коду виконаного тесту:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Application;



// тестовий клас
class ClientMetierTest extends Unit {
  // бізнес-шар
  private $métier;

  public function __construct() {
    parent::__construct();
    // отримання конфігурації
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // створення шару [dao]
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // створення шару [métier]
    $this->métier = new ClientMetier($clientDao);
  }

  // тестування
  public function test1() {

  }

  public function test2() {

  }

  public function test3() {

  }



}

У класі тесту [Codeception] конструктор виконується для кожного тесту.

  • рядок 21: отже, для кожного тесту створюється новий об’єкт [ClientDao] із сесійним файлом cookie NULL. Це пояснює, чому цей клієнт не використовує жодної сесії;

Цей приклад показує, що сесія — не найкраще місце для зберігання даних податкової адміністрації. Адже ці дані є спільними для всіх користувачів додатка. Однак тут вони дублюються в кожній із їхніх сесій.

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

  • дані, спільні для всіх користувачів веб-додатку. Зазвичай це дані, доступні лише для читання. PHP не має вбудованої пам’яті для таких даних;
  • дані, спільні для запитів одного й того самого клієнта. Ці дані зберігаються у сесії. У цьому випадку термін «сесія клієнта» використовується для позначення пам’яті клієнта. Усі запити клієнта мають доступ до цієї сесії. Вони можуть зберігати та зчитувати інформацію з неї. У наведених вище скриптах ця сесія реалізована об’єктом Symfony [HttpFoundation\Session\Session];
  • пам'ять запиту, або контекст запиту. Запит користувача може оброблятися кількома послідовними діями. Контекст запиту дозволяє дії 1 передавати інформацію дії 2. У наведених вище скриптах запит реалізовано за допомогою об’єкта Symfony [HttpFoundation\Request], а його пам’ять — за допомогою атрибута [HttpFoundation\Request::attributes];

Image

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