XVIII. Exercice d’application – version 8▲
Nous allons reprendre l’application exemple – version 5 (paragraphe lienExercice d’application – version 5) et allons en faire une application client / serveur.
XVIII-A. Introduction▲
L’architecture de la version 5 était la suivante :
- la couche appelée [dao] (Data Access Objects) s'occupe des échanges avec la base de données MySQL et le système de fihiers local ;
- la couche appelée [métier] fait le calcul de l'impôt ;
- le script principal est le chef d’orchestre : il instancie les couches [dao] et [métier] puis dialogue avec la couche [métier] pour faire ce qu’il y a faire ;
Nous allons migrer cette architecture vers l’architecture client / serveur suivante :
- en [2], nous reprendrons la couche [dao] de la version 5 en lui enlevant les méthodes d’accès au système de fichiers local. Ces méthodes migreront dans la couche [dao] du client [6, 7] ;
- en [3], la couche [métier] restera celle de la version 5 sans ses méthodes [executeBatchImpôts, saveResults] qui migrent dans la couche [dao] [7] du client ;
- en [4], le script serveur est à écrire : il aura à :
- créer les couches [métier] et [dao] [3, 2] ;
- dialoguer avec le script client [5, 7] ;
- en [7], la couche [dao] du client est à écrire :
- elle sera un client HTTP du script serveur [4, 5] ;
- elle reprendra les méthodes d’accès au système de fichiers local de la couche [dao] de la version 5 ;
- en [8], la couche [métier] du client respectera l’interface [InterfaceMetier] de la version 5. Son implémentation sera cependant différente. Dans la version 5, la couche [métier] faisait le calcul de l’impôt. Ici, c’est la couche [métier] du serveur qui fait ce calcul. La couche [métier] fera donc appel à la couche [dao] [7], pour dialoguer avec le serveur et lui demander de calculer l’impôt ;
- en [9], le script console aura à instancier les couches [dao, métier] du client et à lancer l’exécution de celui-ci ;
XVIII-B. Le serveur▲
Nous nous intéressons à la partie serveur de l’application.
Cette architecture sera implémentée par les scripts suivants :
XVIII-B-1. Les entités échangées entre les couches▲

Les entités échangées entre les couches sont celles de la version 5 décrites au paragraphe lienLes entités.
XVIII-B-2. La couche [dao]▲
La couche [dao] implémente l’interface [InterfaceServerDao] suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
<?php
// espace de noms
namespace Application;
interface InterfaceServerDao {
// lecture des données de l'administration fiscale
public function getTaxAdminData(): TaxAdminData;
}
- ligne 9 : la méthode [getTaxAdminData] va chercher les données de l’administration fiscale dans une base de données ;
L’interface [InterfaceServerDao] est implémentée par la classe [ServerDao] 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.
<?php
// espace de noms
namespace Application;
// définition d'une classe ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
// l'objet de type TaxAdminData qui contient les données des tranches d'impôts
private $taxAdminData;
// l'objet de type [Database] contennat les caractéristiques de la BD
private $database;
// constructeur
public function __construct(string $databaseFilename) {
// on mémorise la configuration JSON de la bd
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// on prépare l'attribut
$this->taxAdminData = new TaxAdminData();
try {
// on ouvre la connexion à la base de données
$connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// on veut qu'à chaque erreur de SGBD, une exception soit lancée
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// on démarre une transaction
$connexion->beginTransaction();
// on remplit la table des tranches d'impôt
$this->getTranches($connexion);
// on remplit la table des constantes
$this->getConstantes($connexion);
// on termine la transaction sur un succès
$connexion->commit();
} catch (\PDOException $ex) {
// y-a-t-il une transaction en cours ?
if (isset($connexion) && $connexion->inTransaction()) {
// on termine la transaction sur un échec
$connexion->rollBack();
}
// on remonte l'exception au code appelant
throw new ExceptionImpots($ex->getMessage());
} finally {
// on ferme la connexion
$connexion = NULL;
}
}
// lecture des données de la base
private function getTranches($connexion): void {
…
}
// lecture de la table des constantes
private function getConstantes($connexion): void {
…
}
// retourne les données permettant le calcul de l'impôt
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Ce code a été présenté au paragraphe lienLa classe [ImpotsWithTaxAdminDataInJsonFile].
XVIII-B-3. La couche [métier]▲
La couche [métier] implémente l’interface [InterfaceServerMetier] suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
<?php
// espace de noms
namespace Application;
interface InterfaceServerMetier {
// calcul des impôts d'un contribuable
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
L’interface [InterfaceServerMetier] est implémentée par la classe [ServerMetier] 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.
<?php
// espace de noms
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// couche Dao
private $dao;
// données administration fiscale
private $taxAdminData;
//---------------------------------------------
// setter couche [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $dao) {
// on mémorise une référence sur la couche [dao]
$this->dao = $dao;
// on récupère les données permettant le calcul de l'impôt
// la méthode [getTaxAdminData] peut lancer une exception ExceptionImpots
// on la laisse alors remonter au code appelant
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// calcul de l'impôt
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// résultat
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
…
// résultat
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=salaireAnnuel-abattement
// l'abattement a un min et un max
private function getRevenuImposable(float $salaire): float {
…
// résultat
return floor($revenuImposable);
}
// calcule une décôte éventuelle
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// résultat
return ceil($décôte);
}
// calcule une réduction éventuelle
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
..
// résultat
return ceil($réduction);
}
}
Ce code a déjà été vu et commenté dès la version 1 au paragraphe lienL’algorithme. Sa version objet avec une base de données a été présentée au paragraphe lienLa couche [métier].
XVIII-B-4. Le script serveur▲
Le script serveur implémente la couche [web] [4]. Le script [impots-server] est configuré par le fichier jSON [config-server.json] suivant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
"databaseFilename": "Data/database.json",
"taxAdminDataFileName": "Data/taxadmindata.json",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/ExceptionImpots.php",
"/Entities/TaxAdminData.php",
"/Entities/Database.php",
"/Dao/InterfaceServerDao.php",
"/Dao/ServerDao.php",
"/Métier/InterfaceServerMetier.php",
"/Métier/ServerMetier.php"
],
"absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
"users": [
{
"login": "admin",
"passwd": "admin"
}
]
}
- ligne 1 : le dossier racine à partir duquel les chemins de fichiers seront mesurés ;
- ligne 2 : le fichier jSON de configuration de la base de données MySQL ;
- ligne 3 : le fichier jSON des données de l’administration fiscale ;
- lignes 5-14 : les fichiers de l’application ;
- ligne 15 : la dépendance nécessaire aux bibliothèques tierces, ici Symfony ;
- lignes 16-20 : le tableau des utilisateurs autorisés à utiliser l’application ;
Les fichiers jSON [database.json, taxadmindata.json] sont ceux de la version 5 décrit au paragraphe lienLes entités.
Le script [impots-server] implémente la couche [web] 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.
148.
149.
150.
151.
<?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");
// on récupère la configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// on inclut les dépendances nécessaires au script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// dépendances absolues (bibliothèques tierces)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// définition des constantes
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// dépendances Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// préparation de la réponse JSON du serveur
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// on récupère la requête courante
$request = Request::createFromGlobals();
// authentification
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// l'utilisateur existe-t-il ?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// on fixe le code de statut de la réponse
if (!$trouvé) {
// pas trouvé - code 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// msg d'erreur
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// fin
exit;
}
// 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());
$erreur = $method !== "get" || $request->query->count() != 3;
// erreur ?
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// on récupère le statut marital
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// erreur ?
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// on récupère le nombre d'enfants
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// le nombre d'enfants doit être un nombre entier >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// erreur ?
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// on récupère le salaire annuel
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// le salaire doit être un nombre entier >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// erreur ?
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// autres paramètres dans la requête ?
foreach (\array_keys($request->query->all()) as $key) {
// paramètre valide ?
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// erreurs ?
if ($erreurs) {
// on envoie un code d'erreur 400 au client
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// on a tout ce qu'il faut pour travailler
// création de l'architecture du serveur
$msgErreur = "";
try {
// création de la couche [dao]
$dao = new ServerDao($config["databaseFilename"]);
// création de la couche [métier]
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// on note l'erreur
$msgErreur = utf8_encode($ex->getMessage());
}
// erreur ?
if ($msgErreur) {
// on envoie un code d'erreur 500 au client
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// calcul de l'impôt
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// on rend la réponse
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
Commentaires
- ligne 16 : on exploite le fichier de configuration ;
- lignes 18-26 : on charge toutes les dépendances ;
- ligne 29 : le nom du fichier [database.json] ;
- lignes 32-33 : on déclare les classes des bibliothèques tierces qu’on va utiliser ;
- lignes 36-38 : on prépare une réponse jSON ;
- lignes 40-52 : on vérifie que l’utilisateur qui fait la requête fait bien partie des utilisateurs autorisés ;
- lignes 54-63 : si ce n’est pas le cas, on envoie le code HTTP 401 qui indique un refus d’accès. A réception de ce code et de l’entête HTTP [WWW-Authenticate => Basic realm=], la plupart des navigateurs affichent une fenêtre d’authentification invitant l’utilisateur à s’authentifier ;
- ligne 59 : la réponse jSON du serveur explique la cause de l’erreur. Toutes les réponses du serveur seront la chaîne jSON d’un tableau [‘réponse’=>’qq chose’] ;
- lignes 64-117 : on vérifie la validité de la requête :
- une requête GET avec trois paramètres exactement ;
- un paramètre [marié] dont la valeur doit être ‘oui’ ou ‘non’ ;
- un paramètre [enfants] dont la valeur doit être un entier >=0 ;
- un paramètre [salaire] dont la valeur doit être un entier >=0 ;
- ligne 65 : à chaque fois qu’une erreur est détectée, un message d’erreur est ajouté au tableau [$erreurs] ;
- lignes 120-126 : si erreur il y a, alors on envoie le code HTTP [400 Bad Request] au client (ligne 122) ;
- ligne 123 : la réponse jSON du serveur explique la cause de l’erreur ;
- à partir de la ligne 132, tout a été vérifié. On peut instancier les couches [dao, métier]. Cette instanciation a un coût et il ne faut la faire que si on est sûr d’avoir une requête valide ;
- lignes 130-138 : on crée l’architecture du serveur. La construction de la couche [dao] peut lancer une exception de type [ExceptionImpots]. Si cette exception se produit, on note l’erreur ;
- lignes 135-138 : si exception il y a eu, alors on envoie le code HTTP 500 au client. Ce code signifie que le serveur a bogué ;
- ligne 143 : la réponse explique la cause de l’erreur ;
- ligne 148 : le calcul de l’impôt est délégué à la couche [métier] ;
- lignes 150-151 : envoi de la réponse ;
Testons ce script avec un navigateur. Demandons l’URL sécurisée [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:
- en [1], l’URL sécurisée demandée ;
- en [2], les trois paramètres [marié, enfants, salaire] ;
- en [3], le serveur Apache de Laragon a envoyé un certificat SSL autosigné. Le navigateur l’a remarqué et affiche un avertissement de sécurité : il considère que le site du serveur n’est pas digne de confiance ;
- en [4], on continue ;
- en [6], on continue ;
- en [7], le navigateur affiche une fenêtre pour que l’utilisateur puisse s’authentifier ;
- en [9,10], on tape [admin] et [admin] ;
- en [13], la réponse jSON du serveur ;
Faisons quelques tests d’erreur :
On demande l’URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]
On obtient le résultat suivant :
On coupe le SGBD MySQL et on demande l’URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000] :
XVIII-B-5. Tests [Codeception]▲
A chaque fois que nous construirons une nouvelle version du serveur, nous testerons les couches [métier] et [dao] comme il a été fait depuis la version 04 (cf paragraphes lienTests de la couche [dao] et lienTests de la couche [métier]).
Tout d’abord, nous associons le projet [scripts-web] aux tests [Codeception]. Pour cela, suivez la même procédure suivie pour le projet [scripts-console] au paragraphe lienInstallation du framework [Codeception]. Nous obtenons un projet [scripts-web] avec un dossier [Test Files] :
Nous allons créer un test pour la couche [dao] et un pour la couche [métier].
XVIII-B-5-a. Tests de la couche [dao]▲

Le test [ServerDaoTest] sera le suivant :
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.
<?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:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// chemin du fichier de configuration
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// on récupère la configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// on inclut les dépendances nécessaires au script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// dépendances absolues (bibliothèques tierces)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// parent
parent::__construct();
// on récupère la configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// création de la couche [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
Commentaires
- lignes 9-24 : on construit le même environnement de travail que celui du serveur [impots-server.php]. Cela se fait en lignes 9-12 avec la définitions des deux constantes dont dépend l’environnement ;
- lignes 32-40 : on construit une instance de la couche [dao] à tester comme il était fait dans le script serveur [impots-server.php] ;
- à partir de maintenant on est dans les mêmes conditions que le script serveur [impots-server.php] : on peut démarrer les tests ;
- lignes 43-45 : la méthode [testTaxAdminData] est celle décrite au paragraphe lienTests de la couche [dao] ;
Les résultats du test sont les suivants :
XVIII-B-5-b. Tests de la couche [métier]▲

Le test [ServerMetierTest] sera le suivant :
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.
<?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:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// chemin du fichier de configuration
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// on récupère la configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// on inclut les dépendances nécessaires au script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// dépendances absolues (bibliothèques tierces)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// classe de test
class ServerMetierTest extends \Codeception\Test\Unit {
// couche métier
private $métier;
public function __construct() {
parent::__construct();
// on récupère la configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// création de la couche [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// création de la couche [métier]
$this->métier = new ServerMetier($dao);
}
// tests
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
Commentaires
- lignes 9-24 : on construit le même environnement de travail que celui du serveur [impots-server.php]. Cela se fait en lignes 9-12 avec la définitions des deux constantes dont dépend l’environnement ;
- lignes 30-38 : on construit une instance de la couche [métier] à tester comme il était fait dans le script serveur [impots-server.php] ;
- à partir de maintenant on est dans les mêmes conditions que le script serveur [impots-server.php] : on peut démarrer les tests ;
- lignes 40-53 : les méthodes [test1, test2…, test11] sont celles décrites au paragraphe lienTests de la couche [métier] ;
Les résultats du test sont les suivants :
XVIII-C. Le client▲
Nous nous intéressons à la partie cliente de l’application.
Cette architecture sera implémentée par les scripts suivants :
XVIII-C-1. Les entités échangées entre couches▲
Les entités ci-dessus ont toutes été décrites et déjà utilisées :
- [BaseEntity] au paragraphe lienLa classe de base [BaseEntity] ;
- [ExceptionImpots] au paragraphe lienL’exception [ExceptionImpots] ;
- [TaxPayerData] au paragraphe lienL’entité [TaxPayerData] ;
XVIII-C-2. La couche [dao]▲
La couche [dao] implémente l’interface [InterfaceClientDao] suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<?php
// espace de noms
namespace Application;
interface InterfaceClientDao {
// lecture des données contribuables
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// calcul des impôts d'un contribuable
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// enregistrement des résultats
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- ligne 9 : la fonction [getTaxPayersData] amène en mémoire les données des contribuables du fichier [$taxPayersFilename]. Si erreurs il y a, elles sont consignées dans le fichier [$errorsFilename] ;
- ligne 12 : la fonction [calculerImpots] calcule l’impôt d’un contribuable ;
- ligne 15 : la fonction [saveResults] sauvegarde dans le fichier [$resultsFilename] les données du tableau [$taxPayersData] qui représentent les résultats de plusieurs calculs d’impôt ;
L’interface [InterfaceClientDao] est implémentée par la classe [ClientDao] 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.
<?php
namespace Application;
// dépendances
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// utilisation d'un Trait
use TraitDao;
// attributs
private $urlServer;
private $user;
// constructeur
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// calcul de l'impôt
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// on crée un client HTTP
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// on fait la requête au serveur
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// on récupère la réponse
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logs
// print "$json=json\n";
// on récupère le statut de la réponse
$statusCode = $response->getStatusCode();
// erreur ?
if ($statusCode !== 200) {
// on a une erreur - on lance une exception
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// on rend la réponse
return $réponse;
}
}
Commentaires
- ligne 10 : on insère [TraitDao] (cf paragraphe lienLe trait [TraitDao]) qui implémente les méthodes [getTaxPayersData] et [saveResults]. Ne reste donc que la méthode [calculerImpots] à implémenter. Celle-ci est implémentée aux lignes 22-49 ;
- lignes 16-19 : le constructeur de la classe [ClientDao] reçoit deux paramètres :
- l’URL [$urlServer] du serveur de calcul d’impôt ;
- le tableau [$user] de clés ‘login’ et ‘passwd’ qui définit l’utilisateur qui fait la requête ;
- ligne 22 : la méthode [calculerImpots] reçoit les trois paramètres à envoyer au serveur de calcul d’impôts ;
- lignes 24-27 : on crée un client HTTP avec :
- ligne 25 : les identifiants de l’utilisateur qui fait la requête ;
- ligne 26 : l’option qui fait que le client HTTP ne vérifiera pas la validité du certificat SSL envoyé par le serveur ;
- lignes 29-34 : le serveur est interrogé avec les trois paramètres qu’il attend ;
- ligne 36 : on récupère la réponse jSON du serveur. Si on ne met pas le paramètre [false] à la méthode [Response::getContent], alors si le statut de la réponse du serveur est dans l’intervalle [3xx-5xx] (cas d’erreur), l’objet [Response] lance une exception dès qu’on cherche à obtenir le contenu de la réponse [Response::getContent] ou ses entêtes HTTP [Response::getHeaders]. Ici quelque soit le statut HTTP de la réponse, on veut pouvoir avoir accès au contenu de celle-ci, ne serait-ce que pour le loguer (ligne 40) ;
- lignes 37-38 : la réponse du serveur est la chaîne jSON d’un tableau [‘réponse’=>qqChose]. On récupère le [qqChose] ;
- ligne 40 : on logue la réponse jSON en mode développement ;
- ligne 42 : on récupère le code de statut de la réponse ;
- lignes 44-49 : si le code de statut HTTP n’est pas 200, alors c’est que notre serveur a rencontré un problème. On lance alors une exception de type [ExceptionImpots] avec pour message, la réponse jSON du serveur augmentée du code HTTP de la réponse ;
- ligne 51 : on rend le résultat qui est un tableau associatif avec les clés [impôt, surcôte, décôte, réduction, taux] ;
XVIII-C-3. La couche [métier]▲
La couche [métier] [8] implémente l’interface [InterfaceClientMetier] suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<?php
// espace de noms
namespace Application;
interface InterfaceClientMetier {
// calcul des impôts d'un contribuable
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// calcul des impôts en mode batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- ligne 9 : la fonction [calculerImpots] calcule l’impôt ;
- ligne 12 : la fonction [executeBatchImpots] calcule l’impôt des contribuables dont les données sont dans le fichier [$taxPayersFileName], met les résultats obtenus dans le fichier [$resultsFileName] et les erreurs rencontrées dans le fichier [$errorsFileName] ;
L’interface [InterfaceClientMetier] est implémentée par la classe [ClientMetier] 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.
<?php
// espace de noms
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// attribut
private $clientDao;
// constructeur
public function __construct(InterfaceClientDao $clientDao) {
// on mémorise la référence sur la couche [dao]
$this->clientDao = $clientDao;
}
// calcul de l'impôt
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// calcul des impôts en mode batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// on laisse remonter les exceptions qui proviennent de la couche [dao]
// on récupère les données contribuables
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tableau des résultats
$results = [];
// on les exploite
foreach ($taxPayersData as $taxPayerData) {
// on calcule l'impôt
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// on complète [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// on met le résultat dans le tableau des résultats
$results [] = $taxPayerData;
}
// enregistrement des résultats
$this->clientDao->saveResults($resultsFileName, $results);
}
}
Commentaires
- lignes 11-14 : le constructeur de la classe [ClientMetier] reçoit comme paramètre une référence sur la couche [dao] ;
- lignes 17-19 : le calcul de l’impôt est délégué à la couche [dao] ;
- lignes 20-38 : la fonction [executeBatchImpots] a été décrite au paragraphe lienLa classe [Metier] ;
XVIII-C-4. Le script principal▲
Le script client [MainImpotsClient.php] implémente la couche [console] [9]. Il est configuré par le fichier jSON [conf-client.json] suivant :
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-08",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": [
"Entities/BaseEntity.php",
"Entities/TaxPayerData.php",
"Entities/ExceptionImpots.php",
"Utilities/Utilitaires.php",
"Dao/InterfaceClientDao.php",
"Dao/TraitDao.php",
"Dao/ClientDao.php",
"Métier/InterfaceClientMetier.php",
"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-08/impots-server.php"
}
- ligne 1 : le dossier racine du client ;
- ligne 2 : le fichier jSON des données contribuables ;
- ligne 3 : le fichier jSON des résultats ;
- ligen 4 : le fichier jSON des erreurs ;
- lignes 6-19 : les différentes dépendances du projet client ;
- lignes 20-23 : l’utilisateur faisant les requêtes au serveur de calcul d’impôts ;
- ligne 24 : l’URL sécurisée du serveur de calcul d’impôts ;
Le code du script [MainImpotsClient.php] est le suivant :
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.
<?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-client.json");
// on récupère la configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// on inclut les dépendances nécessaires au script
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// dépendances absolues (bibliothèques tierces)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// définition des constantes
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// dépendances Symfony
use Symfony\Component\HttpClient\HttpClient;
// création de la couche [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// création de la couche [métier]
$clientMetier = new ClientMetier($clientDao);
// calcul de l'impôts en mode batch
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// on affiche l'erreur
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;
Commentaires
- ligne 13 : chemin du fichier de configuration ;
- ligne 16 : exploitation du fichier de configuration ;
- lignes 18-26 : chargement des dépendances ;
- ligne 37 : création de la couche [dao]. On passe au constructeur de la couche, les deux informations qu’il attend :
- l’URL du serveur de calcul d’impôts ;
- les identifiants de l’utilisateur qui va faire les requêtes ;
- ligne 39 : création de la couche [métier]. On passe au constructeur de la couche, une référence sur la couche [dao] qui vient d’être créée ;
- ligne 43 : on demande à la couche [métier] de :
- calculer les impôts de tous les contribuables du fichier $config["taxPayerDataFileName"] ;
- mettre les résultats dans le fichier $config["resultsFileName"] ;
- mettre les erreurs dans le fichier $config["errorsFileName"] ;
- la ligne 43 peut lancer des exceptions ;
- ligne 46 : affichage du message d’erreur de l’exception ;
L’exécution du client amène les mêmes résultats que les versions précédentes. Vérifiez les fichiers suivants :
- [Data/taxpayersdata.json] : données des contribuables pour lesquel on calcule le montant de l’impôt ;
- [Data/results.json] : résultats pour les différents contribuables du fichier [Data/taxpayersdata.json] ;
- [Data/errors.json] : les erreurs qui ont pu être rencontrées dans l’exploitation du fichier [Data/taxpayersdata.json] ;
Regardons les cas d’erreur possibles. Tout d’abord, arrêtons le serveur Laragon. Les résultats dans la console du client sont alors les suivants :
2.
Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé
Maintenant lançons seulement le serveur Apache et pas le SGBD MySQL :
Les résultats dans la console du client sont alors les suivants :
2.
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é
Maintenant, lançons MySQL puis modifions dans [config-client] l’utilisateur qui se connecte :
2.
3.
4.
"user": {
"login": "x",
"passwd": "x"
},
Les résultats dans la console du client sont alors les suivants :
2.
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
XVIII-C-5. Tests [Codeception]▲
Comme il a été fait pour les version précédentes, nous allons écrire des tests [Codeception] pour la version 08.
XVIII-C-5-a. Test de la couche [métier]▲
Le test [ClientMetierTest.php] est le suivant :
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.
<?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-08");
// chemin du fichier de configuration
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// on récupère la configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// on inclut les dépendances nécessaires au script
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// dépendances absolues (bibliothèques tierces)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// classe de test
class ClientMetierTest extends \Codeception\Test\Unit {
// couche métier
private $métier;
public function __construct() {
parent::__construct();
// on récupère la configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// création de la couche [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// création de la couche [métier]
$this->métier = new ClientMetier($clientDao);
}
// tests
public function test1() {
…
}
-------------
public function test11() {
…
}
}
Commentaires
- lignes 10-26 : définition de l’environnement du test. Nous utilisons le même que celui utilisé par le script principal [MainImpotsClient] décrit au paragraphe lienLe script principal ;
- lignes 33-41 : construction des couches [dao] et [métier] ;
- ligne 40 : l’attribut [$this->métier] référence la couche [métier] ;
- lignes 44-51 : les méthodes [test1, test2…, test11] sont celles décrites au paragraphe lienTests de la couche [métier] ;
Les résultats du test sont les suivants :


























