Skip to content

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:

Image

20.2. Instalacja Redis

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

Image

  • 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:

Image

20.3. Klient Redis w trybie poleceń

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

Image

  • 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:

Image

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:

Image

20.5. Kod serwera

Image

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

Image

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:

Image

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

Image

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]:

Image

Teraz uruchommy test:

Image

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:

Image

  • 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]:

Image

  • 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;