19. Ćwiczenie praktyczne – wersja 9
W tej wersji ulepszymy serwer w następujący sposób:
- obecnie przy każdym żądaniu dane urzędu skarbowego są wyszukiwane w bazie danych. Zastosujemy sesję:
- przy pierwszym żądaniu użytkownika dane z urzędu skarbowego są wyszukiwane w bazie danych i zapisywane w sesji;
- przy kolejnych żądaniach tego samego użytkownika dane z urzędu skarbowego są pobierane z sesji. Można oczekiwać niewielkiego skrócenia czasu wykonania, ponieważ zapytania do bazy danych są kosztowne;
- serwer będzie rejestrował w pliku tekstowym ważne momenty:
- pomyślne lub nieudane uwierzytelnienie;
- poprawność lub niepoprawność parametrów przesłanych przez klienta;
- wynik obliczenia podatku;
- różne przypadki błędów;
- w przypadku błędu krytycznego do administratora aplikacji zostanie wysłana wiadomość e-mail;
Klient również będzie musiał zostać zmodyfikowany, aby obsługiwał plik cookie sesji, który zostanie mu wysłany.
19.1. Serwer
Zajmujemy się częścią serwerową aplikacji.

Architektura ta zostanie zaimplementowana za pomocą następujących skryptów:

19.1.1. Narzędzia

19.1.1.1. Klasa [Logger]
Klasa [Logger] będzie wykorzystywana do zapisywania logów w pliku tekstowym:
<?php
namespace Application;
class Logger {
// atrybut
private $resource;
// konstruktor
public function __construct(string $logsFilename) {
// otwarcie pliku
$this->resource = fopen($logsFilename, "a");
if (!$this->resource) {
throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
}
}
// zapisanie komunikatu w logach
public function write(string $message) {
fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
}
// zamknięcie pliku dziennika
public function close() {
fclose($this->resource);
}
}
Komentarze
- wiersz 7: zasób pliku logów;
- wiersz 10: konstruktor klasy otrzymuje jako parametr nazwę pliku dziennika;
- wiersz 12: otwieramy plik tekstowy w trybie dodawania (a+): plik zostanie otwarty, a jego zawartość zachowana. Zapisy będą dokonywane na końcu obecnej zawartości;
- wiersze 13–15: jeśli nie udało się otworzyć pliku, zgłaszany jest wyjątek;
- wiersze 19–21: metoda [write] umożliwia zapisanie komunikatu [$message] w pliku dziennika, poprzedzonego datą i godziną;
- wiersze 24–16: metoda [close] umożliwia zamknięcie pliku dziennika;
Uwaga: aplikacja serwerowa może obsługiwać wielu klientów jednocześnie. Mamy jednak tylko jeden plik logów dla wszystkich. Istnieje zatem ryzyko równoczesnego dostępu do zapisu w tym pliku. Należy więc zsynchronizować zapisy, aby zapobiec ich pomieszaniu. W tym celu klasa PHP korzysta z semaforów klasy [https://www.php.net/manual/fr/book.sem.php]. W tym miejscu pominęliśmy kwestię synchronizacji zapisów, jednak należy mieć świadomość istnienia tego problemu.
19.1.1.2. Klasa [SendAdminMail]
Klasa [SendAdminMail] umożliwia wysłanie wiadomości e-mail do administratora aplikacji w przypadku jej awarii:
<?php
namespace Application;
class SendAdminMail {
// atrybuty
private $config;
private $logger;
// konstruktor
public function __construct(array $config, Logger $logger = NULL) {
$this->config = $config;
$this->logger = $logger;
}
public function send() {
// wysyła $this->config['message'] na serwer SMTP $this->config['smtp-server'] na porcie $infos[smt-port]
// jeśli warunek $this->config['tls'] jest spełniony, zostanie użyty mechanizm TLS
// wiadomość e-mail jest wysyłana w imieniu $this->config['from']
// dla odbiorcy $this->config['to']
// wiadomość ma temat $this->config['subject']
// do wiadomości dołączono załączniki o nazwach $this->config['attachments']
// wynik działania metody
try {
// utworzenie wiadomości
$message = (new \Swift_Message())
// temat wiadomości
->setSubject($this->config["subject"])
// nadawca
->setFrom($this->config["from"])
// odbiorcy z wykorzystaniem słownika (setTo/setCc/setBcc)
->setTo($this->config["to"])
// treść wiadomości
->setBody($this->config["message"])
;
// załączniki
foreach ($this->config["attachments"] as $attachment) {
// ścieżka do załącznika
$fileName = __DIR__ . $attachment;
// sprawdzamy, czy plik istnieje
if (file_exists($fileName)) {
// dołączamy dokument do wiadomości
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
if ($this->logger !== NULL) {
// błąd
$this->logger->write("L'attachement [$fileName] n'existe pas\n");
}
}
}
// protokół 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 {
// brak TLS
$transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
}
// menedżer wysyłki
$mailer = new \Swift_Mailer($transport);
// wysłanie wiadomości
$mailer->send($message);
// koniec
if ($this->logger !== NULL) {
$this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
}
} catch (\Throwable $ex) {
// błąd
if ($this->logger !== NULL) {
$this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
}
}
}
}
Komentarze
- wiersz 11: konstruktor otrzymuje dwa parametry:
- [$config]: tablica asocjacyjna zawierająca wszystkie informacje niezbędne do wysłania wiadomości e-mail;
- [$logger]: moduł rejestrujący, umożliwiający rejestrowanie kluczowych momentów procesu wysyłania wiadomości e-mail;
Tablica asocjacyjna będzie miała następującą postać:
- wiersze 16–76: metoda [send] umożliwia wysłanie wiadomości e-mail. Kod ten został przedstawiony i opisany w akapicie [link];
19.1.2. Warstwa [dao]

Skrypt [ServeurDaoWithSession.php] wygląda następująco:
<?php
// przestrzeń nazw
namespace Application;
// definicja klasy ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {
// konstruktor
public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
// najprostszy przypadek
if ($taxAdminData !== NULL) {
$this->taxAdminData = $taxAdminData;
} else {
// przekazujemy kontrolę klasie nadrzędnej
parent::__construct($databaseFilename);
}
}
}
Komentarze
- wiersz 7: klasa [ServerDaoWithSession] z wersji 09 rozszerza klasę [ServerDao] z wersji 08. Klasa [ServerDao] potrafi bowiem korzystać z bazy danych. Pozostaje nam tylko uwzględnić przypadek, w którym dane z urzędu skarbowego zostały już pobrane:
- wiersz 10: funkcja otrzymuje teraz dwa parametry:
- [string $databaseFilename]: nazwa pliku zawierającego informacje umożliwiające połączenie z bazą danych, jeśli dane z urzędu skarbowego nie zostały jeszcze pobrane, w przeciwnym razie NULL;
- [TaxAdminData $taxAdminData]: dane z urzędu skarbowego, jeśli zostały już pobrane, w przeciwnym razie NULL;
Podczas uruchamiania sesji internetowej warstwa [dao] zostanie utworzona z obiektem [$databaseFilename] (nie NULL) oraz obiektem [taxAdminData] (NULL). Dane urzędu skarbowego zostaną wówczas wyszukane w bazie i zapisane w sesji. Podczas kolejnych zapytań w ramach tej samej sesji warstwa [dao] zostanie utworzona z obiektem [databaseFilename], NULL oraz obiektem [taxAdminData] pochodzącym z sesji, a nie z obiektem NULL. Nie nastąpi zatem żadne wyszukiwanie w bazie danych.
19.1.3. Skrypt serwera
Skrypt serwera [impots-server.php] jest skonfigurowany przez następujący plik: 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"
}
Skrypt serwera [impots-server.php] zmienia się w następujący sposób:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw
namespace Application;
// obsługa błędów przez PHP
ini_set("display_errors", "0");
//
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", "Data/config-server.json");
// pobieranie konfiguracji
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// dołączamy zależności niezbędne dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// sesja
$session = new Session();
$session->start();
// przygotowanie odpowiedzi serwera JSON
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// tworzenie pliku logów
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// wewnętrzny błąd serwera
doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
// zakończono
exit;
}
// pierwszy wpis w dzienniku
$logger->write("\n---nouvelle requête\n");
// pobieranie bieżącego żądania
$request = Request::createFromGlobals();
// uwierzytelnianie tylko przy pierwszym uruchomieniu
if (!$session->has("user")) {
// log
$logger->write("Autentification en cours…\n");
// uwierzytelnianie
…
}
// czy znaleziono użytkownika?
if (!$trouvé) {
// nie znaleziono – kod 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
);
// zakończono
exit;
} else {
// w sesji odnotowano uwierzytelnienie użytkownika
$session->set("user", TRUE);
// log
$logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
}
} else {
// log
$logger->write("Authentification prise en session…\n");
}
// mamy prawidłowego użytkownika – sprawdzamy otrzymane parametry
$erreurs = [];
// powinny być trzy parametry GET
…
// błędy?
if ($erreurs) {
// wysyłamy do klienta kod błędu 400 HTTP_BAD_REQUEST
sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
// zakończono
exit;
} else {
// logi
$logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// mamy wszystko, co potrzebne do pracy
// tworzenie warstwy [dao]
if (!$session->has("taxAdminData")) {
// dane są pobierane z bazy danych
$logger->write("données fiscales prises en base de données\n");
try {
// tworzenie warstwy [dao]
$dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
// dane są umieszczane w sesji
$session->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// odnotowuje się błąd
doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
// zakończono
exit;
}
} else {
// dane zostały pobrane z sesji
$dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
// logi
$logger->write("données fiscales prises en session\n");
}
// tworzenie warstwy [métier]
$métier = new ServerMetier($dao);
// obliczenie podatku
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// przekazanie odpowiedzi
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// koniec
exit;
function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
// wysyłanie wiadomości e-mail do administratora
// SendAdminMail przechwytuje wszystkie wyjątki i samodzielnie je rejestruje
$infos['message'] = $message;
$sendAdminMail = new SendAdminMail($infos, $logger);
$sendAdminMail->send();
// wysyłamy kod błędu 500 do klienta
sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}
// funkcja wysyłania odpowiedzi HTTP do klienta
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
// $response: odpowiedź HTTP
// $result: tabela wyników
// $statusCode: status odpowiedzi HTTP
// $headers: nagłówki HTTP do umieszczenia w odpowiedzi
// $logger: moduł rejestrujący aplikacji
//
// status HTTTP
$response->setStatusCode($statusCode);
// treść
$body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$response->setContent($body);
// nagłówki
$response->headers->add($headers);
// wysyłanie
$response->send();
// log
if ($logger != NULL) {
$logger->write("$body\n");
$logger->close();
}
}
Komentarze
- wiersze 34–35: rozpoczyna się sesja;
- wiersze 38–40: przygotowuje się odpowiedź jSON;
- wiersze 42–50: próba utworzenia pliku logów. W przypadku wystąpienia wyjątku wywoływana jest metoda [doInternalServer] (wiersze 132–140);
- wiersz 132: metoda [doInternalServer] przyjmuje cztery parametry:
- [$message]: komunikat do zarejestrowania w dzienniku. Musi być zakodowany w formacie UTF-8;
- [$response]: obiekt [Response], który zawiera odpowiedź serwera skierowaną do klienta;
- [$logger]: obiekt [Logger] służący do tworzenia logów;
- [$infos]: informacje umożliwiające wysłanie wiadomości e-mail do administratora aplikacji;
- wiersze 135–137: wysyłamy wiadomość e-mail do administratora aplikacji;
- wiersz 139: wysyłana jest odpowiedź do klienta:
- $response: odpowiedź HTTP;
- $result: serwer wysyła ciąg jSON z tablicy [‘réponse’=>["erreur" => $message]];
- $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], kod 500;
- $headers: [], brak nagłówków HTTP do dodania do odpowiedzi;
- $logger: moduł logowania aplikacji;
- wiersz 58: dzięki utworzonej sesji uwierzytelnianie klienta nastąpi tylko raz:
- po uwierzytelnieniu klienta do sesji zostanie dodany klucz [user] (wiersz 78);
- przy kolejnym żądaniu tego samego klienta wiersz 58 pozwala uniknąć niepotrzebnego uwierzytelniania;
- wiersz 103: dzięki utworzonej sesji dane z bazy będą wyszukiwane tylko raz:
- przy pierwszym żądaniu zostanie przeprowadzone wyszukiwanie w bazie danych (wiersz 108). Pobrane dane są następnie umieszczane w sesji (wiersz 110) i powiązane z kluczem [taxAdminData];
- przy kolejnych żądaniach klucz [taxAdminData] zostanie znaleziony w sesji (wiersz 103), a następnie dane z dysku zostaną bezpośrednio przekazane do warstwy [dao] (wiersz 119);
- wiersze 111–116: wyszukiwanie danych podatkowych w bazie może zakończyć się niepowodzeniem. W takim przypadku do klienta wysyłany jest kod [500 Internal Server Error];
- wiersz 113: komunikat o błędzie związany z wyjątkiem sterownika MySQL jest zakodowany jako ISO 8859-1. Przekształca się go na UTF-8, aby został poprawnie zarejestrowany w dzienniku;
- pozostała część kodu jest niemal identyczna jak w poprzedniej wersji;
- wiersze 143–164: funkcja [sendResponse] wysyła wszystkie odpowiedzi do klienta;
- wiersze 144–148: znaczenie parametrów;
- wiersz 153: odpowiedzią jest zawsze ciąg znaków jSON z tablicy [‘résultat’=>qqChose];
- wiersz 156: czasami do odpowiedzi należy dodać nagłówki HTTP. Tak jest w wierszu 71;
- wiersz 158: odpowiedź zostaje wysłana;
- wiersze 160–163: odpowiedź jest rejestrowana w dzienniku, a moduł rejestrujący jest zamykany;
19.1.4. Testy [Codeception]

Przetestujemy tylko warstwę [dao], która jako jedyna uległa zmianie.
Kod testu [ServerDaoTest] jest następujący:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw
namespace Application;
// definicja stałych
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// pobieranie konfiguracji
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// dołączanie zależności niezbędnych dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// element nadrzędny
parent::__construct();
// pobieranie konfiguracji
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// tworzenie warstwy [dao]
$dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// testy
public function testTaxAdminData() {
…
}
}
- wiersze 9–24: tworzymy środowisko wykonawcze identyczne z tym ze skryptu serwerowego [impots-server];
- wiersz 38: w celu utworzenia warstwy [dao] instancjonuje się klasę [ServerDaoWithSession];
Wynik testów jest następujący:

19.2. Klient
Zajmujemy się częścią kliencką aplikacji.

Architektura ta zostanie zaimplementowana za pomocą następujących skryptów:

W nowej wersji zmieniają się jedynie:
- plik konfiguracyjny [config-client.json];
- warstwa kliencka [dao];
19.2.1. warstwa [dao]
Warstwa [Dao] ulega następującym zmianom:
<?php
namespace Application;
// zależności
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// wykorzystanie funkcji Trait
use TraitDao;
// atrybuty
private $urlServer;
private $user;
private $sessionCookie;
// konstruktor
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// obliczanie podatku
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// plik cookie sesji?
if (!$this->sessionCookie) {
// tworzymy klienta HTTP
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// wysyłamy żądanie do serwera bez pliku cookie sesji
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]
]);
} else {
// wysyłamy żądanie do serwera z plikiem cookie sesji
// tworzymy klienta HTTP
$httpClient = HttpClient::create([
"verify_peer" => false
]);
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"headers" => ["Cookie" => $this->sessionCookie]
]);
}
// odbieramy odpowiedź
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logi
print "$json=json\n";
// pobierany jest status odpowiedzi
$statusCode = $response->getStatusCode();
// błąd?
if ($statusCode !== 200) {
// wystąpił błąd – generowany jest wyjątek
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// pobieramy plik cookie sesji
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// plik cookie sesji?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// zwracamy odpowiedź
return $réponse;
}
}
Komentarze
Modyfikacja warstwy [dao] polega na dodaniu obsługi sesji:
- wiersz 14: plik cookie sesji;
- wiersze 25–39: podczas pierwszego żądania ten plik cookie nie istnieje: wówczas wysyłamy żądanie do serwera, przekazując dane uwierzytelniające (wiersz 28);
- wiersze 40–53: przy kolejnych żądaniach zazwyczaj dysponujemy plikiem cookie sesji. Nie wysyłamy więc danych uwierzytelniających (wiersze 42–44);
- wiersze 69–82: odpowiedź serwera na pierwsze żądanie będzie zawierała plik cookie sesji. Pobieramy go. Kod ten został już wykorzystany i wyjaśniony w akapicie „link”;
- wiersz 78: pobrany plik cookie sesji jest zapisywany w atrybucie klasy [$sessionCookie];
Uwaga: można było zachować poprzednią wersję warstwy [dao] i przeprowadzać uwierzytelnianie przy każdym żądaniu, ponieważ wiąże się to z nieznacznym kosztem. Ze względów dydaktycznych chcieliśmy jednak przypomnieć, w jaki sposób klient HTTP może zarządzać sesją.
19.2.2. Plik konfiguracyjny
Plik konfiguracyjny jSON zmienia się w następujący sposób:
{
"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"
}
Zmienia się jedynie element URL w wierszu 24.
19.3. Kilka testów
19.3.1. Test 1
Najpierw uruchamiamy klienta w środowisku bez błędów. Wyniki są takie same jak w poprzednich wersjach. Jednak teraz po stronie serwera mamy plik dziennika o nazwie [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
…
- wiersze 3–7: podczas pierwszego żądania następuje uwierzytelnienie i wyszukiwanie danych w bazie;
- wiersze 9–14: podczas kolejnego żądania nie następuje już uwierzytelnianie, a dane są pobierane z sesji. Sytuacja ta powtarza się podczas kolejnych żądań (wiersze 15 i kolejne);
19.3.2. Test 2
Teraz wyłączmy bazę danych MySQL. Po stronie klienta otrzymujemy następujący wynik w konsoli:
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é
Po stronie serwera pojawiają się następujące logi [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
…
Aby uzyskać wiadomość e-mail otrzymaną przez administratora aplikacji, używamy skryptu [imap-03.php] z akapitu dotyczącego powiązania z następującym plikiem konfiguracyjnym [config-imap-01.json]:
Otrzymujemy następujący wynik:

Plik [message_1.txt] zawiera następujący tekst:
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. Test 3
Teraz sprawmy, aby plik [logs.txt] nie mógł zostać utworzony. W tym celu wystarczy utworzyć folder o nazwie [logs.txt]:

Po wykonaniu tej czynności uruchommy klienta.
Po stronie klienta otrzymujemy następujące wyniki w konsoli:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé
Po stronie serwera nie ma żadnych logów, ale administrator otrzymuje następującą wiadomość e-mail:
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. Test 4
Tym razem w pliku konfiguracyjnym klienta podajmy błędne dane uwierzytelniające dla łączącego się klienta.
Klient wyświetla następujące wyniki w konsoli:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
Po stronie serwera pojawiają się następujące logi:
---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. Test 5
Wprowadźmy ponownie prawidłowy identyfikator użytkownika [admin, admin] do pliku konfiguracyjnego klienta.
Teraz spróbujmy uzyskać dostęp do serwera URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] bezpośrednio w przeglądarce, bez podawania parametrów:
W pliku logów serwera [logs.txt] znajdują się następujące wiersze:
---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. Testy [Codeception]
Podobnie jak w przypadku poprzednich wersji, napiszemy testy [Codeception] dla wersji 09.

19.4.0.1. Test warstwy [métier]
Test [ClientMetierTest.php] wygląda następująco:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw
namespace Application;
// definicja stałych
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09");
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// pobieranie konfiguracji
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// dołączanie zależności niezbędnych dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// używa
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;
// klasa testowa
class ClientMetierTest extends Unit {
…
}
Uwagi
- w porównaniu z klasą testową wersji 08 zmienia się jedynie wiersz 10, który określa katalog główny testowanego klienta;
Wyniki testu są następujące:

Warto zapoznać się z logami serwera [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}}
Można zauważyć, że dane urzędu skarbowego są zawsze pobierane z bazy danych, a nigdy z sesji. Wróćmy do kodu przeprowadzonego testu:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw
namespace Application;
…
// klasa testowa
class ClientMetierTest extends Unit {
// warstwa biznesowa
private $métier;
public function __construct() {
parent::__construct();
// pobieranie konfiguracji
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// tworzenie warstwy [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// tworzenie warstwy [métier]
$this->métier = new ClientMetier($clientDao);
}
// testy
public function test1() {
…
}
public function test2() {
…
}
public function test3() {
…
}
…
}
W klasie testowej [Codeception] konstruktor jest wywoływany dla każdego testu.
- wiersz 21: dla każdego testu tworzony jest zatem nowy obiekt [ClientDao] z plikiem cookie sesji NULL. To wyjaśnia, dlaczego ten klient nie korzysta z żadnej sesji;
Ten przykład pokazuje, że sesja nie jest odpowiednim miejscem do przechowywania danych administracji podatkowej. Dane te są bowiem wspólne dla wszystkich użytkowników aplikacji. Tutaj jednak są one powielane w każdej z ich sesji.
W programowaniu internetowym rozróżnia się trzy rodzaje widoczności danych współdzielonych:
- dane współdzielone przez wszystkich użytkowników aplikacji internetowej. Są to zazwyczaj dane tylko do odczytu. PHP nie dysponuje natywnie taką pamięcią;
- dane współdzielone przez żądania tego samego klienta. Dane te są przechowywane w sesji. Mówimy wówczas o sesji klienta, aby określić pamięć klienta. Wszystkie żądania danego klienta mają dostęp do tej sesji. Mogą w niej przechowywać i odczytywać informacje. W powyższych skryptach sesja ta jest zaimplementowana przez obiekt Symfony [HttpFoundation\Session\Session];
- pamięć żądania lub kontekst żądania. Żądanie użytkownika może być przetwarzane przez kilka kolejnych akcji. Kontekst żądania pozwala akcji 1 przekazać informacje do akcji 2. W poprzednich skryptach żądanie jest zaimplementowane przez obiekt Symfony [HttpFoundation\Request], a jego pamięć przez atrybut [HttpFoundation\Request::attributes];

Istnieją biblioteki zewnętrzne, które zapewniają obiektowi PHP pamięć aplikacji. Nowa wersja ćwiczenia praktycznego pokazuje wykorzystanie jednej z nich.