20. Anwendungsübung – Version 10
Die vorherige Version zeigte, dass Steuerdaten, die von allen Benutzern der Anwendung gemeinsam genutzt werden, in einem Cache im [Application]-Scope gespeichert werden sollten. Wir werden einen Redis-Server [https://redis.io] verwenden, um dies zu implementieren.
20.1. Redis
Der Speicher im [Application]-Bereich wird durch einen Redis-Server implementiert. Die PHP-Skripte, die diesen Anwendungsspeicher benötigen, werden Clients dieses Servers sein:

20.2. Redis installieren
Laragon enthält einen Redis-Server, der standardmäßig nicht aktiviert ist. Sie müssen ihn daher zunächst aktivieren:

- Aktivieren Sie in [3] den [Redis]-Server;
- Lassen Sie in [4] den Port [6379] als Standardport für Redis-Clients stehen;
Die Laragon-Dienste werden nach der Aktivierung von Redis automatisch neu gestartet:

20.3. Der Redis-Client im Befehlsmodus
Der Redis-Server kann im Befehlsmodus abgefragt werden. Öffnen Sie ein Laragon-Terminal (siehe Abschnitt „Links“):

- In [1] startet der Befehl [redis-cli] den Client im Befehlsmodus für den Redis-Server;
Seit Juli 2019 unterstützt der Redis-Client 172 Befehle für die Interaktion mit dem Server [https://redis.io/commands#list]. Einer davon [Befehlszahl] [2] zeigt diese Zahl an [3].
Wir werden nur diejenigen behandeln, die wir für unsere PHP-Anwendung benötigen. Wir werden Redis für einen einzigen Zweck verwenden: das Speichern eines Arrays [‘attribute’=>’value’] im Redis-Speicher. Dies geschieht mit dem Redis-Befehl [set attribute value] [4]. Der Wert kann dann mit dem Befehl [get attribute] [5] abgerufen werden. Das ist alles, was wir brauchen.
Es kann erforderlich sein, den Speicher von Redis zu leeren. Dies geschieht mit dem Befehl [flushdb] [6]. Wenn wir dann den Wert des Attributs [title] abfragen [7], erhalten wir eine [nil]-Referenz [8], die anzeigt, dass das Attribut nicht gefunden wurde. Wir können auch den Befehl [exists] [9-10] verwenden, um zu prüfen, ob ein Attribut existiert.
Um den Redis-Client zu beenden, geben Sie den Befehl [quit] [11] ein.
20.4. Installation eines Redis-Clients für PHP
Wir müssen nun einen Redis-Client für PHP installieren:

Es gibt mehrere Bibliotheken, die einen Redis-Client implementieren. Wir werden die [Predis]-Bibliothek [https://github.com/nrk/predis] (Juli 2019) verwenden. Wie die vorherigen wird auch diese mit [composer] in einem Laragon-Terminal installiert:

20.5. Server-Code

Die Konfigurationsdatei [config-server.json] ändert sich wie folgt:
{
"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"
}
Kommentare
- Zeilen 5–15: Version 10 führt außer dem Skript [impots-server.php] keine Neuerungen ein. Es verwendet Elemente aus den Versionen 08 und 09;
- Zeile 19: eine Abhängigkeit, die für die soeben installierte [predis]-Bibliothek erforderlich ist;
Der Servercode [impots-server.php] ändert sich wie folgt:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// error handling by PHP
ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
// class alias
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
// session
$session = new Session();
$session->start();
…
…
// 1st log
$logger->write("\n---nouvelle requête\n");
// retrieve the current query
$request = Request::createFromGlobals();
// authentication only the 1st time
if (!$session->has("user")) {
…
} else {
// log
$logger->write("Authentification prise en session…\n");
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());
…
// mistakes?
if ($erreurs) {
// an error code 400 HTTP_BAD_REQUEST is sent to the customer
sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
// completed
exit;
} else {
// logs
$logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// we've got everything you need to work
// Redis
\Predis\Autoloader::register();
try {
// customer [predis]
$redis = new \Predis\Client();
// connect to the server to see if it's there
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// internal server error
doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
// completed
exit;
}
// creation of the [dao] layer
if (!$redis->get("taxAdminData")) {
// tax data is taken from the database
$logger->write("données fiscales prises en base de données\n");
try {
// construction of the [dao] layer
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// put tax data in scope memory [application]
// method [TaxAdminData]->__toString will be called implicitly
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// we note the error
doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
// completed
exit;
}
} else {
// tax data are taken from the [application] scope memory
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// istanciation of the [dao] layer
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
// logs
$logger->write("données fiscales prises dans redis\n");
}
// creation of the [business] layer
$métier = new ServerMetier($dao);
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// end
exit;
function doInternalServerError(string $message, Response $response, array $infos,
Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $message: error message
// $response : answer HTTP
// $infos: information table for sending mail
// $result: results table
// $logger: application logger
// $predisClient: a customer [predis]
//
// send an e-mail to the administrator
// SendAdminMail intercepts all exceptions and logs them itself
$infos['message'] = $message;
$sendAdminMail = new SendAdminMail($infos, $logger);
$sendAdminMail->send();
// an error code 500 is sent to the customer
sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}
// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response : answer HTTP
// $result: results table
// $statusCode: HTTP response status
// $headers: HTTP headers to be included in the response
// $logger: application logger
// $predisClient: a customer [predis]
//
// status HTTTP
$response->setStatusCode($statusCode);
// body
$body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$response->setContent($body);
// headers
$response->headers->add($headers);
// shipping
$response->send();
// log
if ($logger != NULL) {
$logger->write("$body\n");
$logger->close();
}
// close connection [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
Kommentare
- Zeile 15: Wir weisen der Klasse [\Application\ServerDaoWithSession] den Alias [ServerDaoWithRedis] zu, um die Änderung in der Implementierung des Server-Skripts widerzuspiegeln;
- Zeilen 18–19: Die Sitzung wird aufrechterhalten. Hier müssen wir zwei Informationen im Auge behalten:
- die Tatsache, dass sich der Benutzer erfolgreich authentifiziert hat. Diese Information hat den Geltungsbereich [session]: Sie ist an einen bestimmten Benutzer gebunden und gilt nicht für andere Benutzer;
- die Steuerverwaltungsdaten. Diese Informationen haben den Geltungsbereich [application]: Sie sind nicht an einen bestimmten Benutzer gebunden, sondern gelten für alle Benutzer;
- Zeilen 54–64: Erstellung des [redis]-Clients, der mit dem [redis]-Server kommunizieren wird. Dieser Client kommuniziert über den Standardport des Servers. Würde der Server nicht über seinen Standardport kommunizieren oder sich nicht auf dem [localhost]-Rechner befinden, müssten diese Informationen an den Konstruktor der Klasse [\Predis\Client] übergeben werden;
- Zeile 59: Der Client wird sofort mit dem Server verbunden, um zu prüfen, ob dieser antwortet;
- Zeilen 60–65: Wenn die Verbindung zum Redis-Server fehlschlägt, wird eine Fehlermeldung an den Client gesendet und eine E-Mail an den Anwendungsadministrator verschickt;
- Zeile 67: Wir fragen den [redis]-Server nach dem Schlüssel [taxAdminData] ab. Wird dieser nicht gefunden, werden die Steuerdaten aus der Datenbank abgerufen (Zeile 72);
- Zeile 75: Der Schlüssel [taxAdminData] wird zusammen mit der JSON-Zeichenkette der Variablen [$taxAdminData], die ein Objekt vom Typ [TaxAdminData] ist, im [redis]-Speicher abgelegt. Die Methode [$redis→set] erwartet eine Zeichenkette als Wert für den Schlüssel. Sie versucht daher, das [TaxAdminData]-Objekt in einen [string]-Typ zu konvertieren. Dies ruft implizit die Methode [TaxAdminData->__toString] auf, die die JSON-Zeichenkette des [TaxAdminData]-Objekts erzeugt;
- Zeile 84: Der Schlüssel [taxAdminData] befindet sich im [redis]-Speicher, daher rufen wir seinen Wert ab. Wir wissen, dass es sich dabei um die JSON-Zeichenkette eines [TaxAdminData]-Objekts handelt. Diese parsen wir anschließend, um ein Array von Attributen zu erhalten;
- Zeile 85: Aus diesem Array wird ein neues [TaxAdminData]-Objekt instanziiert;
- Zeile 87: Die [dao]-Schicht wird instanziiert;
20.6. Client-Code

Client-Version 10 ist identisch mit Version 9. Die einzige Änderung betrifft die Konfigurationsdatei [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"
}
Die einzige Änderung betrifft die Server-URL in Zeile 24.
Die Ergebnisse sind dieselben wie in Version 09. Testen wir einfach einen neuen Fehlerfall:

Das Ergebnis in der Konsole lautet wie folgt:
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. [Codeception] Client-Tests

Die Testklasse [ClientMetierTest] in Version 10 ist mit einer Ausnahme identisch mit der in Version 09:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");
…
}
- Zeile 10: Die Testumgebung entspricht der des Clients der Version 10;
Bevor wir mit den Tests beginnen, löschen wir mit dem [redis-cli]-Client den Schlüssel [taxAdminData] aus dem Speicher des [redis]-Servers:

Führen wir nun den Test aus:

Sehen wir uns nun die Serverprotokolle [logs.txt] an:
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
…
Wir haben bereits erwähnt, dass für jeden Test der Konstruktor der Testklasse erneut ausgeführt wird, was bedeutet, dass die zu testende Klasse [ClientDao] für jeden Test mit einem nicht vorhandenen Sitzungscookie instanziiert wird. Alles läuft daher so ab, als ob die 11 Tests 11 verschiedene Benutzer mit 11 verschiedenen Sitzungen darstellen würden.
- Zeile 6: Steuerdaten werden aus der Datenbank abgerufen;
- Zeilen 13, 20: Steuerdaten werden aus dem [Redis]-Speicher abgerufen. Wir haben also einen Speicher im [application]-Bereich, der von allen Benutzern der Anwendung gemeinsam genutzt wird;
20.8. [Redis]-Server-Webschnittstelle
Wir haben gesehen, dass der [Redis]-Server im Befehlsmodus verwaltet werden kann. Er kann auch über eine Webschnittstelle verwaltet werden:

- in [4], die Verwaltungs-URL;
- in [5] die vom Server gespeicherten Schlüssel;
- in [6] der aktuelle Serverstatus;
Durch Klicken auf [5] können Sie Informationen zum Schlüssel [taxAdminData] anzeigen:

- in [7] die URL, über die Sie auf die Informationen zum Schlüssel [taxAdminData] zugreifen können [8];
- in [9] den Status des Schlüssels;
- in [10] dessen Wert: Sie erkennen die JSON-Zeichenkette eines [TaxAdminData]-Objekts;
- In [11] können Sie den Schlüssel löschen;
- in [12] können Sie einen weiteren hinzufügen;