18. Ćwiczenie praktyczne – wersja 8
Wrócimy do przykładowej aplikacji – wersja 5 (akapit „link”) i przekształcimy ją w aplikację klient-serwer.
18.1. Introduction
Architektura wersji 5 wyglądała następująco:

- warstwa o nazwie [dao] (Data Access Objects) odpowiada za komunikację z bazą danych MySQL oraz lokalnym systemem plików;
- warstwa o nazwie [métier] dokonuje obliczeń podatku;
- główny skrypt pełni rolę koordynatora: instancjonuje warstwy [dao] i [métier], a następnie komunikuje się z warstwą [métier] w celu wykonania niezbędnych czynności;
Przeniesiemy tę architekturę do następującej architektury klient-serwer:

- W warstwie [2] przejmujemy warstwę [dao] z wersji 5, usuwając z niej metody dostępu do lokalnego systemu plików. Metody te zostaną przeniesione do warstwy [dao] klienta [6, 7];
- w warstwie [3], warstwa [métier] pozostanie warstwą z wersji 5 bez metod [executeBatchImpôts, saveResults], które zostaną przeniesione do warstwy [dao] i [7] klienta;
- w warstwie [4] należy napisać skrypt serwerowy, który będzie musiał:
- utworzyć warstwy [métier] oraz [dao] i [3, 2];
- komunikować się ze skryptem klienckim [5, 7];
- w przypadku [7] należy napisać warstwę kliencką [dao]:
- będzie ona klientem HTTP skryptu serwerowego [4, 5];
- będzie ona wykorzystywać metody dostępu do lokalnego systemu plików warstwy [dao] z wersji 5;
- w [8] warstwa kliencka [métier] będzie zgodna z interfejsem [InterfaceMetier] z wersji 5. Jej implementacja będzie jednak inna. W wersji 5 warstwa [métier] wykonywała obliczenia podatku. Tutaj obliczenia te wykonuje warstwa [métier] serwera. Warstwa [métier] będzie zatem wywoływać warstwy [dao] i [7] w celu nawiązania komunikacji z serwerem i poproszenia go o obliczenie podatku;
- w warstwie [9] skrypt konsolowy będzie musiał utworzyć instancję warstw klienckich [dao, métier] i uruchomić ich wykonywanie;
18.2. Serwer
Zajmujemy się częścią serwerową aplikacji.

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

18.2.1. Elementy wymieniane między warstwami

Entities wymieniane między warstwami to te z wersji 5 opisane w akapicie „link”.
18.2.2. Warstwa [dao]

Warstwa [dao] implementuje następujący interfejs [InterfaceServerDao]:
<?php
// przestrzeń nazw
namespace Application;
interface InterfaceServerDao {
// odczyt danych z urzędu skarbowego
public function getTaxAdminData(): TaxAdminData;
}
- wiersz 9: metoda [getTaxAdminData] pobiera dane z urzędu skarbowego z bazy danych;
Interfejs [InterfaceServerDao] jest zaimplementowany przez następującą klasę [ServerDao]:
<?php
// przestrzeń nazw
namespace Application;
// definicja klasy ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
// obiekt typu TaxAdminData zawierający dane dotyczące przedziałów podatkowych
private $taxAdminData;
// obiekt typu [Database] zawierający cechy charakterystyczne BD
private $database;
// konstruktor
public function __construct(string $databaseFilename) {
// zapisywana jest konfiguracja bazy danych JSON
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// przygotowuje się atrybut
$this->taxAdminData = new TaxAdminData();
try {
// otwieramy połączenie z bazą danych
$connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// chcemy, aby przy każdym błędzie SGBD zgłaszany był wyjątek
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// rozpoczynamy transakcję
$connexion->beginTransaction();
// wypełniamy tabelę przedziałów podatkowych
$this->getTranches($connexion);
// wypełnia się tabelę stałych
$this->getConstantes($connexion);
// transakcja zostaje pomyślnie zakończona
$connexion->commit();
} catch (\PDOException $ex) {
// czy trwa jakaś transakcja?
if (isset($connexion) && $connexion->inTransaction()) {
// zakończenie transakcji z powodu niepowodzenia
$connexion->rollBack();
}
// przekazujemy wyjątek do kodu wywołującego
throw new ExceptionImpots($ex->getMessage());
} finally {
// zamyka się połączenie
$connexion = NULL;
}
}
// odczyt danych z bazy
private function getTranches($connexion): void {
…
}
// odczyt tabeli stałych
private function getConstantes($connexion): void {
…
}
// zwraca dane umożliwiające obliczenie podatku
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Kod ten został przedstawiony w akapicie „link”.
18.2.3. Warstwa [métier]


Warstwa [métier] implementuje następujący interfejs [InterfaceServerMetier]:
<?php
// przestrzeń nazw
namespace Application;
interface InterfaceServerMetier {
// obliczanie podatków podatnika
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
Interfejs [InterfaceServerMetier] jest zaimplementowany przez następującą klasę [ServerMetier]:
<?php
// przestrzeń nazw
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// warstwa DAO
private $dao;
// dane administracji podatkowej
private $taxAdminData;
//---------------------------------------------
// ustawianie warstwy [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $dao) {
// zapisuje się odniesienie do warstwy [dao]
$this->dao = $dao;
// pobierane są dane umożliwiające obliczenie podatku
// metoda [getTaxAdminData] może wygenerować wyjątek ExceptionImpots
// następnie pozwala się na przekazanie wyjątku do kodu wywołującego
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// obliczenie podatku
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// wynik
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 {
…
// wynik
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=wynagrodzenieRoczne-ulga
// odliczenie ma wartość minimalną i maksymalną
private function getRevenuImposable(float $salaire): float {
…
// wynik
return floor($revenuImposable);
}
// oblicza ewentualną obniżkę
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// wynik
return ceil($décôte);
}
// oblicza ewentualną obniżkę
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
..
// wynik
return ceil($réduction);
}
}
Kod ten został już omówiony w wersji 1 w akapicie [link]. Jego wersja obiektowa z bazą danych została przedstawiona w akapicie [link].
18.2.4. Skrypt serwerowy


Skrypt serwerowy implementuje warstwę [web] [4]. Skrypt [impots-server] jest konfigurowany przez następujący plik 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"
}
]
}
- wiersz 1: katalog główny, od którego będą mierzone ścieżki do plików;
- wiersz 2: plik konfiguracyjny bazy danych jSON dla bazy danych MySQL;
- wiersz 3: plik jSON zawierający dane administracji podatkowej;
- wiersze 5–14: pliki aplikacji;
- wiersz 15: zależność od bibliotek zewnętrznych, w tym przypadku Symfony;
- wiersze 16–20: tabela użytkowników uprawnionych do korzystania z aplikacji;
Pliki jSON i [database.json, taxadmindata.json] należą do wersji 5 opisanej w akapicie „link”.
Skrypt [impots-server] implementuje warstwę [web] 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");
// pobieramy konfigurację
$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";
}
// definicja stałych
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// przygotowanie odpowiedzi serwera JSON
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// pobieranie bieżącego żądania
$request = Request::createFromGlobals();
// uwierzytelnianie
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// czy użytkownik istnieje?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// ustalanie kodu statusu odpowiedzi
if (!$trouvé) {
// nie znaleziono – kod 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// komunikat o błędzie
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// koniec
exit;
}
// mamy prawidłowego użytkownika – sprawdzamy otrzymane parametry
$erreurs = [];
// muszą być podane trzy parametry GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// błąd?
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// pobieramy stan cywilny
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// błąd?
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// pobieramy liczbę dzieci
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// liczba dzieci musi być liczbą całkowitą >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// błąd?
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// pobieramy roczne wynagrodzenie
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// wynagrodzenie musi być liczbą całkowitą >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// błąd?
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// inne parametry w zapytaniu?
foreach (\array_keys($request->query->all()) as $key) {
// czy parametr jest prawidłowy?
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// błędy?
if ($erreurs) {
// wysyłamy kod błędu 400 do klienta
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// mamy wszystko, co potrzebne do pracy
// tworzenie architektury serwera
$msgErreur = "";
try {
// tworzenie warstwy [dao]
$dao = new ServerDao($config["databaseFilename"]);
// tworzenie warstwy [métier]
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// zauważono błąd
$msgErreur = utf8_encode($ex->getMessage());
}
// błąd?
if ($msgErreur) {
// wysyłamy kod błędu 500 do klienta
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// obliczanie podatku
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// zwracamy odpowiedź
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
Komentarze
- wiersz 16: odczytuje się plik konfiguracyjny;
- wiersze 18–26: ładowane są wszystkie zależności;
- wiersz 29: nazwa pliku [database.json];
- wiersze 32–33: deklaruje się klasy bibliotek zewnętrznych, które będą używane;
- wiersze 36–38: przygotowuje się odpowiedź o nazwie jSON;
- wiersze 40–52: sprawdzamy, czy użytkownik wysyłający żądanie należy do grupy uprawnionych użytkowników;
- wiersze 54–63: jeśli tak nie jest, wysyłany jest kod HTTP 401, oznaczający odmowę dostępu. Po otrzymaniu tego kodu oraz nagłówka HTTP [WWW-Authenticate => Basic realm=] większość przeglądarek wyświetla okno uwierzytelniania z prośbą o uwierzytelnienie się;
- wiersz 59: odpowiedź serwera jSON wyjaśnia przyczynę błędu. Wszystkie odpowiedzi serwera będą stanowić ciąg znaków jSON z tablicy [‘réponse’=>’qq chose’];
- wiersze 64–117: sprawdzana jest poprawność żądania:
- żądanie o numerze GET zawierające dokładnie trzy parametry;
- parametr [marié], którego wartość musi wynosić „tak” lub „nie”;
- parametr [enfants], którego wartość musi być liczbą całkowitą >=0;
- parametr [salaire], którego wartość musi być liczbą całkowitą >=0;
- wiersz 65: za każdym razem, gdy wykryty zostanie błąd, do tablicy [$erreurs] dodawany jest komunikat o błędzie;
- wiersze 120–126: jeśli wystąpi błąd, wysyłany jest kod HTTP [400 Bad Request] do klienta (wiersz 122);
- wiersz 123: odpowiedź serwera o kodzie jSON wyjaśnia przyczynę błędu;
- od wiersza 132 wszystko zostało sprawdzone. Można utworzyć instancję warstw [dao, métier]. Utworzenie tej instancji wiąże się z kosztem i należy to zrobić tylko wtedy, gdy mamy pewność, że żądanie jest prawidłowe;
- wiersze 130–138: tworzy się architekturę serwera. Tworzenie warstwy [dao] może spowodować wygenerowanie wyjątku typu [ExceptionImpots]. Jeśli wystąpi ten wyjątek, odnotowuje się błąd;
- wiersze 135–138: jeśli wystąpił wyjątek, wysyłamy do klienta kod HTTP 500. Kod ten oznacza, że serwer uległ awarii;
- wiersz 143: odpowiedź wyjaśnia przyczynę błędu;
- wiersz 148 : obliczenie podatku jest przekazywane do warstwy [métier];
- wiersze 150–151: wysłanie odpowiedzi;
Przetestujmy ten skrypt w przeglądarce. Wywołajmy zabezpieczoną stronę URL poprzez [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

- w [1], żądany zabezpieczony URL;
- na [2], trzy parametry [marié, enfants, salaire];
- w [3] serwer Apache Laragon wysłał certyfikat SSL z podpisem własnym. Przeglądarka to wykryła i wyświetla ostrzeżenie dotyczące bezpieczeństwa: uznaje, że strona serwera nie jest godna zaufania;
- w przypadku [4] kontynuujemy;

- w [6] kontynuujemy;

- w [7] przeglądarka wyświetla okno, w którym użytkownik może się uwierzytelnić;
- w [9,10] wpisujemy [admin] i [admin];

- w [13] pojawia się odpowiedź serwera jSON;
Przeprowadźmy kilka testów błędów:
Wysyłamy żądanie o URL i [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]
Otrzymujemy następujący wynik:

Odcinamy SGBD MySQL i wysyłamy żądanie URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

18.2.5. Testy [Codeception]
Za każdym razem, gdy będziemy tworzyć nową wersję serwera, będziemy testować warstwy [métier] i [dao], tak jak to robiliśmy od wersji 04 (patrz akapity link i link).
Najpierw powiążemy projekt [scripts-web] z testami [Codeception]. W tym celu należy postępować zgodnie z procedurą opisaną dla projektu [scripts-console] w akapicie link. Otrzymujemy projekt [scripts-web] z folderem [Test Files]:

Utworzymy test dla warstwy [dao] oraz test dla warstwy [métier].
18.2.5.1. Testy warstwy [dao]

Test [ServerDaoTest] będzie 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:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// pobieramy konfigurację
$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();
// pobieramy konfigurację
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// tworzenie warstwy [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// testy
public function testTaxAdminData() {
…
}
}
Komentarze
- wiersze 9–24: tworzone jest to samo środowisko pracy, co w przypadku serwera [impots-server.php]. Odbywa się to w wierszach 9–12 poprzez zdefiniowanie dwóch stałych, od których zależy to środowisko;
- wiersze 32–40: tworzymy instancję warstwy [dao], która ma zostać przetestowana, tak jak zrobiono to w skrypcie serwera [impots-server.php];
- od tego momentu panują takie same warunki jak w skrypcie serwerowym [impots-server.php]: można rozpocząć testy;
- wiersze 43–45: metoda [testTaxAdminData] jest zgodna z opisem w akapicie „link”;
Wyniki testu są następujące:

18.2.5.2. Testy warstwy [métier]

Test [ServerMetierTest] będzie 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:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// ś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";
}
// klasa testowa
class ServerMetierTest extends \Codeception\Test\Unit {
// warstwa biznesowa
private $métier;
public function __construct() {
parent::__construct();
// pobieramy konfigurację
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// tworzenie warstwy [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// tworzenie warstwy [métier]
$this->métier = new ServerMetier($dao);
}
// testy
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
Komentarze
- wiersze 9–24: tworzymy takie samo środowisko pracy jak w przypadku serwera [impots-server.php]. Odbywa się to w wierszach 9–12 poprzez zdefiniowanie dwóch stałych, od których zależy środowisko;
- wiersze 30–38: tworzymy instancję warstwy [métier] do przetestowania, tak jak zrobiono to w skrypcie serwera [impots-server.php];
- od tego momentu panują takie same warunki jak w skrypcie serwerowym [impots-server.php]: można rozpocząć testy;
- wiersze 40–53: metody [test1, test2…, test11] są opisane w akapicie „link”;
Wyniki testu są następujące:

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

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

18.3.1. Elementy wymieniane między warstwami

Wszystkie powyższe elementy zostały już opisane i wykorzystane:
- [BaseEntity] w akapicie dotyczącym linków;
- [ExceptionImpots] w akapicie „link”;
- [TaxPayerData] w akapicie „link”;
18.3.2. Warstwa [dao]

Warstwa [dao] implementuje następujący interfejs [InterfaceClientDao]:
<?php
// przestrzeń nazw
namespace Application;
interface InterfaceClientDao {
// odczyt danych podatników
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// obliczanie podatków podatnika
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// zapisywanie wyników
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- wiersz 9: funkcja [getTaxPayersData] wczytuje do pamięci dane podatników z pliku [$taxPayersFilename]. W przypadku wystąpienia błędów są one rejestrowane w pliku [$errorsFilename];
- wiersz 12: funkcja [calculerImpots] oblicza podatek podatnika;
- wiersz 15: funkcja [saveResults] zapisuje w pliku [$resultsFilename] dane z tablicy [$taxPayersData], które przedstawiają wyniki kilku obliczeń podatku;
Interfejs [InterfaceClientDao] jest zaimplementowany przez następującą klasę [ClientDao]:
<?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;
// 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 {
// tworzy się klienta HTTP
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// wysyłamy zapytanie do serwera
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// odbieranie odpowiedzi
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logi
// wyświetla się „$json=json\n”;
// pobieramy status odpowiedzi
$statusCode = $response->getStatusCode();
// błąd?
if ($statusCode !== 200) {
// wystąpił błąd – zgłaszamy wyjątek
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// zwracamy odpowiedź
return $réponse;
}
}
Komentarze
- wiersz 10: dodajemy klasę [TraitDao] (patrz akapit „link”), która implementuje metody [getTaxPayersData] i [saveResults]. Pozostaje więc do zaimplementowania tylko metoda [calculerImpots]. Jest ona zaimplementowana w wierszach 22–49;
- wiersze 16–19: konstruktor klasy [ClientDao] przyjmuje dwa parametry:
- URL [$urlServer] z serwera obliczającego podatek;
- tablicę [$user] zawierającą klucze „login” i „passwd”, która określa użytkownika wysyłającego żądanie;
- wiersz 22: metoda [calculerImpots] odbiera trzy parametry, które mają zostać wysłane do serwera obliczającego podatki;
- wiersze 24–27: tworzy się klienta HTTP z:
- wiersz 25: dane identyfikacyjne użytkownika wysyłającego żądanie;
- wiersz 26: opcja, dzięki której klient HTTP nie będzie sprawdzał ważności certyfikatu SSL przesłanego przez serwer;
- wiersze 29–34: wysyłane jest zapytanie do serwera z trzema oczekiwanymi przez niego parametrami;
- wiersz 36: odbierana jest odpowiedź jSON z serwera. Jeśli nie podamy parametru [false] w metodzie [Response::getContent], to w przypadku, gdy status odpowiedzi serwera mieści się w przedziale [3xx-5xx] (sytuacja błędu), obiekt [Response] zgłasza wyjątek, gdy tylko spróbuje się uzyskać treść odpowiedzi [Response::getContent] lub jej nagłówki HTTP i [Response::getHeaders]. W tym przypadku, niezależnie od statusu odpowiedzi HTTP, chcemy mieć dostęp do jej treści, choćby po to, by ją zarejestrować w dzienniku (wiersz 40);
- wiersze 37–38: odpowiedź serwera to ciąg znaków jSON z tablicy [‘réponse’=>qqChose]. Pobieramy [qqChose];
- wiersz 40: rejestrujemy odpowiedź jSON w trybie programistycznym;
- wiersz 42: pobieramy kod statusu odpowiedzi;
- wiersze 44–49: jeśli kod statusu HTTP nie wynosi 200, oznacza to, że nasz serwer napotkał problem. Następnie generujemy wyjątek typu [ExceptionImpots] z komunikatem zawierającym odpowiedź jSON z serwera, uzupełnioną o kod HTTP z odpowiedzi;
- wiersz 51: zwracamy wynik, którym jest tablica asocjacyjna z kluczami [impôt, surcôte, décôte, réduction, taux];
18.3.3. Warstwa [métier]


Warstwa [métier] [8] implementuje następujący interfejs [InterfaceClientMetier]:
<?php
// przestrzeń nazw
namespace Application;
interface InterfaceClientMetier {
// obliczanie podatków podatnika
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// obliczanie podatków w trybie wsadowym
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- wiersz 9: funkcja [calculerImpots] oblicza podatek;
- wiersz 12: funkcja [executeBatchImpots] oblicza podatek dla podatników, których dane znajdują się w pliku [$taxPayersFileName], zapisuje uzyskane wyniki w pliku [$resultsFileName], a napotkane błędy w pliku [$errorsFileName];
Interfejs [InterfaceClientMetier] jest zaimplementowany przez następującą klasę [ClientMetier]:
<?php
// przestrzeń nazw
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// atrybut
private $clientDao;
// konstruktor
public function __construct(InterfaceClientDao $clientDao) {
// zapisywanie odniesienia w warstwie [dao]
$this->clientDao = $clientDao;
}
// obliczanie podatku
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// obliczanie podatków w trybie wsadowym
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// pozwala się na przekazywanie wyjątków pochodzących z warstwy [dao]
// pobieranie danych podatników
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabela wyników
$results = [];
// przetwarzamy je
foreach ($taxPayersData as $taxPayerData) {
// obliczamy podatek
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// uzupełnianie [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// wpisuje się wynik do tabeli wyników
$results [] = $taxPayerData;
}
// zapisywanie wyników
$this->clientDao->saveResults($resultsFileName, $results);
}
}
Komentarze
- wiersze 11–14: konstruktor klasy [ClientMetier] otrzymuje jako parametr odwołanie do warstwy [dao];
- wiersze 17–19: obliczenie podatku jest przekazywane do warstwy [dao];
- wiersze 20–38: funkcja [executeBatchImpots] została opisana w akapicie „link”;
18.3.4. Skrypt główny


Skrypt kliencki [MainImpotsClient.php] implementuje warstwy [console] i [9]. Jest on konfigurowany przez następujący plik: jSON i [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"
}
- wiersz 1: katalog główny klienta;
- wiersz 2: plik jSON zawierający dane podatników;
- wiersz 3: plik jSON zawierający wyniki;
- wiersz 4: plik jSON zawierający błędy;
- wiersze 6–19: różne zależności projektu klienta;
- wiersze 20–23: użytkownik wysyłający zapytania do serwera obliczającego podatki;
- wiersz 24: zabezpieczony plik URL serwera obliczającego podatki;
Kod skryptu [MainImpotsClient.php] jest następujący:
<?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-client.json");
// pobieramy konfigurację
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// dołączamy zależności niezbędne 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";
}
// definicje stałych
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// zależności Symfony
use Symfony\Component\HttpClient\HttpClient;
// tworzenie warstwy [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// tworzenie warstwy [métier]
$clientMetier = new ClientMetier($clientDao);
// obliczanie podatków w trybie wsadowym
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// wyświetlenie błędu
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// koniec
print "Terminé\n";
exit;
Komentarze
- wiersz 13: ścieżka do pliku konfiguracyjnego;
- wiersz 16: przetwarzanie pliku konfiguracyjnego;
- wiersze 18–26: ładowanie zależności;
- wiersz 37: utworzenie warstwy [dao]. Przekazujemy konstruktorowi warstwy dwie informacje, których oczekuje:
- identyfikator URL serwera obliczającego podatki;
- identyfikatory użytkownika, który będzie wysyłał zapytania;
- wiersz 39: utworzenie warstwy [métier]. Przekazujemy konstruktorowi warstwy odwołanie do właśnie utworzonej warstwy [dao];
- wiersz 43: warstwa [métier] otrzymuje polecenie:
- obliczenie podatków wszystkich podatników z pliku $config["taxPayerDataFileName"];
- zapisać wyniki w pliku $config["resultsFileName"];
- zapisać błędy w pliku $config["errorsFileName"];
- w linii 43 mogą wystąpić wyjątki;
- wiersz 46: wyświetlenie komunikatu o błędzie wyjątku;
Działanie klienta daje takie same wyniki jak w poprzednich wersjach. Sprawdź następujące pliki:
- [Data/taxpayersdata.json]: dane podatników, dla których obliczana jest kwota podatku;
- [Data/results.json]: wyniki dla poszczególnych podatników z pliku [Data/taxpayersdata.json];
- [Data/errors.json]: błędy, które mogły wystąpić podczas przetwarzania pliku [Data/taxpayersdata.json];
Przyjrzyjmy się możliwym przypadkom błędów. Najpierw zatrzymajmy serwer Laragon. Wyniki w konsoli klienta będą wówczas następujące:
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é
Teraz uruchommy tylko serwer Apache, a nie serwer SGBD MySQL:

Wyniki w konsoli klienta będą wówczas następujące:
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é
Teraz uruchommy MySQL, a następnie zmieńmy w [config-client] użytkownika, który się loguje:
Wyniki w konsoli klienta są wówczas następujące:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
18.3.5. Testy [Codeception]
Podobnie jak w przypadku poprzednich wersji, napiszemy testy [Codeception] dla wersji 08.

18.3.5.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-08");
// ś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";
}
//
// klasa testowa
class ClientMetierTest extends \Codeception\Test\Unit {
// warstwa biznesowa
private $métier;
public function __construct() {
parent::__construct();
// pobieramy konfigurację
$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 test11() {
…
}
}
Komentarze
- wiersze 10–26: definicja środowiska testowego. Wykorzystujemy to samo środowisko, co w głównym skrypcie [MainImpotsClient] opisanym w akapicie „link”;
- wiersze 33–41: tworzenie warstw [dao] i [métier];
- wiersz 40: atrybut [$this→métier] odwołuje się do warstwy [métier];
- wiersze 44–51: metody [test1, test2…, test11] są zgodne z opisem w akapicie „link”;
Wyniki testu są następujące:
