Skip to content

20. Anwendungsübung – Version 10

Die vorherige Version hat gezeigt, dass die Steuerdaten, die von allen Nutzern der Anwendung gemeinsam genutzt werden, in einem Speicher mit Bereich [Application] gespeichert werden sollten. Wir werden einen Redis-Server [https://redis.io] verwenden, um dies zu implementieren.

20.1. Redis

Der Speicher mit dem Gültigkeitsbereich [Application] wird durch einen Redis-Server implementiert. Die Skripte PHP, die diesen Anwendungsspeicher benötigen, sind Clients dieses Servers:

Image

20.2. Installation von Redis

Laragon wird standardmäßig mit einem deaktivierten Redis-Server ausgeliefert. Dieser muss daher zunächst aktiviert werden:

Image

  • In [3] den Server [Redis] aktivieren;
  • in [4] den Port [6379] beibehalten, den Redis-Clients standardmäßig verwenden;

Die Laragon-Dienste werden nach der Aktivierung von Redis automatisch neu gestartet:

Image

20.3. Der Redis-Client im Befehlsmodus

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

Image

  • Unter [1] startet der Befehl [redis-cli] den Client im Befehlsmodus des Redis-Servers;

Im Juli 2019 stehen dem Redis-Client 172 Befehle zur Kommunikation mit dem Server [https://redis.io/commands#list] zur Verfügung. Einer davon, [command count] [2], zeigt diese Zahl [3] an.

Wir werden hier nur diejenigen vorstellen, die wir in unserer Anwendung PHP benötigen. Wir werden Redis für einen einzigen Zweck verwenden: das Speichern eines Arrays [‘attribut’=>’valeur’] im Redis-Speicher. Dies geschieht mit dem Redis-Befehl [set attribut valeur] [4]. Der Wert kann anschließend mit dem Befehl [get attribut] [5] abgerufen werden. Das ist alles, was wir benötigen.

Es kann erforderlich sein, den Redis-Speicher zu leeren. Dies geschieht mit dem Befehl [flushdb] [6]. Wenn man anschließend den Wert des Attributs [titre] [7] abfragt, erhält man eine Referenz [nil] [8], die darauf hinweist, dass das Attribut nicht gefunden wurde. Man kann auch den Befehl [exists] [9-10] verwenden, um zu überprüfen, ob ein Attribut vorhanden ist.

Um den Redis-Client zu beenden, geben Sie den Befehl [quit] [11] ein.

20.4. Installation eines Redis-Clients für PHP

Nun müssen wir einen Redis-Client für PHP installieren:

Image

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

Image

20.5. Server-Code

Image

Die Konfigurationsdatei [config-server.json] entwickelt 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"
}

Anmerkungen

  • Zeilen 5–15: Version 10 bringt außer dem Skript [impots-server.php] keine Neuerungen mit sich. Sie verwendet Elemente aus den Versionen 08 und 09;
  • Zeile 19: eine erforderliche Abhängigkeit zur Bibliothek [predis], die gerade installiert wurde;

Der Code des Servers [impots-server.php] entwickelt sich wie folgt:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);

// Namensraum
namespace Application;

// Fehlerbehandlung durch PHP
ini_set("display_errors", "0");
//
// Pfad zur Konfigurationsdatei
define("CONFIG_FILENAME", "Data/config-server.json");
// Klassenalias
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

// Sitzung
$session = new Session();
$session->start();


// 1. Protokoll
$logger->write("\n---nouvelle requête\n");

// die aktuelle Anfrage wird abgerufen
$request = Request::createFromGlobals();
// Authentifizierung nur beim ersten Mal
if (!$session->has("user")) {

} else {
  // Protokoll
  $logger->write("Authentification prise en session…\n");
}

// Es liegt ein gültiger Benutzer vor – die empfangenen Parameter werden überprüft
$erreurs = [];
// Es müssen drei Parameter vorhanden sein GET
$method = strtolower($request->getMethod());


// Fehler?
if ($erreurs) {
// Es wird ein Fehlercode 400 HTTP_BAD_REQUEST an den Client gesendet
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // abgeschlossen
  exit;
} else {
  // Protokolle
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}

// Wir haben alles, was wir zum Arbeiten brauchen
// Redis
\Predis\Autoloader::register();
try {
  // Client [predis]
  $redis = new \Predis\Client();
  // Wir stellen eine Verbindung zum Server her, um zu prüfen, ob er erreichbar ist
  $redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
  // Interner Serverfehler
  doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
  // abgeschlossen
  exit;
}

// Erstellung der Ebene [dao]
if (!$redis->get("taxAdminData")) {
  // Die Steuerdaten werden aus der Datenbank abgerufen
  $logger->write("données fiscales prises en base de données\n");
  try {
    // Erstellung der Ebene [dao]
    $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
    // Die Steuerdaten werden in den Arbeitsspeicher [application] geladen
    // Die Methode [TaxAdminData]->__toString wird implizit aufgerufen
    $redis->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // Der Fehler wird vermerkt
    doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
    // abgeschlossen
    exit;
  }
} else {
  // Die Steuerdaten werden aus dem Bereichsspeicher [application] entnommen
  $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
  $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
  // Instanziierung der Schicht [dao]
  $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
  // Protokolle
  $logger->write("données fiscales prises dans redis\n");
}
// Erstellung der Ebene [métier]
$métier = new ServerMetier($dao);
// Steuerberechnung
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// die Antwort wird ausgegeben
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// Ende
exit;

function doInternalServerError(string $message, Response $response, array $infos,
  Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $message: die Fehlermeldung
  // $response: Antwort HTTP
  // $infos: Informationstabelle für den E-Mail-Versand
  // $result: Ergebnistabelle
  // $logger: das Protokoll der Anwendung
  // $predisClient: ein Client [predis]
  //
  // Es wird eine E-Mail an den Administrator gesendet
  // SendAdminMail fängt alle Ausnahmen ab und protokolliert sie selbst
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // Es wird ein 500-Fehlercode an den Client gesendet
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}

// Funktion zum Senden der Antwort HTTP an den Kunden
function sendResponse(Response $response, array $result, int $statusCode,
  array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $response: Antwort HTTP
  // $result: Ergebnistabelle
  // $statusCode: Status HTTP der Antwort
  // $headers: HTTP-Kopfzeilen, die in die Antwort aufgenommen werden sollen
  // $logger: der Logger der Anwendung
  // $predisClient: ein Client [predis]
  //
  // Status HTTTP
  $response->setStatusCode($statusCode);
  // Hauptteil
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // Header
  $response->headers->add($headers);
  // Versand
  $response->send();
  // Protokoll
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
  // Verbindung schließen [redis]
  if ($predisClient != NULL) {
    $predisClient->disconnect();
  }
}

Kommentare

  • Zeile 15: Der Klasse [\Application\ServerDaoWithSession] wird der Alias [ServerDaoWithRedis] zugewiesen, um die geänderte Implementierung des Serverskripts widerzuspiegeln;
  • Zeilen 18–19: Die Sitzung wird beibehalten. Hier müssen zwei Informationen gespeichert werden:
    • 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 Daten der Steuerbehörde. Diese Information hat den Geltungsbereich [application]: Sie ist nicht an einen bestimmten Benutzer gebunden, gilt jedoch für alle Benutzer;
  • Zeilen 54–64: Erstellung des Clients [redis], der mit dem Server [redis] kommunizieren wird. Dieser Client wird mit dem Standardport des Servers kommunizieren. Sollte dieser nicht über seinen Standardport kommunizieren oder sich nicht auf dem Rechner [localhost] 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 Administrator der Anwendung verschickt;
  • Zeile 67: Der Server [redis] wird nach dem Schlüssel [taxAdminData] abgefragt. Wird dieser nicht gefunden, werden die Steuerdaten aus der Datenbank abgerufen (Zeile 72);
  • Zeile 75: Der Schlüssel [taxAdminData] wird im Speicher [redis] abgelegt, der der Zeichenkette jSON der Variablen [$taxAdminData] zugeordnet ist, bei der es sich um ein Objekt vom Typ QZXW2HTMLP005328Z handeltQX. Die Methode [$redis→set] erwartet eine Zeichenkette als Schlüsselwert. Sie wird daher versuchen, das Objekt vom Typ [TaxAdminData] in den Typ [string] umzuwandeln. In diesem Fall wird implizit die Methode [TaxAdminData->__toString] aufgerufen. Diese erzeugt die Zeichenkette jSON aus dem Objekt [TaxAdminData];
  • Zeile 84: Der Schlüssel [taxAdminData] befindet sich im Speicher [redis], daher wird sein Wert abgerufen. Wir wissen, dass es sich um die Zeichenkette jSON eines Objekts [TaxAdminData] handelt. Diese wird dann dekodiert, um ein Array von Attributen zu erhalten;
  • Zeile 85: Aus diesem Array wird ein neues Objekt [TaxAdminData] instanziiert;
  • Zeile 87: Die Schicht [dao] wird instanziiert;

20.6. Client-Code

Image

Die Version 10 des Clients ist identisch mit Version 9. Es ändert sich lediglich 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"
}

Es ändert sich lediglich in Zeile 24 die Server-ID URL.

Die Ergebnisse sind dieselben wie in Version 09. Testen wir einfach einen neuen Fehlerfall:

Image

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. Tests mit [Codeception] auf dem Client

Image

Die Testklasse [ClientMetierTest] der Version 10 ist bis auf eine Ausnahme identisch mit der der Version 09:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);

// Namensraum
namespace Application;

// Definition von Konstanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");



}
  • Zeile 10: Die Testumgebung ist die des Clients der Version 10;

Bevor wir mit den Tests beginnen, löschen wir mithilfe des Clients [redis-cli] den Schlüssel [taxAdminData] aus dem Speicher des Servers [redis]:

Image

Führen wir nun den Test durch:

Image

Sehen wir uns nun die Protokolle [logs.txt] des Servers 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

Wie bereits erwähnt, wird bei jedem Test der Konstruktor der Testklasse erneut ausgeführt, was dazu führt, dass die getestete Klasse [ClientDao] bei jedem Test mit einem nicht vorhandenen Session-Cookie instanziiert wird. Es ist also so, als würden die 11 Tests 11 verschiedene Benutzer mit 11 verschiedenen Sitzungen darstellen.

  • Zeile 6: Die Steuerdaten werden aus der Datenbank abgerufen;
  • Zeilen 13, 20: Die Steuerdaten werden aus dem Speicher [redis] abgerufen. Es handelt sich hier also um einen Speicher mit dem Gültigkeitsbereich [application], der von allen Benutzern der Anwendung gemeinsam genutzt wird;

20.8. Webschnittstelle des Servers [Redis]

Wir haben gesehen, dass der Server [Redis] im Befehlsmodus verwaltet werden kann. Er kann auch über eine Webschnittstelle verwaltet werden:

Image

  • in [4], dem Verwaltungsserver URL;
  • in [5] die vom Server gespeicherten Schlüssel;
  • in [6], den aktuellen Status des Servers;

Wenn man auf „[5]“ klickt, erhält man Informationen zum Schlüssel „[taxAdminData]“:

Image

  • in [7], den Schlüssel URL, der Zugriff auf die Informationen der Schlüssel [taxAdminData] und [8] gewährt;
  • in [9], den Status des Schlüssels;
  • in [10], dessen Wert: Man erkennt die Zeichenfolge jSON eines Objekts vom Typ [TaxAdminData];
  • in [11] kann der Schlüssel gelöscht werden;
  • in [12] kann man einen weiteren hinzufügen;