19. Anwendungsübung – Version 9
In dieser Version werden wir den Server wie folgt verbessern:
- Derzeit werden bei jeder Anfrage die Steuerverwaltungsdaten aus der Datenbank abgerufen. Wir werden eine Sitzung verwenden:
- Bei der ersten Anfrage eines Benutzers werden die Daten der Steuerbehörde aus der Datenbank abgerufen und in der Sitzung gespeichert;
- Bei nachfolgenden Anfragen desselben Benutzers werden die Daten der Steuerbehörde aus der Sitzung abgerufen. Da Datenbankabfragen ressourcenintensiv sind, ist mit einer leichten Verbesserung der Ausführungszeit zu rechnen;
- Der Server protokolliert wichtige Ereignisse in einer Textdatei:
- erfolgreiche oder fehlgeschlagene Authentifizierung;
- ob die vom Client gesendeten Parameter gültig sind;
- das Ergebnis der Steuerberechnung;
- verschiedene Fehlerfälle;
- im Falle eines schwerwiegenden Fehlers wird eine E-Mail an den Anwendungsadministrator gesendet;
Der Client muss außerdem so angepasst werden, dass er das an ihn gesendete Sitzungs-Cookie verarbeitet.
19.1. Der Server
Wir konzentrieren uns auf die serverseitige Komponente der Anwendung.

Diese Architektur wird durch die folgenden Skripte implementiert:

19.1.1. Hilfsprogramme

19.1.1.1. Die Klasse [Logger]
Die Klasse [Logger] wird verwendet, um Protokolle in eine Textdatei zu schreiben:
<?php
namespace Application;
class Logger {
// attribute
private $resource;
// manufacturer
public function __construct(string $logsFilename) {
// open file
$this->resource = fopen($logsFilename, "a");
if (!$this->resource) {
throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
}
}
// writing a message to the logs
public function write(string $message) {
fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
}
// close log file
public function close() {
fclose($this->resource);
}
}
Kommentare
- Zeile 7: die Logdatei-Ressource;
- Zeile 10: Der Klassenkonstruktor erhält den Namen der Protokolldatei als Parameter;
- Zeile 12: Die Textdatei wird im Anfüge-Modus (a+) geöffnet: Die Datei wird geöffnet und ihr Inhalt bleibt erhalten. Neue Daten werden hinter dem aktuellen Inhalt geschrieben;
- Zeilen 13–15: Wenn die Datei nicht geöffnet werden konnte, wird eine Ausnahme ausgelöst;
- Zeilen 19–21: Die Methode [write] schreibt die Meldung [$message] in die Protokolldatei, vorangestellt mit Datum und Uhrzeit;
- Zeilen 24–16: Die Methode [close] schließt die Protokolldatei;
Hinweis: Die Serveranwendung kann mehrere Clients gleichzeitig bedienen. Es gibt jedoch nur eine Protokolldatei für alle. Daher besteht beim Schreiben in die Datei die Gefahr von Parallelzugriffen. Schreibvorgänge müssen daher synchronisiert werden, um zu verhindern, dass sie durcheinander geraten. PHP stellt zu diesem Zweck Semaphoren [https://www.php.net/manual/fr/book.sem.php] zur Verfügung. Wir werden die Schreibsynchronisation hier außer Acht lassen, müssen uns dieses Problems jedoch bewusst sein.
19.1.1.2. Die Klasse [SendAdminMail]
Die Klasse [SendAdminMail] ermöglicht es Ihnen, im Falle eines Absturzes eine E-Mail an den Anwendungsadministrator zu senden:
<?php
namespace Application;
class SendAdminMail {
// attributes
private $config;
private $logger;
// manufacturer
public function __construct(array $config, Logger $logger = NULL) {
$this->config = $config;
$this->logger = $logger;
}
public function send() {
// sends $this->config['message'] to smtp server $this->config['smtp-server'] on port $infos[smt-port]
// if $this->config['tls'] is true, TLS support will be used
// mail is sent from $this->config['from']
// for recipient $this->config['to']
// message has subject $this->config['subject']
// attachments from $this->config['attachments'] are attached to the mail
// the result of the method
try {
// message creation
$message = (new \Swift_Message())
// message subject
->setSubject($this->config["subject"])
// sender
->setFrom($this->config["from"])
// recipients with a dictionary (setTo/setCc/setBcc)
->setTo($this->config["to"])
// message text
->setBody($this->config["message"])
;
// attachments
foreach ($this->config["attachments"] as $attachment) {
// path of attachment
$fileName = __DIR__ . $attachment;
// check that the file exists
if (file_exists($fileName)) {
// attach the document to the message
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
if ($this->logger !== NULL) {
// error
$this->logger->write("L'attachement [$fileName] n'existe pas\n");
}
}
}
// protocol TLS ?
if ($this->config["tls"] === "TRUE") {
// TLS
$transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
->setUsername($this->config["user"])
->setPassword($this->config["password"]);
} else {
// no TLS
$transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
}
// the shipment manager
$mailer = new \Swift_Mailer($transport);
// sending the message
$mailer->send($message);
// end
if ($this->logger !== NULL) {
$this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
}
} catch (\Throwable $ex) {
// error
if ($this->logger !== NULL) {
$this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
}
}
}
}
Kommentare
- Zeile 11: Der Konstruktor nimmt zwei Parameter entgegen:
- [$config]: ein assoziatives Array, das alle für den E-Mail-Versand erforderlichen Informationen enthält;
- [$logger]: ein Logger, der verwendet wird, um wichtige Momente während des E-Mail-Versandprozesses zu protokollieren;
Das assoziative Array hat folgende Form:
- Zeilen 16–76: Die Methode [send] wird zum Versenden der E-Mail verwendet. Dieser Code wurde im verlinkten Abschnitt vorgestellt und beschrieben;
19.1.2. Die [dao]-Schicht

Das Skript [ServeurDaoWithSession.php] lautet wie folgt:
<?php
// namespace
namespace Application;
// definition of a ImpotsWithDataInDatabase class
class ServerDaoWithSession extends ServerDao {
// manufacturer
public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
// simplest case
if ($taxAdminData !== NULL) {
$this->taxAdminData = $taxAdminData;
} else {
// hand over to parent class
parent::__construct($databaseFilename);
}
}
}
Kommentare
- Zeile 7: Die Klasse [ServerDaoWithSession] in Version 09 erweitert die Klasse [ServerDao] aus Version 08. Tatsächlich weiß die Klasse [ServerDao] bereits, wie die Datenbank zu verwenden ist. Es bleibt nur noch, den Fall zu behandeln, in dem die Daten der Steuerverwaltung bereits abgerufen wurden:
- Zeile 10: Der Konstruktor nimmt nun zwei Parameter entgegen:
- [string $databaseFilename]: Name der Datei, die die für die Verbindung zur Datenbank erforderlichen Informationen enthält, falls die Steuerverwaltungsdaten noch nicht abgerufen wurden, andernfalls NULL;
- [TaxAdminData $taxAdminData]: die Daten der Steuerverwaltung, sofern bereits abgerufen, andernfalls NULL;
Wenn eine Web-Sitzung beginnt, wird die [dao]-Schicht mit einem nicht-NULL-Objekt [$databaseFilename] und einem NULL-Objekt [taxAdminData] aufgebaut. Die Daten der Steuerbehörde werden dann aus der Datenbank abgerufen und in der Sitzung gespeichert. Bei nachfolgenden Anfragen innerhalb derselben Sitzung wird die [dao]-Schicht mit einem NULL-Objekt [databaseFilename] und einem aus der Sitzung abgerufenen [taxAdminData]-Objekt (das nicht NULL ist) aufgebaut. Daher findet keine Datenbankabfrage statt.
19.1.3. Das Server-Skript
Das Serverskript [impots-server.php] wird durch die folgende JSON-Datei [config-server.json] konfiguriert:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
"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",
"/Dao/ServerDaoWithSession.php",
"/../version-08/Métier/InterfaceServerMetier.php",
"/../version-08/Métier/ServerMetier.php",
"/Utilities/Logger.php",
"/Utilities/SendAdminMail.php"
],
"absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/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"
}
Das Server-Skript [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");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// session
$session = new Session();
$session->start();
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// log file creation
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// internal server error
doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
// completed
exit;
}
// 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")) {
// log
$logger->write("Autentification en cours…\n");
// authentication
…
}
// has the user been found?
if (!$trouvé) {
// not found - code 401 HTTP_UNAUTHORIZED
sendResponse(
$response,
["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
Response::HTTP_UNAUTHORIZED,
["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
$logger
);
// completed
exit;
} else {
// we note in the session that we have authenticated the user
$session->set("user", TRUE);
// log
$logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
}
} 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
…
// 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 have everything you need to work
// creation of the [dao] layer
if (!$session->has("taxAdminData")) {
// the 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 ServerDaoWithSession($config["databaseFilename"], NULL);
// put data in session
$session->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// we note the error
doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
// completed
exit;
}
} else {
// data are taken from the session
$dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
// logs
$logger->write("données fiscales prises en session\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);
// end
exit;
function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
// 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);
}
// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
// $response : answer HTTP
// $result: results table
// $statusCode: HTTP response status
// $headers: HTTP headers to be included in the response
// $logger: application logger
//
// 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();
}
}
Kommentare
- Zeilen 34–35: Starten einer Sitzung;
- Zeilen 38–40: eine JSON-Antwort vorbereiten;
- Zeilen 42–50: Versuch, die Protokolldatei zu erstellen. Tritt eine Ausnahme auf, wird die Methode [doInternalServer] (Zeilen 132–140) aufgerufen;
- Zeile 132: Die Methode [doInternalServer] akzeptiert vier Parameter:
- [$message]: die zu protokollierende Meldung. Muss in UTF-8 kodiert sein;
- [$response]: das [Response]-Objekt, das die Antwort des Servers an seinen Client kapselt;
- [$logger]: das für die Protokollierung verwendete [Logger]-Objekt;
- [$infos]: die Informationen, die zum Senden einer E-Mail an den Anwendungsadministrator verwendet werden;
- Zeilen 135–137: Es wird eine E-Mail an den Anwendungsadministrator gesendet;
- Zeile 139: Die Antwort wird an den Client gesendet:
- $response: HTTP-Antwort;
- $result: Der Server sendet die JSON-Zeichenkette aus dem Array [‘response’=>["error" => $message]];
- $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], Code 500;
- $headers: [], keine HTTP-Header, die der Antwort hinzugefügt werden sollen;
- $logger: der Anwendungslogger;
- Zeile 58: Dank der eingerichteten Sitzung authentifizieren wir den Client nur einmal:
- Sobald der Client authentifiziert ist, setzen wir einen [user]-Schlüssel in der Sitzung (Zeile 78);
- Bei der nächsten Anfrage desselben Clients verhindert Zeile 58 eine unnötige Authentifizierung;
- Zeile 103: Dank der eingerichteten Sitzung wird die Datenbank nur einmal durchsucht:
- Bei der ersten Anfrage wird die Datenbanksuche durchgeführt (Zeile 108). Die abgerufenen Daten werden dann in der Sitzung (Zeile 110) unter dem Schlüssel [taxAdminData] gespeichert;
- Bei nachfolgenden Anfragen wird der Schlüssel [taxAdminData] in der Sitzung gefunden (Zeile 103), und die Daten auf der Festplatte werden dann direkt an die [dao]-Schicht übergeben (Zeile 119);
- Zeilen 111–116: Die Suche nach Steuerdaten in der Datenbank kann fehlschlagen. In diesem Fall wird ein [500 Internal Server Error]-Code an den Client gesendet;
- Zeile 113: Die Fehlermeldung aus der MySQL-Treiberausnahme ist in ISO 8859-1 kodiert. Sie wird in UTF-8 konvertiert, um korrekt protokolliert zu werden;
- Der Rest des Codes ist fast identisch mit dem der vorherigen Version;
- Zeilen 143–164: Die Funktion [sendResponse] sendet alle Antworten an den Client;
- Zeilen 144–148: Bedeutung der Parameter;
- Zeile 153: Die Antwort ist immer die JSON-Zeichenkette eines Arrays [‘result’=>something];
- Zeile 156: Manchmal müssen der Antwort HTTP-Header hinzugefügt werden. Dies ist in Zeile 71 der Fall;
- Zeile 158: Die Antwort wird gesendet;
- Zeilen 160–163: Die Antwort wird protokolliert und der Logger wird geschlossen;
19.1.4. [Codeception] Tests

Wir werden nur die [dao]-Schicht testen, da sich nur diese geändert hat.
Der Testcode für [ServerDaoTest] lautet wie folgt:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// parent
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
- Zeilen 9–24: Wir erstellen eine Laufzeitumgebung, die mit der des Server-Skripts [impots-server] identisch ist;
- Zeile 38: Um die [dao]-Schicht zu erstellen, instanziieren wir die Klasse [ServerDaoWithSession];
Die Testergebnisse lauten wie folgt:

19.2. Der Client
Wir konzentrieren uns auf die Client-Seite der Anwendung.

Diese Architektur wird durch die folgenden Skripte implementiert:

In der neuen Version gibt es lediglich folgende Änderungen:
- die Konfigurationsdatei [config-client.json];
- die [dao]-Schicht des Clients;
19.2.1. Die [dao]-Schicht
Die [Dao]-Schicht entwickelt sich wie folgt:
<?php
namespace Application;
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// using a Trait
use TraitDao;
// attributes
private $urlServer;
private $user;
private $sessionCookie;
// manufacturer
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// session cookie ?
if (!$this->sessionCookie) {
// create a HTTP customer
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// make the request to the server without a session cookie
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]
]);
} else {
// make a request to the server with the session cookie
// create a HTTP customer
$httpClient = HttpClient::create([
"verify_peer" => false
]);
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"headers" => ["Cookie" => $this->sessionCookie]
]);
}
// the answer is retrieved
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logs
print "$json=json\n";
// retrieve response status
$statusCode = $response->getStatusCode();
// mistake?
if ($statusCode !== 200) {
// we have an error - we throw an exception
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// retrieve the session cookie
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// session cookie ?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// we return the answer
return $réponse;
}
}
Kommentare
Die Änderung an der [dao]-Schicht umfasst nun die Verwaltung einer Sitzung:
- Zeile 14: das Sitzungs-Cookie;
- Zeilen 25–39: Bei der ersten Anfrage existiert dieses Cookie noch nicht; daher senden wir die Anfrage an den Server, indem wir die Authentifizierungsinformationen übermitteln (Zeile 28);
- Zeilen 40–53: Bei nachfolgenden Anfragen verfügen wir normalerweise über das Sitzungscookie. Wir senden daher die Authentifizierungsinformationen nicht (Zeilen 42–44);
- Zeilen 69–82: Die Antwort des Servers auf die erste Anfrage enthält ein Sitzungscookie. Wir rufen es ab. Dieser Code wurde bereits im verlinkten Abschnitt verwendet und erläutert;
- Zeile 78: Das abgerufene Sitzungs-Cookie wird im Klassenattribut [$sessionCookie] gespeichert;
Hinweis: Wir hätten die alte Version der [dao]-Schicht beibehalten und bei jeder Anfrage eine Authentifizierung durchführen können, da der Aufwand vernachlässigbar ist. Zu Lehrzwecken wollten wir jedoch zeigen, wie ein HTTP-Client eine Sitzung verwalten kann.
19.2.2. Die Konfigurationsdatei
Die JSON-Konfigurationsdatei entwickelt sich wie folgt:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
"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",
"/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-09/impots-server.php"
}
Nur die URL in Zeile 24 ändert sich.
19.3. Einige Tests
19.3.1. Test 1
Zunächst führen wir den Client in einer fehlerfreien Umgebung aus. Die Ergebnisse sind dieselben wie in früheren Versionen. Auf der Serverseite haben wir nun jedoch eine Protokolldatei [logs.txt]:
04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours…
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session…
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session…
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête
…
- Zeilen 3–7: Bei der ersten Anfrage erfolgt die Authentifizierung und die Daten werden aus der Datenbank abgerufen;
- Zeilen 9–14: Bei der nächsten Anfrage findet keine weitere Authentifizierung statt und die Daten werden aus der Sitzung abgerufen. Dies wiederholt sich bei nachfolgenden Anfragen (Zeile 15 und weiter);
19.3.2. Test 2
Schalten wir nun die MySQL-Datenbank ab. Auf der Client-Seite erhalten wir folgende Konsolenausgabe:
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é
Auf der Serverseite haben wir die folgenden Protokolle [logs.txt]:
04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête
…
Um die vom Anwendungsadministrator empfangene E-Mail abzurufen, verwenden wir das Skript [imap-03.php] aus dem Abschnitt, der mit der folgenden Konfigurationsdatei [config-imap-01.json] verknüpft ist:
Es ergibt sich folgendes Ergebnis:

Die Datei [message_1.txt] enthält den folgenden Text:
return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable
SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
19.3.3. Test 3
Stellen wir nun sicher, dass die Datei [logs.txt] nicht erstellt werden kann. Erstellen Sie dazu einfach einen Ordner [logs.txt]:

Sobald das erledigt ist, starten wir den Client.
Auf der Client-Seite erhalten wir die folgende Konsolenausgabe:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé
Auf der Serverseite sind keine Protokolle vorhanden, aber der Administrator erhält die folgende E-Mail:
return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable
Echec lors de la création du fichier de logs [Data/logs.txt]
19.3.4. Test 4
Geben wir diesmal im Client-Konfigurationsdatei falsche Anmeldedaten für den verbindenden Client an.
Der Client zeigt die folgende Konsolenausgabe an:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
Auf der Serverseite erscheinen folgende Protokolleinträge:
---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}
19.3.5. Test 5
Tragen wir den korrekten Benutzernamen und das Passwort [admin, admin] wieder in die Client-Konfigurationsdatei ein.
Rufen wir nun die URL des Servers [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] direkt in einem Browser auf, ohne Parameter zu übergeben:
In der Logdatei des Servers [logs.txt] sehen wir die folgenden Zeilen:
---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}
19.4. Tests [Codeception]
Wie bei früheren Versionen werden wir auch für Version 09 [Codeception]-Tests schreiben.

19.4.0.1. [Business]-Layer-Test
Der Test [ClientMetierTest.php] sieht wie folgt aus:
<?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-09");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// uses
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;
// test class
class ClientMetierTest extends Unit {
…
}
Kommentare
- Im Vergleich zur Testklasse in Version 08 besteht die einzige Änderung in Zeile 10, in der das Stammverzeichnis des zu testenden Clients angegeben wird;
Die Testergebnisse lauten wie folgt:

Es lohnt sich, die Serverprotokolle [logs.txt] zu überprüfen:
04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête
…
---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}
Wir sehen, dass die Daten der Steuerbehörde immer aus der Datenbank und niemals aus der Sitzung abgerufen werden. Kehren wir zum Code des ausgeführten Tests zurück:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
…
// test class
class ClientMetierTest extends Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$this->métier = new ClientMetier($clientDao);
}
// tests
public function test1() {
…
}
public function test2() {
…
}
public function test3() {
…
}
…
}
In einer [Codeception]-Testklasse wird der Konstruktor für jeden Test ausgeführt.
- Zeile 21: Daher wird für jeden Test ein neues [ClientDao] mit einem NULL-Session-Cookie erstellt. Dies erklärt, warum dieser Client nicht von einer Session profitiert;
Dieses Beispiel zeigt uns, dass die Sitzung nicht der richtige Ort ist, um Daten der Steuerverwaltung zu speichern. Tatsächlich werden diese Daten von allen Benutzern der Anwendung gemeinsam genutzt. Hier werden sie jedoch in jeder ihrer Sitzungen dupliziert.
In der Webprogrammierung gibt es drei Arten der Sichtbarkeit für gemeinsam genutzte Daten:
- Daten, die von allen Benutzern der Webanwendung gemeinsam genutzt werden. Dabei handelt es sich in der Regel um schreibgeschützte Daten. PHP unterstützt diese Art der Speicherung nicht nativ;
- Daten, die über Anfragen desselben Clients hinweg gemeinsam genutzt werden. Diese Daten werden in der Sitzung gespeichert. Wir bezeichnen dies als Client-Sitzung, um den Speicher des Clients zu kennzeichnen. Alle Anfragen eines Clients haben Zugriff auf diese Sitzung. Sie können dort Informationen speichern und lesen. In den vorherigen Skripten wird diese Sitzung durch das Symfony-Objekt [HttpFoundation\Session\Session] implementiert;
- Der Anforderungsspeicher oder Anforderungskontext. Die Anforderung eines Benutzers kann durch mehrere aufeinanderfolgende Aktionen verarbeitet werden. Der Anforderungskontext ermöglicht es Aktion 1, Informationen an Aktion 2 weiterzugeben. In den vorherigen Skripten wird die Anforderung durch das Symfony-Objekt [HttpFoundation\Request] und deren Speicher durch das Attribut [HttpFoundation\Request::attributes] implementiert;

Es gibt Bibliotheken von Drittanbietern, die PHP mit einem Anwendungsstatus versorgen. Die neue Version der Anwendungsübung demonstriert die Verwendung einer davon.