20. Ćwiczenie praktyczne – wersja 10
W poprzedniej wersji wykazano, że dane podatkowe, wspólne dla wszystkich użytkowników aplikacji, powinny być przechowywane w pamięci o zasięgu [Application]. W celu jej zaimplementowania wykorzystamy serwer Redis [https://redis.io].
20.1. Redis
Pamięć o identyfikatorze [Application] zostanie zaimplementowana przez serwer Redis. Skrypty o identyfikatorach PHP, które potrzebują tej pamięci aplikacyjnej, będą klientami tego serwera:

20.2. Instalacja Redis
W Laragonie serwer Redis jest domyślnie wyłączony. Należy więc najpierw go włączyć:

- w [3] włącz serwer [Redis];
- w [4] pozostawić port [6379], z którego domyślnie korzystają klienci Redis;
Usługi Laragon są automatycznie uruchamiane ponownie po aktywacji Redis:

20.3. Klient Redis w trybie poleceń
Z serwerem Redis można komunikować się w trybie poleceń. Otwieramy terminal Laragon (patrz link w akapicie):

- w [1] polecenie [redis-cli] uruchamia klienta w trybie poleceń serwera Redis;
W lipcu 2019 r. klient Redis może korzystać ze 172 poleceń do komunikacji z serwerem [https://redis.io/commands#list]. Jedno z nich, [command count] [2], wyświetla tę liczbę [3].
Przedstawimy tylko te polecenia, które będą nam potrzebne w naszej aplikacji PHP. Będziemy używać Redis wyłącznie do jednego celu: przechowywania tablicy [‘attribut’=>’valeur’] w pamięci Redis. Odbywa się to za pomocą polecenia Redis [set attribut valeur] [4]. Wartość tę można następnie odzyskać za pomocą polecenia [get attribut] [5]. To wszystko, czego będziemy potrzebować.
Konieczne może być wyczyszczenie pamięci Redis. Odbywa się to za pomocą polecenia [flushdb] [6]. Następnie, jeśli zapytamy o wartość atrybutu [titre] [7], otrzymamy odwołanie [nil] [8] wskazujące, że atrybut nie został znaleziony. Można również użyć polecenia [exists] [9-10], aby sprawdzić, czy dany atrybut istnieje.
Aby zamknąć klienta Redis, należy wpisać polecenie [quit] [11].
20.4. Instalacja klienta Redis dla PHP
Teraz musimy zainstalować klienta Redis dla PHP:

Istnieje kilka bibliotek implementujących klienta Redis. Wykorzystamy bibliotekę [Predis] [https://github.com/nrk/predis] (lipiec 2019 r.). Tak jak poprzednie, instaluje się ją za pomocą [composer] w terminalu Laragon:

20.5. Kod serwera

Plik konfiguracyjny [config-server.json] zmienia się w następujący sposób:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-10",
"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",
"/../version-09/Dao/ServerDaoWithSession.php",
"/../version-08/Métier/InterfaceServerMetier.php",
"/../version-08/Métier/ServerMetier.php",
"/../version-09/Utilities/Logger.php",
"/../version-09/Utilities/SendAdminMail.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/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"
}
Komentarze
- wiersze 5–15: wersja 10 nie wprowadza żadnych nowości poza skryptem [impots-server.php]. Wykorzystuje elementy z wersji 08 i 09;
- wiersz 19: wymagana zależność od biblioteki [predis], którą właśnie zainstalowaliśmy;
Kod 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");
// alias klasy
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
// sesja
$session = new Session();
$session->start();
…
…
// pierwszy wpis
$logger->write("\n---nouvelle requête\n");
// pobieranie bieżącego żądania
$request = Request::createFromGlobals();
// uwierzytelnianie tylko przy pierwszym uruchomieniu
if (!$session->has("user")) {
…
} else {
// logowanie
$logger->write("Authentification prise en session…\n");
}
// mamy prawidłowego użytkownika – sprawdzamy otrzymane parametry
$erreurs = [];
// muszą być podane trzy parametry GET
$method = strtolower($request->getMethod());
…
// 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
// Redis
\Predis\Autoloader::register();
try {
// klient [predis]
$redis = new \Predis\Client();
// łączymy się z serwerem, aby sprawdzić, czy jest dostępny
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// błąd wewnętrzny serwera
doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
// zakończono
exit;
}
// tworzenie warstwy [dao]
if (!$redis->get("taxAdminData")) {
// dane podatkowe są pobierane z bazy danych
$logger->write("données fiscales prises en base de données\n");
try {
// tworzenie warstwy [dao]
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// dane podatkowe są umieszczane w pamięci zakresu [application]
// metoda [TaxAdminData]->__toString zostanie wywołana niejawnie
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// zauważono błąd
doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
// zakończono
exit;
}
} else {
// dane podatkowe są pobierane z pamięci o zasięgu [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// instancjonowanie warstwy [dao]
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
// logi
$logger->write("données fiscales prises dans redis\n");
}
// utworzenie 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, $redis);
// koniec
exit;
function doInternalServerError(string $message, Response $response, array $infos,
Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $message: komunikat o błędzie
// $response: odpowiedź HTTP
// $infos: tabela informacji dotyczących wysyłania wiadomości e-mail
// $result: tabela wyników
// $logger: moduł logowania aplikacji
// $predisClient: klient [predis]
//
// wysyłamy wiadomość 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, $predisClient);
}
// funkcja wysyłania odpowiedzi HTTP do klienta
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response: odpowiedź HTTP
// $result: tabela wyników
// $statusCode: status odpowiedzi HTTP
// $headers: nagłówki HTTP do umieszczenia w odpowiedzi
// $logger: moduł rejestrujący aplikacji
// $predisClient: klient [predis]
//
// 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łka
$response->send();
// log
if ($logger != NULL) {
$logger->write("$body\n");
$logger->close();
}
// zamknięcie połączenia [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
Komentarze
- wiersz 15: klasie [\Application\ServerDaoWithSession] nadano alias [ServerDaoWithRedis], aby odzwierciedlić zmianę w implementacji skryptu serwera;
- wiersze 18–19: sesja jest zachowywana. Należy tutaj zapamiętać dwie informacje:
- fakt, że użytkownik pomyślnie się uwierzytelnił. Informacja ta ma zakres [session]: jest powiązana z konkretnym użytkownikiem i nie dotyczy innych użytkowników;
- dane administracji podatkowej. Informacja ta ma zakres [application]: nie jest powiązana z konkretnym użytkownikiem, ale dotyczy wszystkich użytkowników;
- wiersze 54–64: utworzenie klienta [redis], który będzie komunikował się z serwerem [redis]. Klient ten będzie komunikował się z domyślnym portem serwera. Gdyby serwer nie komunikował się na swoim domyślnym porcie lub nie znajdował się na komputerze o nazwie [localhost], należałoby przekazać te informacje konstruktorowi klasy [\Predis\Client];
- wiersz 59: natychmiast łączymy klienta z serwerem, aby sprawdzić, czy ten odpowiada;
- wiersze 60–65: jeśli połączenie z serwerem Redis nie powiedzie się, wysyłamy odpowiedź o błędzie do klienta, a do administratora aplikacji zostanie wysłana wiadomość e-mail;
- wiersz 67: wysyłamy zapytanie do serwera [redis] o klucz [taxAdminData]. Jeśli nie zostanie on znaleziony, dane podatkowe są pobierane z bazy danych (wiersz 72);
- wiersz 75: klucz [taxAdminData] jest umieszczany w pamięci [redis] powiązanej z ciągiem znaków jSON zmiennej [$taxAdminData], która jest obiektem typu [TaxAdminData]. Metoda [$redis→set] oczekuje ciągu znaków jako wartości klucza. Będzie więc próbowała przekształcić obiekt typu [TaxAdminData] na typ [string]. Wówczas w sposób domyślny zostanie wywołana metoda [TaxAdminData->__toString]. Metoda ta generuje ciąg znaków jSON dla obiektu [TaxAdminData];
- wiersz 84: klucz [taxAdminData] znajduje się w pamięci [redis], więc pobieramy jego wartość. Wiemy, że jest to ciąg jSON obiektu [TaxAdminData]. Następnie dekodujemy ten ciąg, aby uzyskać tablicę atrybutów;
- wiersz 85: na podstawie tej tablicy tworzony jest nowy obiekt [TaxAdminData];
- wiersz 87: instancja warstwy [dao] jest tworzona;
20.6. Kod klienta

Wersja 10 klienta jest identyczna z wersją 9. Zmianie ulega jedynie plik konfiguracyjny [config-client.json]:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10",
"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",
"/../version-09/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-10/impots-server.php"
}
Zmienia się jedynie, w wierszu 24, identyfikator serwera URL.
Wyniki są takie same jak w wersji 09. Przetestujmy po prostu nowy przypadek błędu:

Wynik w konsoli jest następujący:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"[redis], Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée. [tcp:\/\/127.0.0.1:6379]"}
Terminé
20.7. Testy [Codeception] po stronie klienta

Klasa testowa [ClientMetierTest] z wersji 10 jest identyczna z klasą z wersji 09, z jednym wyjątkiem:
<?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-10");
…
}
- wiersz 10: środowisko testowe odpowiada środowisku klienta w wersji 10;
Zanim rozpoczniemy testy, usuńmy za pomocą klienta [redis-cli] klucz [taxAdminData] z pamięci serwera [redis]:

Teraz uruchommy test:

Teraz przeanalizujmy logi serwera o numerze [logs.txt]:
05/07/19 08:52:16:396 :
---nouvelle requête
05/07/19 08:52:16:403 : Autentification en cours…
05/07/19 08:52:16:403 : Authentification réussie [admin, admin]
05/07/19 08:52:16:403 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
05/07/19 08:52:16:407 : données fiscales prises en base de données
05/07/19 08:52:16:420 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:546 :
---nouvelle requête
05/07/19 08:52:16:555 : Autentification en cours…
05/07/19 08:52:16:555 : Authentification réussie [admin, admin]
05/07/19 08:52:16:556 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
05/07/19 08:52:16:559 : données fiscales prises dans redis
05/07/19 08:52:16:559 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
05/07/19 08:52:16:668 :
---nouvelle requête
05/07/19 08:52:16:675 : Autentification en cours…
05/07/19 08:52:16:675 : Authentification réussie [admin, admin]
05/07/19 08:52:16:675 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
05/07/19 08:52:16:678 : données fiscales prises dans redis
05/07/19 08:52:16:678 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:776 :
---nouvelle requête
…
Jak już wspomniano, przy każdym teście ponownie uruchamiany jest konstruktor klasy testowej, co powoduje, że testowana klasa [ClientDao] jest przy każdym teście instancjonowana z nieistniejącym plikiem cookie sesji. Wszystko przebiega zatem tak, jakby 11 testów reprezentowało 11 różnych użytkowników z 11 różnymi sesjami.
- wiersz 6: dane podatkowe są pobierane z bazy danych;
- wiersze 13, 20: dane podatkowe są pobierane z pamięci [redis]. Mamy więc do czynienia z pamięcią o zakresie [application], współdzieloną przez wszystkich użytkowników aplikacji;
20.8. Interfejs internetowy serwera [Redis]
Widzieliśmy już, że serwer [Redis] może być zarządzany w trybie poleceń. Można nim również zarządzać za pomocą interfejsu internetowego:

- w [4], panelu administracyjnym URL;
- w [5] – klucze zapisane na serwerze;
- w [6] – aktualny stan serwera;
Klikając na [5], uzyskuje się informacje o kluczu [taxAdminData]:

- w [7] – URL, który zapewnia dostęp do informacji o kluczu [taxAdminData] [8];
- w [9] – status klucza;
- w [10] – jego wartość: rozpoznajemy ciąg znaków jSON jako obiekt typu [TaxAdminData];
- w [11] można usunąć klucz;
- w [12] można dodać kolejny klucz;