19. Toepassingsopdracht – versie 9
In deze versie gaan we de server als volgt verbeteren:
- momenteel worden bij elk verzoek de gegevens van de belastingdienst in de database opgezocht. We gaan een sessie gebruiken:
- bij het eerste verzoek van een gebruiker worden de gegevens van de belastingdienst opgezocht in de database en in de sessie opgeslagen;
- bij volgende verzoeken van dezelfde gebruiker worden de gegevens van de belastingdienst uit de sessie opgehaald. We kunnen een lichte verbetering in de uitvoeringstijd verwachten, aangezien databaseverzoeken veel rekenkracht vergen;
- de server registreert de belangrijke momenten in een tekstbestand:
- de geslaagde of mislukte authenticatie;
- of de door de klant verzonden parameters geldig zijn of niet;
- het resultaat van de belastingberekening;
- de verschillende foutgevallen;
- in geval van een fatale fout wordt er een e-mail verzonden naar de beheerder van de applicatie;
Ook de client moet worden aangepast om de sessiecookie te beheren die we naar de client zullen sturen.
19.1. De server
We richten ons op het servergedeelte van de applicatie.

Deze architectuur wordt geïmplementeerd door de volgende scripts:

19.1.1. Hulpprogramma's

19.1.1.1. De klasse [Logger]
De klasse [Logger] wordt gebruikt om logbestanden naar een tekstbestand te schrijven:
<?php
namespace Application;
class Logger {
// attribuut
private $resource;
// constructor
public function __construct(string $logsFilename) {
// het bestand openen
$this->resource = fopen($logsFilename, "a");
if (!$this->resource) {
throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
}
}
// een bericht in de logbestanden schrijven
public function write(string $message) {
fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
}
// logbestand sluiten
public function close() {
fclose($this->resource);
}
}
Opmerkingen
- regel 7: de bron van het logbestand;
- regel 10: de constructor van de klasse ontvangt als parameter de naam van het logbestand;
- regel 12: het tekstbestand wordt geopend in de toevoegmodus (a+): het bestand wordt geopend en de inhoud blijft behouden. Er wordt achter de huidige inhoud geschreven;
- regels 13-15: als het openen niet is gelukt, wordt er een uitzondering gegenereerd;
- regels 19-21: met de methode [write] kan het bericht [$message] in het logbestand worden geschreven, voorafgegaan door de datum en de tijd;
- regels 24-16: met de methode [close] kan het logbestand worden gesloten;
Opmerking: de serverapplicatie kan meerdere clients tegelijkertijd bedienen. Er is echter slechts één logbestand voor alle clients. Er bestaat dus een risico op gelijktijdige toegang om in het bestand te schrijven. De schrijfbewerkingen moeten daarom worden gesynchroniseerd om te voorkomen dat ze door elkaar raken. Hiervoor beschikt PHP over semaforen [https://www.php.net/manual/fr/book.sem.php]. We zullen de synchronisatie van de schrijfbewerkingen hier buiten beschouwing laten, maar men moet zich wel bewust blijven van het probleem.
19.1.1.2. De klasse [SendAdminMail]
Met de klasse [SendAdminMail] kan een e-mail naar de beheerder van de applicatie worden verzonden als deze crasht:
<?php
namespace Application;
class SendAdminMail {
// attributen
private $config;
private $logger;
// constructor
public function __construct(array $config, Logger $logger = NULL) {
$this->config = $config;
$this->logger = $logger;
}
public function send() {
// verzendt $this->config['message'] naar de SMTP-server $this->config['smtp-server'] op poort $infos[smt-port]
// als $this->config['tls'] waar is, wordt de ondersteuning TLS gebruikt
// de e-mail wordt verzonden namens $this->config['from']
// voor de ontvanger $this->config['to']
// het bericht heeft als onderwerp $this->config['subject']
// aan de e-mail worden de bijlagen van $this->config['attachments'] toegevoegd
// het resultaat van de methode
try {
// het bericht wordt aangemaakt
$message = (new \Swift_Message())
// onderwerp van het bericht
->setSubject($this->config["subject"])
// afzender
->setFrom($this->config["from"])
// ontvangers met een woordenboek (setTo/setCc/setBcc)
->setTo($this->config["to"])
// berichttekst
->setBody($this->config["message"])
;
// bijlagen
foreach ($this->config["attachments"] as $attachment) {
// pad naar de bijlage
$fileName = __DIR__ . $attachment;
// er wordt gecontroleerd of het bestand bestaat
if (file_exists($fileName)) {
// het document wordt aan het bericht toegevoegd
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
if ($this->logger !== NULL) {
// fout
$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 {
// geen TLS
$transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
}
// de verzendmanager
$mailer = new \Swift_Mailer($transport);
// verzending van het bericht
$mailer->send($message);
// einde
if ($this->logger !== NULL) {
$this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
}
} catch (\Throwable $ex) {
// fout
if ($this->logger !== NULL) {
$this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
}
}
}
}
Opmerkingen
- regel 11: de constructor ontvangt twee parameters:
- [$config]: een associatieve array met alle informatie die nodig is om de e-mail te versturen;
- [$logger]: een logger waarmee belangrijke momenten tijdens het verzenden van de e-mail kunnen worden gelogd;
Het associatieve array heeft de volgende vorm:
- regels 16-76: met de methode [send] kan de e-mail worden verzonden. Deze code is gepresenteerd en beschreven in de paragraaf ‘link’;
19.1.2. De laag [dao]

Het script [ServeurDaoWithSession.php] is als volgt:
<?php
// naamruimte
namespace Application;
// definitie van een klasse ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {
// constructor
public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
// het eenvoudigste geval
if ($taxAdminData !== NULL) {
$this->taxAdminData = $taxAdminData;
} else {
// het wordt doorgegeven aan de bovenliggende klasse
parent::__construct($databaseFilename);
}
}
}
Opmerkingen
- regel 7: de klasse [ServerDaoWithSession] van versie 09 breidt de klasse [ServerDao] van versie 08 uit. De klasse [ServerDao] kan namelijk de database gebruiken. Nu hoeven we alleen nog maar rekening te houden met het geval waarin de gegevens van de belastingdienst al zijn verkregen:
- regel 10: de constructor ontvangt nu twee parameters:
- [string $databaseFilename]: naam van het bestand met de gegevens om verbinding te maken met de database als de gegevens van de belastingdienst nog niet zijn opgehaald, NULL anders;
- [TaxAdminData $taxAdminData]: de gegevens van de belastingdienst indien deze al zijn opgehaald, NULL anders;
Bij het starten van een websessie wordt de laag [dao] opgebouwd met een object [$databaseFilename] dat niet NULL is en een object [taxAdminData] NULL. De gegevens van de belastingdienst worden vervolgens uit de database opgehaald en in de sessie opgeslagen. Bij latere verzoeken binnen dezelfde sessie wordt de laag [dao] opgebouwd met een object [databaseFilename], NULL en een object [taxAdminData] uit de sessie, en niet met NULL. Er vindt dus geen zoekopdracht in de database plaats.
19.1.3. Het serverscript
Het serverscript [impots-server.php] wordt geconfigureerd door het volgende bestand jSON [config-server.json]:
{
"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"
}
Het serverscript [impots-server.php] ontwikkelt zich als volgt:
<?php
// strikte naleving van de gedeclareerde typen van de functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
// foutafhandeling door PHP
ini_set("display_errors", "0");
//
// pad naar het configuratiebestand
define("CONFIG_FILENAME", "Data/config-server.json");
// de configuratie wordt opgehaald
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// de benodigde afhankelijkheden worden in het script opgenomen
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute afhankelijkheden (bibliotheken van derden)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// Symfony-afhankelijkheden
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// sessie
$session = new Session();
$session->start();
// voorbereiding van het antwoord JSON van de server
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// logbestanden aanmaken
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// interne serverfout
doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
// voltooid
exit;
}
// eerste log
$logger->write("\n---nouvelle requête\n");
// het huidige verzoek wordt opgehaald
$request = Request::createFromGlobals();
// authenticatie alleen de eerste keer
if (!$session->has("user")) {
// log
$logger->write("Autentification en cours…\n");
// authenticatie
…
}
// is de gebruiker gevonden?
if (!$trouvé) {
// niet gevonden - 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
);
// voltooid
exit;
} else {
// in de sessie wordt genoteerd dat de gebruiker is geauthenticeerd
$session->set("user", TRUE);
// log
$logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
}
} else {
// log
$logger->write("Authentification prise en session…\n");
}
// er is een geldige gebruiker - de ontvangen parameters worden gecontroleerd
$erreurs = [];
// er moeten drie parameters zijn: GET
…
// fouten?
if ($erreurs) {
// er wordt een foutcode 400 HTTP_BAD_REQUEST naar de klant verzonden
sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
// voltooid
exit;
} else {
// logbestanden
$logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// we hebben alles wat we nodig hebben om te werken
// aanmaken van de laag [dao]
if (!$session->has("taxAdminData")) {
// de gegevens worden uit de database gehaald
$logger->write("données fiscales prises en base de données\n");
try {
// opbouw van de laag [dao]
$dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
// de gegevens worden in de sessie geplaatst
$session->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// de fout wordt genoteerd
doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
// voltooid
exit;
}
} else {
// de gegevens worden in de sessie opgenomen
$dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
// logbestanden
$logger->write("données fiscales prises en session\n");
}
// aanmaken van de laag [métier]
$métier = new ServerMetier($dao);
// berekening van de belasting
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// het antwoord wordt weergegeven
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// einde
exit;
function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
// er wordt een e-mail naar de beheerder verzonden
// SendAdminMail vangt alle uitzonderingen op en logt deze zelf
$infos['message'] = $message;
$sendAdminMail = new SendAdminMail($infos, $logger);
$sendAdminMail->send();
// er wordt een 500-foutcode naar de klant gestuurd
sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}
// functie voor het verzenden van het antwoord HTTP naar de klant
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
// $response: antwoord HTTP
// $result: resultatenoverzicht
// $statusCode: status HTTP van het antwoord
// $headers: HTTP-headers die in het antwoord moeten worden opgenomen
// $logger: de logger van de applicatie
//
// status HTTTP
$response->setStatusCode($statusCode);
// hoofdtekst
$body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$response->setContent($body);
// headers
$response->headers->add($headers);
// verzending
$response->send();
// log
if ($logger != NULL) {
$logger->write("$body\n");
$logger->close();
}
}
Opmerkingen
- regels 34-35: er wordt een sessie gestart;
- regels 38-40: er wordt een antwoord jSON voorbereid;
- regels 42-50: er wordt geprobeerd het logbestand aan te maken. Als er een uitzondering optreedt, wordt de methode [doInternalServer] (regels 132-140) aangeroepen;
- regel 132: de methode [doInternalServer] accepteert vier parameters:
- [$message]: het bericht dat moet worden gelogd. Moet worden gecodeerd in UTF-8;
- [$response]: het object [Response] dat het antwoord van de server aan zijn client omvat;
- [$logger]: het object [Logger] waarmee logbestanden kunnen worden aangemaakt;
- [$infos]: de gegevens waarmee een e-mail naar de beheerder van de applicatie kan worden verzonden;
- regels 135-137: er wordt een e-mail verzonden naar de beheerder van de applicatie;
- regel 139: het antwoord wordt naar de klant verzonden:
- $response: antwoord HTTP;
- $result: de server verstuurt de tekenreeks jSON uit de array [‘réponse’=>["erreur" => $message]];
- $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], code 500;
- $headers: [], geen HTTP-headers om aan het antwoord toe te voegen;
- $logger: de logger van de applicatie;
- regel 58: dankzij de ingestelde sessie hoeft de klant slechts één keer te worden geauthenticeerd:
- zodra de klant is geauthenticeerd, wordt er een sleutel [user] in de sessie geplaatst (regel 78);
- bij het volgende verzoek van dezelfde klant voorkomt regel 58 een authenticatie die overbodig is geworden;
- regel 103: dankzij de ingestelde sessie wordt er slechts één keer in de database naar gegevens gezocht:
- bij het eerste verzoek vindt de zoekopdracht in de database plaats (regel 108). De opgehaalde gegevens worden vervolgens in de sessie opgeslagen (regel 110) en gekoppeld aan de sleutel [taxAdminData];
- bij de volgende verzoeken wordt de sleutel [taxAdminData] in de sessie gevonden (regel 103) en worden de gegevens op de schijf vervolgens rechtstreeks doorgegeven aan de laag [dao] (regel 119);
- regels 111-116: het opzoeken van fiscale gegevens in de database kan mislukken. In dat geval wordt een code [500 Internal Server Error] naar de klant verzonden;
- regel 113: de foutmelding van de uitzondering van de driver MySQL is gecodeerd als ISO 8859-1. Deze wordt omgezet naar UTF-8 om correct te worden gelogd;
- de rest van de code is vrijwel identiek aan die van de vorige versie;
- regels 143-164: de functie [sendResponse] stuurt alle antwoorden naar de client;
- regels 144-148: betekenis van de parameters;
- regel 153: het antwoord is altijd de tekenreeks jSON uit een array [‘résultat’=>qqChose];
- regel 156: soms moeten er HTTP-headers aan het antwoord worden toegevoegd. Dit is het geval in regel 71;
- regel 158: het antwoord wordt verzonden;
- regels 160-163: het antwoord wordt gelogd en de logger wordt gesloten;
19.1.4. Tests [Codeception]

We gaan alleen de laag [dao] testen, aangezien dit de enige is die is gewijzigd.
De testcode [ServerDaoTest] is als volgt:
<?php
// strikte naleving van de gedeclareerde typen van functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
// definitie van constanten
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// pad naar het configuratiebestand
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// de configuratie wordt opgehaald
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// de benodigde afhankelijkheden worden in het script opgenomen
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute afhankelijkheden (bibliotheken van derden)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// bovenliggend
parent::__construct();
// de configuratie wordt opgehaald
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// aanmaken van de laag [dao]
$dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
- regels 9-24: we creëren een uitvoeringsomgeving die identiek is aan die van het serverscript [impots-server];
- regel 38: om de laag [dao] op te bouwen, wordt de klasse [ServerDaoWithSession] geïnstantieerd;
Het testresultaat is als volgt:

19.2. De client
We richten ons op het clientgedeelte van de applicatie.

Deze architectuur wordt geïmplementeerd door de volgende scripts:

In de nieuwe versie veranderen alleen:
- het configuratiebestand [config-client.json];
- de clientlaag [dao];
19.2.1. de laag [dao]
De laag [Dao] verandert als volgt:
<?php
namespace Application;
// afhankelijkheden
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// gebruik van een Trait
use TraitDao;
// attributen
private $urlServer;
private $user;
private $sessionCookie;
// constructor
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// belastingberekening
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// sessiecookie?
if (!$this->sessionCookie) {
// een klant aanmaken HTTP
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// er wordt een verzoek naar de server gestuurd zonder sessiecookie
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]
]);
} else {
// er wordt een verzoek naar de server gestuurd met de sessiecookie
// we maken een client aan: HTTP
$httpClient = HttpClient::create([
"verify_peer" => false
]);
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"headers" => ["Cookie" => $this->sessionCookie]
]);
}
// het antwoord wordt opgehaald
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logbestanden
print "$json=json\n";
// de status van het antwoord wordt opgehaald
$statusCode = $response->getStatusCode();
// fout?
if ($statusCode !== 200) {
// er is een fout opgetreden – er wordt een uitzondering gegenereerd
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// de sessiecookie wordt opgehaald
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// sessiecookie?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// we sturen het antwoord terug
return $réponse;
}
}
Opmerkingen
De wijziging in de laag [dao] houdt in dat er nu een sessie wordt beheerd:
- regel 14: de sessiecookie;
- regels 25-39: bij het eerste verzoek bestaat dit cookie nog niet; het verzoek wordt dan naar de server gestuurd met de authenticatiegegevens (regel 28);
- regels 40-53: bij volgende verzoeken beschikken we normaal gesproken over de sessiecookie. We sturen dan geen authenticatiegegevens mee (regels 42-44);
- regels 69-82: het antwoord van de server op het eerste verzoek bevat een sessiecookie. Deze wordt opgehaald. Deze code is al gebruikt en uitgelegd in de paragraaf ‘link’;
- regel 78: het opgehaalde sessiecookie wordt opgeslagen in het klasse-attribuut [$sessionCookie];
Opmerking: we hadden de oude versie van de laag [dao] kunnen behouden en bij elk verzoek authenticatie kunnen uitvoeren, aangezien de kosten hiervan verwaarloosbaar zijn. Om didactische redenen wilden we echter nogmaals laten zien hoe een client HTTP een sessie kan beheren.
19.2.2. Het configuratiebestand
Het configuratiebestand jSON verandert als volgt:
{
"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"
}
Alleen de URL op regel 24 verandert.
19.3. Enkele tests
19.3.1. Test 1
Allereerst voeren we de client uit in een foutloze omgeving. De resultaten zijn nog steeds dezelfde als bij de vorige versies. Maar nu hebben we aan de serverzijde een logbestand met de naam [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
…
- regels 3-7: bij het eerste verzoek vindt authenticatie plaats en worden de gegevens in de database opgezocht;
- regels 9-14: bij de volgende verzoek vindt er geen authenticatie meer plaats en worden de gegevens uit de sessie opgehaald. Dit herhaalt zich bij de volgende verzoeken (regels 15 en verder);
19.3.2. Test 2
Laten we nu de database MySQL afsluiten. Aan de clientzijde krijgen we de volgende console-uitvoer:
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é
Aan de serverzijde zien we de volgende logbestanden: [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
…
Om de e-mail te bekijken die de beheerder van de applicatie heeft ontvangen, gebruiken we het script [imap-03.php] uit de paragraaf 'Koppeling met het configuratiebestand' met het volgende configuratiebestand:
Het resultaat is als volgt:

Het bestand [message_1.txt] bevat de volgende tekst:
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
Laten we er nu voor zorgen dat het bestand [logs.txt] niet kan worden aangemaakt. Hiervoor volstaat het om een map [logs.txt] aan te maken:

Zodra dit is gebeurd, voeren we de client uit.
Aan de clientzijde krijgen we de volgende console-uitvoer:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé
Aan de serverzijde zijn er geen logbestanden, maar de beheerder ontvangt de volgende 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
Laten we deze keer in het configuratiebestand van de client onjuiste inloggegevens opgeven voor de client die verbinding maakt.
De client geeft de volgende console-uitvoer weer:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
Aan de serverzijde verschijnen de volgende logberichten:
---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
Laten we de juiste gebruiker [admin, admin] weer in het configuratiebestand van de client zetten.
Laten we nu de URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] van de server rechtstreeks in een browser opvragen zonder parameters door te geven:
In het logbestand [logs.txt] van de server staan de volgende regels:
---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]
Net als bij eerdere versies gaan we [Codeception]-tests schrijven voor versie 09.

19.4.0.1. Test van de laag [métier]
De test [ClientMetierTest.php] is als volgt:
<?php
// strikte naleving van de gedeclareerde typen van de functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
// definitie van constanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09");
// pad naar het configuratiebestand
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// de configuratie wordt opgehaald
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// de benodigde afhankelijkheden worden in het script opgenomen
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute afhankelijkheden (bibliotheken van derden)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// gebruikt
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;
// testklasse
class ClientMetierTest extends Unit {
…
}
Opmerkingen
- ten opzichte van de testklasse van versie 08 verandert alleen regel 10, die de hoofdmap van de te testen client aangeeft;
De testresultaten zijn als volgt:

Het is interessant om de logbestanden van de server [logs.txt] te bekijken:
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}}
We zien dat de gegevens van de belastingdienst altijd uit de database worden gehaald en nooit uit de sessie. Laten we teruggaan naar de code van de uitgevoerde test:
<?php
// strikte naleving van de gedeclareerde typen van functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
…
// testklasse
class ClientMetierTest extends Unit {
// bedrijfslaag
private $métier;
public function __construct() {
parent::__construct();
// de configuratie wordt opgehaald
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// de laag [dao] aanmaken
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// aanmaken van de laag [métier]
$this->métier = new ClientMetier($clientDao);
}
// tests
public function test1() {
…
}
public function test2() {
…
}
public function test3() {
…
}
…
}
In een testklasse [Codeception] wordt de constructor voor elke test uitgevoerd.
- regel 21: voor elke test wordt dus een nieuwe [ClientDao] aangemaakt met een sessiecookie NULL. Dit verklaart waarom deze klant geen gebruik maakt van een sessie;
Dit voorbeeld laat zien dat de sessie niet de juiste plek is om gegevens van de belastingdienst op te slaan. Deze gegevens zijn namelijk gemeenschappelijk voor alle gebruikers van de applicatie. Hier worden ze echter in elke sessie van deze gebruikers gedupliceerd.
Bij webprogrammering onderscheiden we drie soorten zichtbaarheid voor gedeelde gegevens:
- gegevens die door alle gebruikers van de webapplicatie worden gedeeld. Dit zijn over het algemeen alleen-lezen-gegevens. PHP beschikt niet standaard over dit geheugen;
- gegevens die worden gedeeld door de verzoeken van dezelfde klant. Deze gegevens worden opgeslagen in de sessie. We spreken dan van een klantsessie om het geheugen van de klant aan te duiden. Alle verzoeken van een klant hebben toegang tot deze sessie. Ze kunnen er informatie opslaan en lezen. In de voorgaande scripts wordt deze sessie geïmplementeerd door het Symfony-object [HttpFoundation\Session\Session];
- het verzoekgeheugen, of de verzoekcontext. Het verzoek van een gebruiker kan door meerdere opeenvolgende acties worden verwerkt. Dankzij de verzoekcontext kan actie 1 informatie doorgeven aan actie 2. In de voorgaande scripts wordt het verzoek geïmplementeerd door het Symfony-object [HttpFoundation\Request] en het geheugen ervan door het attribuut [HttpFoundation\Request::attributes];

Er bestaan bibliotheken van derden om PHP een applicatiegeheugen te geven. De nieuwe versie van de praktijkopdracht laat het gebruik van een van deze bibliotheken zien.