20. Esercizio sull'applicazione – Versione 10
La versione precedente mostrava che i dati fiscali, condivisi da tutti gli utenti dell'applicazione, dovevano essere memorizzati in una cache a livello di [Application]. Useremo un server Redis [https://redis.io] per implementare questo.
20.1. Redis
La memoria nell'ambito [Application] sarà implementata da un server Redis. Gli script PHP che necessitano di questa memoria dell'applicazione saranno client di questo server:

20.2. Installazione di Redis
Laragon include un server Redis che non è abilitato di default. È quindi necessario iniziare abilitandolo:

- In [3], abilitare il server [Redis];
- In [4], lasciare la porta [6379] come impostazione predefinita utilizzata dai client Redis;
I servizi Laragon vengono riavviati automaticamente dopo l'abilitazione di Redis:

20.3. Il client Redis in modalità comando
Il server Redis può essere interrogato in modalità comando. Apri un terminale Laragon (vedi sezione link):

- In [1], il comando [redis-cli] avvia il client in modalità comando per il server Redis;
A luglio 2019, il client Redis supportava 172 comandi per interagire con il server [https://redis.io/commands#list]. Uno di questi [numero di comandi] [2] mostra questo numero [3].
Tratteremo solo quelli necessari per la nostra applicazione PHP. Useremo Redis per un unico scopo: memorizzare un array [‘attributo’=>’valore’] nella memoria di Redis. Ciò avviene tramite il comando Redis [set attributo valore] [4]. Il valore può poi essere recuperato utilizzando il comando [get attributo] [5]. Questo è tutto ciò di cui avremo bisogno.
Potrebbe essere necessario svuotare la memoria di Redis. Ciò si ottiene con il comando [flushdb] [6]. Quindi, se interroghiamo il valore dell’attributo [title] [7], otteniamo un riferimento [nil] [8] che indica che l’attributo non è stato trovato. Possiamo anche utilizzare il comando [exists] [9-10] per verificare se un attributo esiste.
Per uscire dal client Redis, digitare il comando [quit] [11].
20.4. Installazione di un client Redis per PHP
Ora dobbiamo installare un client Redis per PHP:

Esistono diverse librerie che implementano un client Redis. Useremo la libreria [Predis] [https://github.com/nrk/predis] (luglio 2019). Come le precedenti, questa si installa con [composer] in un terminale Laragon:

20.5. Codice server

Il file di configurazione [config-server.json] cambia come segue:
{
"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"
}
Commenti
- righe 5–15: La versione 10 non introduce nulla di nuovo a parte lo script [impots-server.php]. Utilizza elementi delle versioni 08 e 09;
- riga 19: una dipendenza richiesta dalla libreria [predis] che abbiamo appena installato;
Il codice del server [impots-server.php] cambia come segue:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// error handling by PHP
ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
// class alias
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
// session
$session = new Session();
$session->start();
…
…
// 1st log
$logger->write("\n---nouvelle requête\n");
// retrieve the current query
$request = Request::createFromGlobals();
// authentication only the 1st time
if (!$session->has("user")) {
…
} else {
// log
$logger->write("Authentification prise en session…\n");
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());
…
// mistakes?
if ($erreurs) {
// an error code 400 HTTP_BAD_REQUEST is sent to the customer
sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
// completed
exit;
} else {
// logs
$logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// we've got everything you need to work
// Redis
\Predis\Autoloader::register();
try {
// customer [predis]
$redis = new \Predis\Client();
// connect to the server to see if it's there
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// internal server error
doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
// completed
exit;
}
// creation of the [dao] layer
if (!$redis->get("taxAdminData")) {
// tax data is taken from the database
$logger->write("données fiscales prises en base de données\n");
try {
// construction of the [dao] layer
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// put tax data in scope memory [application]
// method [TaxAdminData]->__toString will be called implicitly
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// we note the error
doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
// completed
exit;
}
} else {
// tax data are taken from the [application] scope memory
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// istanciation of the [dao] layer
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
// logs
$logger->write("données fiscales prises dans redis\n");
}
// creation of the [business] layer
$métier = new ServerMetier($dao);
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// end
exit;
function doInternalServerError(string $message, Response $response, array $infos,
Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $message: error message
// $response : answer HTTP
// $infos: information table for sending mail
// $result: results table
// $logger: application logger
// $predisClient: a customer [predis]
//
// send an e-mail to the administrator
// SendAdminMail intercepts all exceptions and logs them itself
$infos['message'] = $message;
$sendAdminMail = new SendAdminMail($infos, $logger);
$sendAdminMail->send();
// an error code 500 is sent to the customer
sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}
// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response : answer HTTP
// $result: results table
// $statusCode: HTTP response status
// $headers: HTTP headers to be included in the response
// $logger: application logger
// $predisClient: a customer [predis]
//
// status HTTTP
$response->setStatusCode($statusCode);
// body
$body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$response->setContent($body);
// headers
$response->headers->add($headers);
// shipping
$response->send();
// log
if ($logger != NULL) {
$logger->write("$body\n");
$logger->close();
}
// close connection [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
Commenti
- riga 15: assegniamo l'alias [ServerDaoWithRedis] alla classe [\Application\ServerDaoWithSession] per riflettere la modifica nell'implementazione dello script del server;
- righe 18–19: la sessione viene mantenuta. Qui, dobbiamo tenere a mente due informazioni:
- il fatto che l'utente si sia autenticato con successo. Questa informazione ha ambito [session]: è legata a un utente specifico e non è valida per altri utenti;
- i dati dell'amministrazione fiscale. Queste informazioni hanno ambito [application]: non sono collegate a un utente specifico ma si applicano a tutti gli utenti;
- righe 54–64: creazione del client [redis] che comunicherà con il server [redis]. Questo client comunicherà con la porta predefinita del server. Se il server non comunicasse sulla sua porta predefinita o non si trovasse sulla macchina [localhost], queste informazioni dovrebbero essere passate al costruttore della classe [\Predis\Client];
- riga 59: il client viene immediatamente connesso al server per verificare se risponde;
- righe 60–65: se la connessione al server Redis fallisce, viene inviata una risposta di errore al client e un'e-mail all'amministratore dell'applicazione;
- riga 67: interroghiamo il server [redis] per la chiave [taxAdminData]. Se non viene trovata, i dati fiscali vengono recuperati dal database (riga 72);
- riga 75: la chiave [taxAdminData] viene memorizzata nella memoria [redis] insieme alla stringa JSON della variabile [$taxAdminData], che è un oggetto di tipo [TaxAdminData]. Il metodo [$redis→set] si aspetta una stringa come valore della chiave. Cercherà quindi di convertire l'oggetto [TaxAdminData] in un tipo [string]. Questo chiama implicitamente il metodo [TaxAdminData->__toString], che produce la stringa JSON dell'oggetto [TaxAdminData];
- riga 84: la chiave [taxAdminData] si trova nella memoria [redis], quindi ne recuperiamo il valore. Sappiamo che si tratta della stringa JSON di un oggetto [TaxAdminData]. La analizziamo quindi per ottenere un array di attributi;
- riga 85: da questo array viene istanziato un nuovo oggetto [TaxAdminData];
- riga 87: viene istanziato il livello [dao];
20.6. Codice client

La versione 10 del client è identica alla versione 9. L'unica modifica riguarda il file di configurazione [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"
}
L'unica modifica riguarda l'URL del server alla riga 24.
I risultati sono gli stessi della versione 09. Proviamo semplicemente un nuovo caso di errore:

Il risultato nella console è il seguente:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"[redis], Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée. [tcp:\/\/127.0.0.1:6379]"}
Terminé
20.7. [Codeception] test client

La classe di test [ClientMetierTest] nella versione 10 è identica a quella della versione 09 con un'unica eccezione:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");
…
}
- riga 10: l'ambiente di test è quello del client versione 10;
Prima di avviare i test, utilizziamo il client [redis-cli] per eliminare la chiave [taxAdminData] dalla memoria del server [redis]:

Ora, eseguiamo il test:

Ora esaminiamo i log del server [logs.txt]:
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
…
Abbiamo già menzionato che per ogni test, il costruttore della classe di test viene rieseguito, il che significa che la classe [ClientDao] sottoposta a test viene istanziata con un cookie di sessione inesistente per ogni test. Tutto procede quindi come se gli 11 test rappresentassero 11 utenti diversi, con 11 sessioni diverse.
- riga 6: i dati fiscali vengono recuperati dal database;
- righe 13, 20: i dati fiscali vengono recuperati dalla memoria [Redis]. Abbiamo quindi una memoria a livello di [applicazione] condivisa da tutti gli utenti dell'applicazione;
20.8. Interfaccia web del server [Redis]
Abbiamo visto che il server [Redis] può essere gestito in modalità comando. Può anche essere gestito tramite un'interfaccia web:

- in [4], l'URL di amministrazione;
- in [5], le chiavi memorizzate dal server;
- in [6], lo stato attuale del server;
Cliccando su [5], è possibile visualizzare le informazioni relative alla chiave [taxAdminData]:

- in [7], l'URL che consente di accedere alle informazioni relative alla chiave [taxAdminData] [8];
- in [9], lo stato della chiave;
- in [10], il suo valore: è possibile riconoscere la stringa JSON di un oggetto [TaxAdminData];
- In [11], è possibile eliminare la chiave;
- in [12], è possibile aggiungerne un'altra;