Skip to content

20. Toepassingsopdracht – versie 10

In de vorige versie is aangetoond dat de fiscale gegevens, die door alle gebruikers van de applicatie worden gedeeld, moeten worden opgeslagen in een geheugen met bereik [Application]. We gaan een Redis-server [https://redis.io] gebruiken om dit te implementeren.

20.1. Redis

Het werkgeheugen met de code [Application] wordt geïmplementeerd door een Redis-server. De scripts met de code PHP die dit werkgeheugen nodig hebben, zullen clients van deze server zijn:

Image

20.2. Installatie van Redis

Laragon wordt geleverd met een Redis-server die standaard niet is ingeschakeld. Deze moet dus eerst worden ingeschakeld:

Image

  • in [3], activeer de server [Redis];
  • in [4] de poort [6379] laten staan die Redis-clients standaard gebruiken;

De Laragon-services worden automatisch opnieuw gestart nadat Redis is geactiveerd:

Image

20.3. De Redis-client in opdrachtmodus

De Redis-server kan in de opdrachtmodus worden opgevraagd. Open een Laragon-terminal (zie paragraaf 'link'):

Image

  • in [1] start het commando [redis-cli] de client in de commandomodus van de Redis-server;

In juli 2019 kan de Redis-client 172 commando’s gebruiken om met de server [https://redis.io/commands#list] te communiceren. Een daarvan, [command count] [2], geeft dit getal weer: [3].

We zullen alleen die commando’s bespreken die we nodig hebben voor onze toepassing PHP. We gaan Redis voor één enkel doel gebruiken: het opslaan van een array [‘attribut’=>’valeur’] in het geheugen van Redis. Dit gebeurt met het Redis-commando [set attribut valeur] [4]. De waarde kan vervolgens worden opgehaald met het commando [get attribut] [5]. Dat is alles wat we nodig hebben.

Het kan nodig zijn om het geheugen van Redis te legen. Dit doe je met het commando [flushdb] [6]. Als we vervolgens de waarde van het attribuut [titre] [7] opvragen, krijgen we een verwijzing [nil] [8] die aangeeft dat het attribuut niet is gevonden. Je kunt ook het commando [exists] [9-10] gebruiken om te controleren of een attribuut bestaat.

Om de Redis-client af te sluiten, voert u de opdracht [quit] [11] in.

20.4. Installatie van een Redis-client voor PHP

Nu moeten we een Redis-client voor PHP installeren:

Image

Er zijn verschillende bibliotheken die een Redis-client implementeren. We gebruiken de bibliotheek [Predis] [https://github.com/nrk/predis] (juli 2019). Deze wordt, net als de vorige, met [composer] geïnstalleerd in een Laragon-terminal:

Image

20.5. Servercode

Image

Het configuratiebestand [config-server.json] verandert als volgt:


{
    "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"
}

Opmerkingen

  • regels 5-15: versie 10 voegt niets nieuws toe, afgezien van het script [impots-server.php]. Het maakt gebruik van elementen uit de versies 08 en 09;
  • regel 19: een vereiste afhankelijkheid van de bibliotheek [predis] die zojuist is geïnstalleerd;

De code van de server [impots-server.php] verandert als volgt:


<?php

// strikte naleving van de gedeclareerde typen van functieparameters
declare (strict_types=1);

// naamruimte
namespace Application;

// foutbeheer door PHP
ini_set("display_errors", "0");
//
// pad naar het configuratiebestand
define("CONFIG_FILENAME", "Data/config-server.json");
// klasse-alias
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

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


// eerste log
$logger->write("\n---nouvelle requête\n");

// de huidige aanvraag wordt opgehaald
$request = Request::createFromGlobals();
// authenticatie alleen de eerste keer
if (!$session->has("user")) {

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

// we hebben een geldige gebruiker – we controleren de ontvangen parameters
$erreurs = [];
// er moeten drie parameters zijn GET
$method = strtolower($request->getMethod());


// fouten?
if ($erreurs) {
// we sturen een foutcode 400 HTTP_BAD_REQUEST naar de client
  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
// Redis
\Predis\Autoloader::register();
try {
  // client [predis]
  $redis = new \Predis\Client();
  // we maken verbinding met de server om te controleren of deze beschikbaar is
  $redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
  // interne serverfout
  doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
  // voltooid
  exit;
}

// de laag [dao] wordt aangemaakt
if (!$redis->get("taxAdminData")) {
  // de fiscale gegevens worden uit de database opgehaald
  $logger->write("données fiscales prises en base de données\n");
  try {
    // opbouw van de laag [dao]
    $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
    // de fiscale gegevens worden in het bereikgeheugen geplaatst [application]
    // de methode [TaxAdminData]->__toString wordt impliciet aangeroepen
    $redis->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // er wordt een fout gemeld
    doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
    // voltooid
    exit;
  }
} else {
  // de fiscale gegevens worden uit het bereikgeheugen gehaald [application]
  $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
  $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
  // instantiëring van de laag [dao]
  $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
  // logbestanden
  $logger->write("données fiscales prises dans redis\n");
}
// aanmaken van de laag [métier]
$métier = new ServerMetier($dao);
// belastingberekening
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// het antwoord wordt weergegeven
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// einde
exit;

function doInternalServerError(string $message, Response $response, array $infos,
  Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $message: de foutmelding
  // $response: antwoord HTTP
  // $infos: informatietabel voor het verzenden van de e-mail
  // $result: resultatenoverzicht
  // $logger: de logger van de applicatie
  // $predisClient: een [predis]-client
  //
  // 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 foutcode 500 naar de klant gestuurd
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}

// functie voor het verzenden van het antwoord HTTP naar de klant
function sendResponse(Response $response, array $result, int $statusCode,
  array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $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
  // $predisClient: een client [predis]
  //
  // 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();
  // logboek
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
  // verbinding afsluiten [redis]
  if ($predisClient != NULL) {
    $predisClient->disconnect();
  }
}

Opmerkingen

  • regel 15: aan de klasse [\Application\ServerDaoWithSession] wordt de alias [ServerDaoWithRedis] toegekend om de gewijzigde implementatie van het serverscript weer te geven;
  • regels 18-19: de sessie wordt behouden. Hier moeten we twee zaken onthouden:
    • het feit dat de gebruiker zich correct heeft geauthenticeerd. Deze informatie heeft het bereik [session]: ze is gekoppeld aan een specifieke gebruiker en geldt niet voor andere gebruikers;
    • de gegevens van de belastingdienst. Deze informatie valt onder het bereik [application]: ze is niet gekoppeld aan een specifieke gebruiker, maar geldt voor alle gebruikers;
  • regels 54-64: aanmaken van de client [redis] die zal communiceren met de server [redis]. Deze client zal communiceren via de standaardpoort van de server. Als de server niet via zijn standaardpoort zou communiceren of als hij zich niet op de machine [localhost] zou bevinden, zouden deze gegevens moeten worden doorgegeven aan de constructor van de klasse [\Predis\Client];
  • regel 59: de client wordt onmiddellijk met de server verbonden om te controleren of deze reageert;
  • regels 60-65: als de verbinding met de Redis-server mislukt, wordt er een foutmelding naar de client gestuurd en wordt er een e-mail verzonden naar de beheerder van de applicatie;
  • regel 67: de server [redis] wordt gevraagd om de sleutel [taxAdminData]. Als deze niet wordt gevonden, worden de fiscale gegevens uit de database gehaald (regel 72);
  • regel 75: de sleutel [taxAdminData] wordt geplaatst in het geheugen [redis], gekoppeld aan de tekenreeks jSON van de variabele [$taxAdminData], die een object is van het type [TaxAdminData]. De methode [$redis→set] verwacht een tekenreeks als waarde voor de sleutel. Ze zal dus proberen het object van het type [TaxAdminData] om te zetten naar het type [string]. Vervolgens wordt impliciet de methode [TaxAdminData->__toString] aangeroepen. Deze produceert de tekenreeks jSON van het object [TaxAdminData];
  • regel 84: de sleutel [taxAdminData] bevindt zich in het geheugen [redis], dus wordt de waarde ervan opgehaald. We weten dat dit de tekenreeks jSON is van een object [TaxAdminData]. Deze wordt vervolgens gedecodeerd om een array met attributen te verkrijgen;
  • regel 85: op basis van deze array wordt een nieuw object [TaxAdminData] geïnstantieerd;
  • regel 87: de laag [dao] wordt geïnstantieerd;

20.6. Code van de klant

Image

Versie 10 van de client is identiek aan versie 9. Alleen het configuratiebestand [config-client.json] is gewijzigd:


{
    "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"
}

Alleen op regel 24 verandert de URL van de server.

De resultaten zijn hetzelfde als in versie 09. Laten we gewoon een nieuw foutgeval testen:

Image

Het resultaat in de console is als volgt:


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 met [Codeception] van de client

Image

De testklasse [ClientMetierTest] van versie 10 is identiek aan die van versie 09, op één uitzondering na:


<?php

// strikte naleving van de gedeclareerde typen van functieparameters
declare (strict_types=1);

// naamruimte
namespace Application;

// definitie van constanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");



}
  • regel 10: de testomgeving is die van de klant van versie 10;

Laten we, voordat we met de tests beginnen, met behulp van de client [redis-cli] de sleutel [taxAdminData] uit het geheugen van de server [redis] verwijderen:

Image

Laten we nu de test uitvoeren:

Image

Laten we nu de logbestanden [logs.txt] van de server bekijken:


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

We hebben al gezegd dat bij elke test de constructor van de testklasse opnieuw wordt uitgevoerd, waardoor de geteste klasse [ClientDao] bij elke test wordt geïnstantieerd met een niet-bestaande sessiecookie. Het is dus alsof de 11 tests 11 verschillende gebruikers vertegenwoordigen, met 11 verschillende sessies.

  • regel 6: de fiscale gegevens worden uit de database opgehaald;
  • regels 13, 20: de fiscale gegevens worden uit het geheugen [redis] opgehaald. We hebben hier dus inderdaad te maken met een geheugen met de naam [application] dat door alle gebruikers van de applicatie wordt gedeeld;

20.8. Webinterface van de server [Redis]

We hebben gezien dat de server [Redis] via de opdrachtregel kan worden beheerd. Hij kan ook via een webinterface worden beheerd:

Image

  • in [4], de beheerinterface URL;
  • in [5], de door de server opgeslagen sleutels;
  • in [6], de huidige status van de server;

Door op [5] te klikken, krijgt men informatie over de sleutel [taxAdminData]:

Image

  • in [7], de URL die toegang geeft tot de informatie van de sleutel [taxAdminData] [8];
  • in [9], de status van de sleutel;
  • in [10], de waarde ervan: we herkennen de tekenreeks jSON van een object van het type [TaxAdminData];
  • in [11] kan de sleutel worden verwijderd;
  • in [12] kan men een andere toevoegen;