XX. Exercice d’application – version 10▲
La version précédente a montré que les données fiscales, partagées par tous les utilisateurs de l’application, devraient être stockées dans une mémoire de portée [Application]. Nous allons utiliser un serveur Redis [https://redis.io] pour implémenter celle-ci.
XX-A. Redis▲
La mémoire de portée [Application] sera implémentée par un serveur Redis. Les scripts PHP ayant besoin de cette mémoire d’application seront des clients de ce serveur :
XX-B. Installation de Redis▲
Laragon vient avec un serveur Redis non activé par défaut. Il faut donc commencer par l’activer :
- en [3], activer le serveur [Redis] ;
- en [4], laisser le port [6379] que les clients Redis utilisent par défaut ;
Les services Laragon sont automatiquement relancés après activation de Redis :
XX-C. Le client Redis en mode commande▲
Le serveur Redis peut être interrogé en mode commande. On ouvre un terminal Laragon (cf paragraphe lienInstallation de Laragon) :
- en [1], la commande [redis-cli] lance le client en mode commande du serveur Redis ;
En juillet 2019, le client Redis peut utiliser 172 commandes pour dialoguer avec le serveur [https://redis.io/commands#list]. L’une d’elles [command count] [2], affiche ce nombre [3].
Nous n’allons présenter que celles dont nous allons avoir besoin dans notre application PHP. Nous allons utiliser Redis pour une unique chose : stocker un tableau [‘attribut’=>’valeur’] dans la mémoire de Redis. Cela se fait avec la commande Redis [set attribut valeur] [4]. La valeur peut ensuite être récupérée avec la commande [get attribut] [5]. C’est tout ce dont nous aurons besoin.
Il peut être nécessaire de vider la mémoire de Redis. Cela se fait avec la commande [flushdb] [6]. Ensuite si on demande la valeur de l’attribut [titre] [7], on obtient une référence [nil] [8] indiquant que l’attribut n’a pas été trouvé. On peut également utiliser la commande [exists] [9-10] pour vérifier l’existence d’un attribut.
Pour quitter le client Redis, taper la commande [quit] [11].
XX-D. Installation d’un client Redis pour PHP▲
Il nous faut maintenant installer un client Redis pour PHP :
Il existe plusieurs bibliothèques implémentant un client Redis. Nous utiliserons la bibliothèque [Predis] [https://github.com/nrk/predis] (juillet 2019). Celle-ci comme les précédentes s’installe avec [composer] dans un terminal Laragon :
XX-E. Code du serveur▲
Le fichier de configuration [config-server.json] évolue de la façon suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
{
"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"
}
Commentaires
- lignes 5-15 : la version 10 n’amène rien de nouveau en dehors du script [impots-server.php]. Elle utilise des éléments des verions 08 et 09 ;
- ligne 19 : une dépendance nécessaire à la bibliothèque [predis] que l’on vient d’installer ;
Le code du serveur [impots-server.php] évolue de la façon suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
<?php
// respect strict des types déclarés des paramètres de foctions
declare
(strict_types=
1
);
// espace de noms
namespace
Application;
// gestion des erreurs par PHP
ini_set("display_errors"
,
"0"
);
//
// chemin du fichier de configuration
define("CONFIG_FILENAME"
,
"Data/config-server.json"
);
// alias de classe
use
\Application\ServerDaoWithSession as
ServerDaoWithRedis;
// session
$session
=
new
Session();
$session
->
start();
…
…
// 1er log
$logger
->
write("
\n
---nouvelle requête
\n
"
);
// on récupère la requête courante
$request
=
Request::
createFromGlobals();
// authentification seulement la 1re fois
if
(!
$session
->
has("user"
)) {
…
}
else
{
// log
$logger
->
write("Authentification prise en session…
\n
"
);
}
// on a un utilisateur valide - on vérifie les paramètres reçus
$erreurs
=
[];
// on doit avoir trois paramètres GET
$method
=
strtolower($request
->
getMethod());
…
// erreurs ?
if
($erreurs
) {
// on envoie un code d'erreur 400 HTTP_BAD_REQUEST au client
sendResponse($response
,
[
"erreurs"
=>
$erreurs
],
Response::
HTTP_BAD_REQUEST,
[],
$logger
);
// terminé
exit;
}
else
{
// logs
$logger
->
write("paramètres ['marié'=>
$marié
, 'enfants'=>
$enfants
, 'salaire'=>
$salaire
] valides
\n
"
);
}
// on a tout ce qu'il faut pour travailler
// Redis
\Predis\Autoloader::
register();
try
{
// client [predis]
$redis
=
new
\Predis\Client();
// on se connecte au serveur pour voir s'il est là
$redis
->
connect();
}
catch
(\Predis\Connection\ConnectionException $ex
) {
// internal server error
doInternalServerError("[redis], "
.
utf8_encode($ex
->
getMessage()),
$response
,
$config
[
'adminMail'
],
$logger
);
// terminé
exit;
}
// création de la couche [dao]
if
(!
$redis
->
get("taxAdminData"
)) {
// les données fiscales sont prises dans la base de données
$logger
->
write("données fiscales prises en base de données
\n
"
);
try
{
// construction de la couche [dao]
$dao
=
new
ServerDaoWithRedis($config
[
"databaseFilename"
],
NULL
);
// on met les données fiscales dans la mémoire de portée [application]
// la méthode [TaxAdminData]->__toString va être appelée implicitement
$redis
->
set("taxAdminData"
,
$dao
->
getTaxAdminData());
}
catch
(\RuntimeException $ex
) {
// on note l'erreur
doInternalServerError("[dao], "
.
utf8_encode($ex
->
getMessage()),
$response
,
$config
[
'adminMail'
],
$logger
,
$redis
);
// terminé
exit;
}
}
else
{
// les données fiscales sont prises dans la mémoire de portée [application]
$arrayOfAttributes
=
\json_decode($redis
->
get("taxAdminData"
),
true
);
$taxAdminData
=
(new
TaxAdminData())->
setFromArrayOfAttributes($arrayOfAttributes
);
// isntanciation de la couche [dao]
$dao
=
new
ServerDaoWithRedis(NULL
,
$taxAdminData
);
// logs
$logger
->
write("données fiscales prises dans redis
\n
"
);
}
// création de la couche [métier]
$métier
=
new
ServerMetier($dao
);
// calcul de l'impôt
$result
=
$métier
->
calculerImpot($marié
,
(int)
$enfants
,
(int)
$salaire
);
// on rend la réponse
sendResponse($response
,
$result
,
Response::
HTTP_OK,
[],
$logger
,
$redis
);
// fin
exit;
function
doInternalServerError(string $message
,
Response $response
,
array
$infos
,
Logger $logger
=
NULL
,
\Predis\Client $predisClient
=
NULL
) {
// $message : le message d'erreur
// $response : réponse HTTP
// $infos : tableau d'informations pour l'envoi du mail
// $result : tableau des résultats
// $logger : le logueur de l'application
// $predisClient : un client [predis]
//
// on envoie un mail à l'administrateur
// SendAdminMail intercepte toutes les exception et les logue lui-même
$infos
[
'message'
]
=
$message
;
$sendAdminMail
=
new
SendAdminMail($infos
,
$logger
);
$sendAdminMail
->
send();
// on envoie un code d'erreur 500 au client
sendResponse($response
,
[
"erreur"
=>
$message
],
Response::
HTTP_INTERNAL_SERVER_ERROR,
[],
$logger
,
$predisClient
);
}
// fonction d'envoi de la réponse HTTP au client
function
sendResponse(Response $response
,
array
$result
,
int $statusCode
,
array
$headers
,
Logger $logger
=
NULL
,
\Predis\Client $predisClient
=
NULL
) {
// $response : réponse HTTP
// $result : tableau des résultats
// $statusCode : statut HTTP de la réponse
// $headers : entêtes HTTP à mettre dans la réponse
// $logger : le logueur de l'application
// $predisClient : un client [predis]
//
// statut HTTTP
$response
->
setStatusCode($statusCode
);
// body
$body
=
\json_encode([
"réponse"
=>
$result
],
JSON_UNESCAPED_UNICODE);
$response
->
setContent($body
);
// headers
$response
->
headers->
add($headers
);
// envoi
$response
->
send();
// log
if
($logger
!=
NULL
) {
$logger
->
write("
$body\n
"
);
$logger
->
close();
}
// fermeture de la connexion [redis]
if
($predisClient
!=
NULL
) {
$predisClient
->
disconnect();
}
}
Commentaires
- ligne 15 : on donne l’alias [ServerDaoWithRedis] à la classe [\Application\ServerDaoWithSession] pour refléter le changement d’implémentation du script serveur ;
- lignes 18-19 : la session est conservée. Nous avons ici deux informations à mémoriser :
- le fait que l’utilisateur se soit authentifié correctement. Cette information est de portée [session] : elle est liée à un utilisateur précis et n’est pas valable pour les autres utilisateurs ;
- les données de l’administration fiscale. Cette information est de portée [application] : elle n’est pas liée à un utilisateur précis mais est valable pour tous les utilisateurs ;
- lignes 54-64 : création du client [redis] qui va communiquer avec le serveur [redis]. Ce client va communiquer avec le port par défaut du serveur. Si celui-ci ne communiquait pas sur son port par défaut ou s’il n’était pas sur la machine [localhost], il faudrait passer ces informations au constructeur de la classe [\Predis\Client] ;
- ligne 59 : on connecte tout de suite le client au serveur pour savoir si celui-ci répond ;
- lignes 60-65 : si la connexion au serveur Redis échoue, on envoie une réponse d’erreur au client et un mail sera envoyé à l’administrateur de l’application ;
- ligne 67 : on demande au serveur [redis], la clé [taxAdminData]. Si elle n’est pas trouvée, alors les données fiscales sont prises en base de données (ligne 72) ;
- ligne 75 : la clé [taxAdminData] est placée dans la mémoire [redis] associée à la chaîne jSON de la variable [$taxAdminData] qui est un objet de type [TaxAdminData]. La méthode [$redis->set] s’attend à une chaîne de caractères pour la valeur de la clé. Elle va donc chercher à transformer l’objet de type [TaxAdminData] en type [string]. C’est alors implicitement, la méthode [TaxAdminData->__toString] qui va être appelée. Celle-ci produit la chaîne jSON de l’objet [TaxAdminData] ;
- ligne 84 : la clé [taxAdminData] est dans la mémoire[redis], alors on récupère sa valeur. On sait que c’est la chaîne jSON d’un objet [TaxAdminData]. On décode alors celle-ci pour obtenir un tableau d’attributs ;
- ligne 85 : à partir de ce tableau, un nouvel objet [TaxAdminData] est instancié ;
- ligne 87 : la couche [dao] est instanciée ;
XX-F. Code du client▲
La version 10 du client est identique à la version 9. Seul change le fichier de configuration [config-client.json] :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
{
"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"
}
Seule change, ligne 24, l’URL du serveur.
Les résultats sont les mêmes que dans la version 09. Testons simplement un nouveau cas d’erreur :
Le résultat dans la console est le suivant :
2.
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é
XX-G. Tests [Codeception] du client▲
La classe de test [ClientMetierTest] de la version 10 est identique à celle de la version 09 à une exception près :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
<?php
// respect strict des types déclarés des paramètres de foctions
declare
(strict_types=
1
);
// espace de noms
namespace
Application;
// définition des constantes
define("ROOT"
,
"C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10"
);
…
}
- ligne 10 : l’environnement du test est celui du client de la version 10 ;
Avant de commencer les tests, supprimons à l’aide du client [redis-cli] la clé [taxAdminData] de la mémoire du serveur [redis] :
Maintenant, exécutons le test :
Maintenant examinons les logs [logs.txt] du serveur :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
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
…
On a déjà dit qu’à chaque test, le constructeur de la classe de test est réexécuté, ce qui fait que la classe [ClientDao] testée est à chaque test instanciée avec un cookie de session inexistant. Tout se passe donc comme si les 11 tests représentaient 11 utilisateurs différents, avec 11 sessions différentes.
- ligne 6 : les données fiscales sont prises en base de données ;
- lignes 13, 20 : les données fiscales sont prises dans la mémoire [redis]. On a donc bien là une mémoire de portée [application] partagée par tous les utilisateurs de l’application ;
XX-H. Interface web du serveur [Redis]▲
Nous avons vu que le serveur [Redis] pouvait être géré en mode commande. Il peut également être géré grâce à une interface web :
- en [4], l’URL d’administration ;
- en [5], les clés mémorisées par le serveur ;
- en [6], l’état actuel du serveur ;
En cliquant sur [5], on obtient des informations sur la clé [taxAdminData] :
- en [7], l’URL qui donne accès aux informations de la clé [taxAdminData] [8] ;
- en [9], le statut de la clé ;
- en [10], sa valeur : on reconnaît la chaîne jSON d’un objet de type [TaxAdminData] ;
- en [11], on peut supprimer la clé ;
- en [12], on peut en ajouter une autre ;