INTRODUCTION AU LANGAGE PHP7 PAR L’EXEMPLE¶
Auteur¶
Serge Tahé, juillet 2019, https://sergetahe.com
Licence¶
Téléchargements¶
Téléchargement du PDF du cours
Téléchargement des exemples du cours (rar)
Présentation du cours¶
Ce document fait partie d’une série de quatre articles :
- [Introduction au langage PHP7 par l’exemple]. C’est le document présent ;
- [Introduction au langage ECMASCRIPT 6 par l’exemple] ;
- [Introduction au framework VUE.JS par l’exemple] ;
- [Introduction au framework NUXT.JS par l’exemple] ;
Ce sont tous des documents pour débutants. Les articles ont une suite logique mais sont faiblement couplés :
- le document [1] présente le langage PHP 7. Le lecteur seulement intéressé par le langage PHP et pas par le langage Javascript des articles suivants s’arrêtera là ;
- les documents [2-4] visent à construire un client Javascript au serveur de calcul de l’impôt développé dans le document [1] ;
- les frameworks Javascript [vue.js] et [nuxt.js] des articles 3 et 4 nécessitent de connaître le Javascript des dernières versions d’ECMASCRIPT, celles de la version 6. Le document [2] est donc destiné à ceux qui ne connaissent pas cette version de Javascript. Il fait référence au serveur de calcul de l’impôt construit dans le document [1]. Le lecteur de [2] aura alors parfois besoin de se référer au document [1] ;
- une fois ECMASCRIPT 6 maîtrisé, on peut aborder le framework VUE.JS qui permet de construire des clients Javascript s’exécutant dans un navigateur en mode SPA (Single Page Application). C’est le document [3]. Il fait référence à la fois au serveur de calcul de l’impôt construit dans le document [1] et au code du client Javascript autonome construit en [2]. Le lecteur de [3] aura alors parfois besoin de se référer aux documents [1] et [2] ;
- une fois VUE.JS maîtrisé, on peut aborder le framework NUXT.JS qui permet de construire des clients Javascript s’exécutant dans un navigateur en mode SSR (Server Side Rendered). Il fait référence à la fois au serveur de calcul de l’impôt construit dans le document [1], au code du client Javascript autonome construit en [2] ainsi qu’à l’application [vue.js] développée dans le document [3]. Le lecteur de [4] aura alors parfois besoin de se référer aux documents [1] [2] et [3] ;
Ce document propose une liste de scripts console PHP 7 dans différents domaines (structures du langage, accès aux fichiers, aux bases de données, au réseau internet). La programmation web est abordée via des services web. On a appelé dans ce document, service web, toute application web produisant du texte brut. Ce sont des serveurs de données et non des serveurs de pages web qui sont un mixte HTML, CSS et Javascript. On y aborde des concepts web classiques (protocole HTTP, réponses jSON ou XML, gestion de session, authentification) également utilisés dans la programmation web classique.
De nos jours, il est fréquent de construire des applications web en mode client / serveur :
- en [1], le navigateur web affiche des pages web à destination d’un utilisateur [5, 7]. Ces pages contiennent du Javascript implémentant un client d’un service web de données [2] ainsi qu’un client d’un serveur de fragments de pages web [3]. Un framework JS bien établi dans ce domaine est Angular 2 de Google (mai 2019) ;
- en [2], le serveur web est un serveur de données. Il peut être écrit dans n’importe quel langage. Il ne produit pas de pages web au sens classique (HTML, CSS, Javascript) sauf peut-être la 1ère fois. Mais cette 1ère page peut être obtenue d’un serveur web classique [3] (pas un serveur de données). Le Javascript de la page initiale va alors générer les différentes pages web de l’application en obtenant les données [4] à afficher auprès du serveur web qui agit comme un serveur de données [2]. Il peut également obtenir des fragments de page web [5] pour habiller ces données auprès du serveur de pages web [3] ;
- en [4], l’utilisateur initie une action ;
- en [6,7] : il reçoit des données habillées par un fragment de page web ;
Nous allons dans ce document écrire des applications client / serveur en PHP7 ayant la structure suivante :
On a là une application client / serveur écrite en PHP. Un script console [9] interrogera un serveur de données [4]. Ce qui sera appris ici pour écrire le service de données pourra être réutilisé dans une application web. Le service de données en PHP pourra être conservé et le client PHP sera lui remplacé par un client Javascript.
Comme fil rouge du document, nous construirons un service de calcul de l’impôt en 13 versions. La version 13 aura l’architecture suivante :
La couche [web] du serveur aura une architecture MVC (Model – View – Controller). Tout le cours PHP 7 vise à construire cette version.
Les scripts de ce document sont commentés et leur exécution console reproduite. Des explications supplémentaires sont parfois fournies. Le document nécessite une lecture active : pour comprendre un script, il faut à la fois lire son code, ses commentaires et ses résultats d’exécution.
Les exemples du document sont disponibles |ici|.
L’application serveur PHP 7 peut être testée |ici|.
Serge Tahé, juillet 2019
Installation d’un environnement de travail¶
Les scripts ont été écrits et testés dans l’environnement suivant :
- un environnement serveur web Apache / SGBD MySQL / PHP 7.3 appelé Laragon ;
- l’IDE de développement Netbeans 10.0 ;
Installation de Laragon¶
Laragon est un package réunissant plusieurs logiciels :
- un serveur web Apache. Nous l’utiliserons pour l’écriture de scripts web en PHP ;
- le SGBD MySQL ;
- le langage de script PHP ;
- un serveur Redis implémentant un cache pour des applications web :
Laragon peut être téléchargé (mars 2019) à l’adresse suivante :
- l’installation [1-5] donne naissance à l’arborescence suivante :
- en [6] le dossier d’installation de PHP ;
Le lancement de [Laragon] affiche la fenêtre suivante :
- [1] : le menu principal de Laragon ;
- [2] : le bouton [Start All] lance le serveur web Apache et le SGBD MySQL ;
- [3] : le bouton [WEB] affiche la page web [http://localhost] qui correspond au fichier PHP [<laragon>/www/index.php] où <laragon> est le dossier d’installation de Laragon ;
- [4] : le bouton [Database] permet de gérer le SGBD MySQL avec l’outil [phpMyAdmin]. Il faut auparavant installer celui-ci ;
- [5] : le bouton [Terminal] ouvre un terminal de commandes ;
- [6] : le bouton [Root] ouvre un explorateur Windows positionné sur le dossier [<laragon>/www] qui est la racine du site web [http://localhost]. C’est là qu’il faut placer toutes les applications web gérées par le serveur Apache de Laragon ;
Ouvrons un terminal Laragon [5] :
- en [1], le type du terminal. Trois types de terminaux sont disponibles en [6] ;
- en [2, 3] : le dossier courant ;
- en [4], on tape la commande [echo %PATH%] qui affiche la liste des dossiers explorés lors de la recherche d’un exécutable. Tous les principaux dossiers de Laragon sont inclus dans ce chemin des exécutables, ce qui ne serait pas le cas si on ouvrait une fenêtre de commandes [cmd] dans Windows. Dans ce document, lorsqu’on est amené à taper des commandes pour installer tel ou tel logiciel, c’est en général dans un terminal Laragon que ces commandes sont tapées ;
Installation de l’IDE Netbeans 10.0¶
L’IDE Netbeans 10.0 peut être téléchargé à l’adresse suivante (mars 2019) :
https://netbeans.apache.org/download/index.HTML
Le fichier téléchargé est un zip qu’il suffit de dézipper. Une fois Netbeans installé et lancé, on peut créer un premier projet PHP.
- en [1], prendre l’option File / New Project ;
- en [2], prendre la catégorie [PHP] ;
- en [3], prendre le type de projet [PHP Application] ;
- en [4], donner un nom au projet ;
- en [5], choisir un dossier pour le projet ;
- en [6], choisir la version de PHP téléchargée ;
- en [7], choisir l’encodage UTF-8 pour les fichiers PHP ;
- en [8], choisir le mode [Script] pour exécuter les scripts PHP en mode ligne de commande. Choisir [Local WEB Server] pour exécuter un script PHP dans un environnement web ;
- en [9,10], indiquer le répertoire d’installation de l’interpréteur PHP du package Laragon :
- choisir [Finish] pour terminer l’assistant de création du projet PHP ;
- en [11], le projet est créé avec un script [index.php] ;
- en [12], on écrit un script PHP minimal ;
- en [13], on exécute [index.php] ;
- en [14], les résultats dans la fenêtre [output] de Netbeans ;
- en [15], on crée un nouveau script ;
- en [16], le nouveau script ;
Le lecteur pourra créer tous les scripts qui vont suivre dans différents dossiers du même projet PHP. Les codes source des scripts de ce document sont disponibles sous la forme de l’arborescence Netbeans suivante :
Les scripts de ce document sont placés dans l’arborescence du projet [scripts-console] [1]. Nous allons utiliser également des bibliothèques PHP qui seront placées dans le dossier [<laragon-lite>/www/vendor] [2] où <laragon-lite> est le dossier d’installation du logiciel Laragon. Pour que Netbeans reconnaisse les bibliothèques de [2] comme faisant partie du projet [scripts-console], il nous faut inclure le dossier [vendor] [2] dans la branche [Include Path] [3] du projet. Nous allons configurer Netbeans pour que le dossier [<laragon-lite>/www/vendor] [2] soit inclus dans tout nouveau projet PHP et pas seulement dans le projet [scripts-console] :
- en [1-2], on va dans les options de Netbeans ;
- en [3-4], on configure les options de PHP ;
- en [5-7], on configure le [Global Include Path] de PHP : les dossiers indiqués en [7] sont automatiquement inclus dans le [Include Path] de tout projet PHP ;
- en [9], on accède aux propriétés de la branche [Include Path] ;
- en [10-11], les nouvelles bibliothèques explorées par Netbeans. Netbeans explore le code PHP de ces bibliothèques et mémorise leurs classes, interfaces, fonctions… afin de pouvoir proposer de l’aide au développeur ;
- en [12], un code utilise la classe [PhpMimeMailParserParser] de la bibliothèque [vendor/php-mime-mail-parser] ;
- en [13], Netbeans propose les méthodes de cette classe ;
- en [14-15], Netbeans affiche la documentation de la méthode sélectionnée ;
La notion d’[Include Path] est ici propre à Netbeans. PHP a également cette notion mais ce sont a priori deux notions différentes.
Maintenant que l’environnement de travail a été installé, nous pouvons aborder les bases de PHP.
Les bases de PHP¶
L’arborescence des scripts¶
Configuration de PHP¶
PHP arrive préconfiguré par un fichier texte [php.ini]. Toutes ces configurations peuvent être changées par programmation. La configuration de PHP influence grandement l’exécution des scripts. Il est donc important de la connaître. Le script suivant [phpinfo.php] le permet :
1 2 3 | <?php
phpinfo();
|
Commentaires
- ligne 3 : la fonction [phpinfo] affiche la configuration de PHP ;
Résultats de l’exécution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | "C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.exe" "C:\Data\st-2019\dev\php7\php5-exemples\exemples\tests\phpinfo.php"
phpinfo()
PHP Version => 7.2.11
System => Windows NT DESKTOP-528I5CU 10.0 build 17134 (Windows 10) AMD64
Build Date => Oct 10 2018 01:57:32
Compiler => MSVC15 (Visual C++ 2017)
Architecture => x64
Configure Command => cscript /nologo configure.js "--enable-snapshot-build" "--enable-debug-pack" "--with-pdo-oci=c:\php-snap-build\deps_aux\oracle\x64\instantclient_12_1\sdk,shared" "--with-oci8-12c=c:\php-snap-build\deps_aux\oracle\x64\instantclient_12_1\sdk,shared" "--enable-object-out-dir=../obj/" "--enable-com-dotnet=shared" "--without-analyzer" "--with-pgo"
Server API => Command Line Interface
Virtual Directory Support => enabled
Configuration File (php.ini) Path => C:\windows
Loaded Configuration File => C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.ini
Scan this dir for additional .ini files => (none)
Additional .ini files parsed => (none)
…
Done.
|
La fonction [phpinfo] affiche ici plus de 800 lignes de configuration. Nous n’allons pas les commenter car la plupart concerne un usage avancé de PHP. Une ligne importante est la ligne 13 ci-dessus : elle indique quel fichier [php.ini] a été utilisé pour configurer le PHP que vous allez utiliser pour exécuter vos scripts. Si vous souhaitez modifier la configuration d’exécution de PHP, c’est ce fichier qu’il vous faut modifier. De nombreux commentaires sont présents dans ce fichier pour expliquer le rôle des différentes configurations.
Un premier exemple¶
Le code¶
Ci-dessous, on trouvera un programme [bases-01.php] présentant les premières caractéristiques de PHP.
1 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 | <?php
// ceci est un commentaire
// variable utilisée sans avoir été déclarée
$nom = "dupont";
// un affichage écran
print "nom=$nom\n";
// un tableau avec des éléments de type différent
$tableau = array("un", "deux", 3, 4);
// son nombre d'éléments
$n = count($tableau);
// une boucle
for ($i = 0; $i < $n; $i++) {
print "tableau[$i]=$tableau[$i]\n";
}
// initialisation de 2 variables avec le contenu d'un tableau
list($chaine1, $chaine2) = array("chaine1", "chaine2");
// concaténation des 2 chaînes
$chaine3 = $chaine1 . $chaine2;
// affichage résultat
print "[$chaine1,$chaine2,$chaine3]\n";
// utilisation fonction
affiche($chaine1);
// le type d'une variable peut être connu
afficheType("n", $n);
afficheType("chaine1", $chaine1);
afficheType("tableau", $tableau);
// le type d'une variable peut changer en cours d'exécution
$n = "a changé";
afficheType("n", $n);
// une fonction peut rendre un résultat
$res1 = f1(4);
print "res1=$res1\n";
// une fonction peut rendre un tableau de valeurs
list($res1, $res2, $res3) = f2();
print "(res1,res2,res3)=[$res1,$res2,$res3]\n";
// on aurait pu récupérer ces valeurs dans un tableau
$t = f2();
for ($i = 0; $i < count($t); $i++) {
print "t[$i]=$t[$i]\n";
}
// des tests
for ($i = 0; $i < count($t); $i++) {
// n'affiche que les chaînes
if (getType($t[$i]) === "string") {
print "t[$i]=$t[$i]\n";
}
}
// opérateurs de comparaison == et ===
if("2"==2){
print "avec l'opérateur ==, la chaîne 2 est égale à l'entier 2\n";
}else{
print "avec l'opérateur ==, la chaîne 2 n'est pas égale à l'entier 2\n";
}
if("2"===2){
print "avec l'opérateur ===, la chaîne 2 est égale à l'entier 2\n";
}
else{
print "avec l'opérateur ===, la chaîne 2 n'est pas égale à l'entier 2\n";
}
// d'autres tests
for ($i = 0; $i < count($t); $i++) {
// n'affiche que les entiers >10
if (getType($t[$i]) === "integer" and $t[$i] > 10) {
print "t[$i]=$t[$i]\n";
}
}
// une boucle while
$t = [8, 5, 0, -2, 3, 4];
$i = 0;
$somme = 0;
while ($i < count($t) and $t[$i] > 0) {
print "t[$i]=$t[$i]\n";
$somme += $t[$i]; //$somme=$somme+$t[$i]
$i++; //$i=$i+1
}//while
print "somme=$somme\n";
// fin programme
exit;
//----------------------------------
function affiche($chaine) {
// affiche $chaine
print "chaine=$chaine\n";
}
//affiche
//----------------------------------
function afficheType($name, $variable) {
// affiche le type de $variable
print "type[variable $" . $name . "]=" . getType($variable) . "\n";
}
//afficheType
//----------------------------------
function f1($param) {
// ajoute 10 à $param
return $param + 10;
}
//----------------------------------
function f2() {
// rend 3 valeurs
return array("un", 0, 100);
}
?>
|
Les résultats :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | nom=dupont
tableau[0]=un
tableau[1]=deux
tableau[2]=3
tableau[3]=4
[chaine1,chaine2,chaine1chaine2]
chaine=chaine1
type[variable $n]=integer
type[variable $chaine1]=string
type[variable $tableau]=array
type[variable $n]=string
res1=14
(res1,res2,res3)=[un,0,100]
t[0]=un
t[1]=0
t[2]=100
t[0]=un
avec l'opérateur ==, la chaîne 2 est égale à l'entier 2
avec l'opérateur ===, la chaîne 2 n'est pas égale à l'entier 2
t[2]=100
t[0]=8
t[1]=5
somme=13
|
Commentaires
- ligne 5 : en PHP, on ne déclare pas le type des variables. Celles-ci ont un type dynamique qui peut varier au cours du temps. $nom représente la variable d’identifiant nom ;
- ligne 7 : pour écrire à l’écran, on peut utiliser l’instruction print ou l’instruction echo ;
- ligne 9 : le mot clé array permet de définir un tableau. La variable $nom***[$i]* représente l’élément $i du tableau $tableau ;
- ligne 11 : la fonction count($tableau) rend le nombre d’éléments du tableau $tableau ;
- lignes 13-15 : une boucle. Celle-ci n’ayant qu’une instruction, les accolades sont alors facultatives. Dans la suite de ce document, nous mettrons systématiquement les accolades quelque soit le nombre d’instructions ;
- ligne 14 : les chaînes de caractères sont entourées de guillemets » ou d’apostrophes “. A l’intérieur de guillemets, les variables $variable sont évaluées mais pas à l’intérieur d’apostrophes ;
- ligne 17 : la fonction list permet de rassembler des variables dans une liste et de leur attribuer une valeur avec une unique opération d’affectation. Ici $chaine1= »chaine1 » et $chaine2= »chaine2 » ;
- ligne 19 : l’opérateur . est l’opérateur de concaténation de chaînes ;
- lignes 83-86 : le mot clé function définit une fonction. Une fonction rend ou non des valeurs par l’instruction return. Le code appelant peut ignorer ou récupérer les résultats d’une fonction. Une fonction peut être définie n’importe où dans le code.
- ligne 92 : la fonction prédéfinie getType($variable) rend une chaîne de caractères représentant le type de $variable. Ce type peut changer au cours du temps ;
- ligne 45 : l’opérateur === compare deux éléments de façon stricte : il faut qu’ils aient le même type pour être comparés. L’opérateur == est moins strict : deux éléments peuvent être égaux sans être du même type. C’est ce que montrent les instructions des lignes 50-60. Dans le cas de l’opérateur ==, la comparaison se fait après transtypage des deux éléments comparés dans un même type. Des conversions implicites ont alors lieu. Il est assez facile « d’oublier » la présence de ces conversions implicites et d’aboutir ainsi à des résultats imprévus, tels que de découvrir qu’une condition est vraie alors que vous l’attendiez fausse. Pour éviter cet écueil, nous utiliserons systématiquement l’opérateur de compraison === ;
- ligne 64 : on peut utiliser également les opérateurs booléens or et ! ;
- ligne 69 : à la place de la notation array(), on peut utiliser la notation [] pour initialiser un tableau depuis PHP7 ;
- ligne 80 : la fonction prédéfinie exit arrête l’exécution du script ;
- ligne 107 : la balise ?> signale la fin du script PHP. Elle n’est pas indispensable. De plus, dans un contexte web, elle peut poser problème si elle est suivie par des espaces ou des marques de fin de ligne, difficiles à détecter car non visibles dans un éditeur de texte. Aussi dans la suite du document, nous omettrons systématiquement cette balise ;
Note : dans ce document, nous utiliserons le mot clé [print] pour afficher du texte sur la console. Une autre méthode pour faire la même chose est d’utiliser le mot clé [echo]. Il y a de subtiles différences entre ces deux mots clés, mais dans le contexte de ce document il n’y en aura aucune. Si donc vous préférez utiliser [echo], alors faites-le.
Usage de Netbeans¶
Netbeans émet divers avertissements qu’il est utile de vérifier. Prenons l’exemple du script [bases-01.php] :
A la ligne 5, Netbeans émet un avertissement comme quoi le fichier ne respecte pas la recommandation PSR-1 (PHP Standard Recommendations n° 1). Les PSR sont des recommandations pour produire un code standard et ainsi faciliter l’interopérabilité et la maintenance de codes écrits par différentes personnes. Il peut être ennuyeux d’avoir des avertissements si on veut transgresser délibérément les standards, parce que par exemple l’équipe du projet en a d’autres. Ce que l’on souhaite vérifier ou pas avec Netbeans est configurable :
- en [5], on trouve les éléments que l’on veut contrôler avec Netbeans ;
- en [6], le niveau de sévérité assigné à l’erreur signalée par Netbeans ;
On voit en [7] qu’on a demandé à contrôler que le code suit bien les recommandations PSR-0 et PSR-1. Il n’y a rien d’obligatoire. En phase d’apprentissage du langage, ll est conseillé de contrôler le maximum d’options proposées par Netbeans. On apprend ainsi beaucoup de choses. On adaptera ensuite ce contrôle de Netbeans aux normes de codage de l’équipe d’un projet.
Voyons les normes de codage PSR-1 [8, 9] :
Option | Contrôle |
---|---|
|
Class constants MUST be declared in all upper case with underscore separators. Ex : const TAUX_TVA |
|
Method names MUST be declared in camelCase(). Ex : public function executeBatchImpots{} |
|
Property names SHOULD be declared in $StudlyCaps, $camelCase, or $under_score format (consistently in a scope) Ex : public SalaireAnnuel (StudlyCaps), public salaireAnnuel (camelCase), public salaire_annuel (under_score) |
|
A file SHOULD declare new symbols and cause no other side effects, or it SHOULD execute logic with side effects, but SHOULD NOT do both. |
|
Type names MUST be declared in StudlyCaps (Code written for 5.2.x and before SHOULD use the pseudonamespacing convention of Vendor_ prefixes on type names). Each type is in a file by itself, and is in a namespace of at least one level: a top-level vendor name. Ex : class EtudiantBoursier {} |
La recommandation PSR-1 / 4 dit que dans un fichier PHP, on doit trouver :
- soit la déclaration d’un type (classes, interface) ;
- soit du code exécutable sans déclaration de nouveaux types ;
Il existe d’autres recommandations PHP non contrôlées par Netbeans : PSR-3, PSR-4, PSR-6, PSR-7 et PSR-13.
Par facilité, les exemples du document ne vérifient pas tous la recommandation PSR-1 car cela oblige à dispatcher le code des classes et interfaces dans des fichiers séparés, ce qui est trop lourd pour des exemples basiques. Il est alors plus facile de tout mettre dans un fichier. Pour l’exemple de l’application présentée comme fil rouge de ce document, on a cherché à observer au mieux la recommandation PSR-1.
Certains avertissements de Netbeans signalent une erreur potentielle :
L’avertissement [Unitialized Variables] signale une erreur probable, souvent une erreur de frappe sur le nom d’une variable. Il en est de même pour l’avertissement [Unused Variables].
Finalement, il est conseillé de vérifier tous les avertissements de Netbeans signalés par un panneau dans la marge gauche du code et un tiret jaune dans la marge droite :
La portée des variables¶
Exemple 1¶
Le script [bases-02.php] est le suivant :
1 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 | <?php
// portée des variables
function f1() {
// on utilise la variable globale $i
global $i;
$i++;
$j = 10;
print "f1[i,j]=[$i,$j]\n";
}
function f2() {
// on utilise la variable globale $i
global $i;
$i++;
$j = 20;
print "f2[i,j]=[$i,$j]\n";
}
function f3() {
// on utilise une variable locale $i
$i = 4;
$j = 30;
print "f3[i,j]=[$i,$j]\n";
}
// tests
$i = 0;
$j = 0; // ces deux variables ne sont connues d'une fonction f
// que si celle-ci déclare explicitement par l'instruction global
// qu'elle veut les utiliser
f1();
f2();
f3();
print "test[i,j]=[$i,$j]\n";
|
Les résultats :
1 2 3 4 | f1[i,j]=[1,10]
f2[i,j]=[2,20]
f3[i,j]=[4,30]
test[i,j]=[2,0]
|
Commentaires
lignes 29-30 : définissent 2 variables $i et $j du programme principal. Ces variables ne sont pas connues à l’intérieur des fonctions. Ainsi, ligne 9, la variable $j de la fonction f1 est une variable locale à la fonction f1 et est différente de la variable $j du programme principal. Une fonction peut accéder à une variable $variable du programme principal via le mot clé global ;
ligne 7, l’instruction désigne la variable globale $i du programme principal ;
Exemple 3
Le script [bases-03.php] est le suivant :
1 2 3 4 5 6 7 8 | <?php
// la portée d'une variable est globale aux blocs de code
$i = 0; {
$i = 4;
$i++;
}
print "i=$i\n";
|
Les résultats :
1 | i=5
|
Commentaires
Dans certains langages, une variable définie à l’intérieur d’accolades a la portée de celles-ci : elle n’est pas connue à l’extérieur de celles-ci. Les résultats ci-dessus montrent qu’il n’en est rien en PHP. La variable $i définie ligne 5 à l’intérieur des accolades est la même que celle utilisée lignes 4 et 8 à l’extérieur de celles-ci.
Les changements de types¶
Les variables en PHP n’ont pas un type constant. Celui-ci peut changer en cours d’exécution selon la valeur affectée à la variable. Dans des opérations impliquant des données de divers types, l’interpréteur PHP fait des conversions implicites pour ramener les opérandes dans un type commun. Ces conversions implicites, si elles ne sont pas connues du développeur, peuvent être une source d’erreurs difficiles à repérer. On présente ci-dessous, un script [bases-04.php] montrant des conversions implicites et explicites :
1 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 | <?php
// types stricts dans le passage des paramètres
declare(strict_types=1);
// changements implicites de types
// type -->bool
print "Conversion vers un booléen------------------------------\n";
showBool("abcd", "abcd");
showBool("", "");
showBool("[1, 2, 3]", [1, 2, 3]);
showBool("[]", []);
showBool("NULL", NULL);
showBool("0.0", 0.0);
showBool("0", 0);
showBool("4.6", 4.6);
function showBool(string $prefixe, $var) : void {
print "(bool) $prefixe : ";
// la conversion de $var en booléen se fait automatiquement dans le test qui suit
if ($var) {
print "true";
} else {
print "false";
}
print "\n";
}
|
Commentaires
- ligne 4 : demande une vérification stricte du type des paramètres d’une fonction lorsque celui-ci est précisé ;
- ligne 18 : la fonction [showBool] a pour but de montrer la transformation implicite (automatique) que fait l’interpréteur PHP lorsqu’une valeur de tout type doit être transformée en booléen (ligne 21) ;
- ligne 18 : la paramètre $var n’a pas de type assigné. Le paramètre effectif pourra donc être de tout type. Le paramètre $prefixe lui devra être de type string. La fonction showBool ne rend aucune valeur (void) ;
- ligne 21 : dans l’instruction if($var), la valeur de $var doit être transformée en booléen pour que le if soit évalué. De façon étonnante l’interpréteur PHP a une réponse pour tout type de valeur qu’on lui donne ;
- lignes 9-16 : la valeur du paramètre de la fonction [showBool]
sera successivement :
- ligne 9 : une chaîne non vide : résultat TRUE (la casse n’est pas importante, TRUE=true) ;
- ligne 10 : une chaîne vide : résultat FALSE ;
- ligne 11 : un tableau non vide : résultat TRUE ;
- ligne 12 : un tableau vide : résultat FALSE ;
- ligne 14 : le nombre réel 0 : résultat FALSE ;
- ligne 15 : le nombre entier 0 : résultat FALSE ;
- ligne 16 : le nombre réel (ou entier) différent de 0 : résultat TRUE ;
C’est ce que montrent les affichages écrans obtenus :
1 2 3 4 5 6 7 8 9 | Conversion vers un booléen------------------------------
(bool) abcd : true
(bool) : false
(bool) [1, 2, 3] : true
(bool) [] : false
(bool) NULL : false
(bool) 0.0 : false
(bool) 0 : false
(bool) 4.6 : true
|
Continuons le code du script :
1 2 3 4 5 6 7 8 9 10 11 12 | // changements implicites de type string vers un type numérique
// string --> nombre
print "Conversion chaîne vers nombre------------------------------\n";
showNumber("12");
showNumber("45.67");
showNumber("abcd");
function showNumber(string $var) : void {
$nombre = $var + 1;
var_dump($nombre);
print "($var): $nombre\n";
}
|
Commentaires
- ligne 9 : la fonction [showNumber] admet un paramètre de type string et ne rend pas de résultat (void) ;
- ligne 10 : ce paramètre est utilisé dans une opération arithmétique,
ce qui va forcer l’interpréteur PHP à essayer de transformer $var en
nombre ;
- ligne 5 : va transformer la chaîne “12” en nombre entier 12 ;
- ligne 6 : va transformer la chaîne “45.67” en nombre réel 45.67 ;
- ligne 7 : va émettre un avertissement mais va quand même transformer la chaîne “abcd” en nombre 0 ;
Voici les résultats de l’exécution :
1 2 3 4 5 6 7 8 9 | Conversion chaîne vers nombre------------------------------
int(13)
(12): 13
float(46.67)
(45.67): 46.67
Warning: A non-numeric value encountered in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_031.php on line 37
int(1)
(abcd): 1
|
Continuons le code du script :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // changements explicites de type
// vers int
showInt("12.45");
showInt(67.8);
showInt(TRUE);
showInt(NULL);
function showInt($var) : void {
print "paramètre : ";
var_dump($var);
print "\n";
print "résultat de la conversion : ";
var_dump((int) $var);
print "\n";
}
|
Commentaires
- ligne 21 : la fonction [showInt] reçoit un paramètre de n’importe quel type et ne rend pas de résultat. Elle essaie de convertir le paramètre $var en entier ligne 26. De façon générale, pour changer une variable $var en un type T, on écrit (T) $var où T peut être : int, integer, bool, boolean, float, double, real, string, array, object, unset ;
- ligne 16 : convertit la chaîne “12.45” en entier 12 ;
- ligne 17 : convertit le réel 67.8 en entier 67 ;
- ligne 18 : convertit le booléen TRUE en l’entier 1 (le booléen FALSE en l’entier 0) ;
- ligne 19 : convertit le pointeur NULL en entier 0 ;
C’est ce que montrent les affichages écran :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | paramètre : string(5) "12.45"
résultat de la conversion : int(12)
paramètre : float(67.8)
résultat de la conversion : int(67)
paramètre : bool(true)
résultat de la conversion : int(1)
paramètre : NULL
résultat de la conversion : int(0)
|
Nous continuons l’étude du script avec la conversion explicite de valeurs en type float :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // vers float
showFloat("12.45");
showFloat(67);
showFloat(TRUE);
showFloat(NULL);
function showFloat($var) : void {
print "paramètre : ";
var_dump($var);
print "\n";
print "résultat de la conversion : ";
var_dump((float) $var);
print "\n";
}
|
Commentaires
- ligne 35 : la fonction [showFloat] reçoit un paramètre de type quelconque et ne rend pas de résultat ;
- ligne 40 : la valeur de ce paramètre est transformée explicitement en float ;
- ligne 30 : la chaîne “12.45” est transformée en le nombre réel 12.45 ;
- ligne 31 : l’entier 67 est transformée en le nombre réel 67 ;
- ligne 32 : le booléen TRUE est transformé en le nombre réel 1 (la valeur FALSE en le nombre 0) ;
- ligne 33 : le pointeur NULL est transformé en le nombre réel 0 ;
C’est ce montrent les résultats écran :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | paramètre : string(5) "12.45"
résultat de la conversion : float(12.45)
paramètre : int(67)
résultat de la conversion : float(67)
paramètre : bool(true)
résultat de la conversion : float(1)
paramètre : NULL
résultat de la conversion : float(0)
|
Nous continuons la présentation du script en étudiant des conversions vers le type string :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // vers string
showstring(5);
showString(6.7);
showString(FALSE);
showString(NULL);
function showString($var) : void {
print "paramètre : ";
var_dump($var);
print "\n";
print "résultat de la conversion : ";
var_dump((string) $var);
print "\n";
}
|
- ligne 49 : la fonction [showString] reçoit un paramètre de type quelconque et ne rend pas de résultat ;
- ligne 54 : la valeur du paramètre est transformée en un type string ;
- ligne 44 : l’entier 5 sera transformé en la chaîne « 5 » ;
- ligne 45 : le réel 6.7 sera transformé en la chaîne « 6.7 » ;
- ligne 46 : le booléen FALSE sera ransformé en chaîne vide ;
- ligne 47 : le pointeur NULL sera transformé en chaîne vide ;
Voici les résultats écran :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | paramètre : int(5)
résultat de la conversion : string(1) "5"
paramètre : float(6.7)
résultat de la conversion : string(3) "6.7"
paramètre : bool(false)
résultat de la conversion : string(0) ""
paramètre : NULL
résultat de la conversion : string(0) ""
|
Les tableaux¶
Tableaux classiques à une dimension¶
Le script [bases-05.php] est le suivant :
1 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
// tableaux classiques
// initialisation
$tab1 = array(0, 1, 2, 3, 4, 5);
// parcours - 1
print "tab1 a " . count($tab1) . " éléments\n";
for ($i = 0; $i < count($tab1); $i++) {
print "tab1[$i]=$tab1[$i]\n";
}
// parcours - 2
print "tab1 a " . count($tab1) . " éléments\n";
reset($tab1);
while (list($clé, $valeur) = each($tab1)) {
print "tab1[$clé]=$valeur\n";
}
// ajout d'éléments
$tab1[] = $i++;
$tab1[] = $i++;
// parcours - 3
print "tab1 a " . count($tab1) . " éléments\n";
$i = 0;
foreach ($tab1 as $élément) {
print "tab1[$i]=$élément\n";
$i++;
}
// suppression dernier élément
array_pop($tab1);
// parcours - 4
print "tab1 a " . count($tab1) . " éléments\n";
for ($i = 0; $i < count($tab1); $i++) {
print "tab1[$i]=$tab1[$i]\n";
}
// suppression premier élément
array_shift($tab1);
// parcours - 5
print "tab1 a " . count($tab1) . " éléments\n";
for ($i = 0; $i < count($tab1); $i++) {
print "tab1[$i]=$tab1[$i]\n";
}
// ajout en fin de tableau
array_push($tab1, -2);
// parcours - 6
print "tab1 a " . count($tab1) . " éléments\n";
for ($i = 0; $i < count($tab1); $i++) {
print "tab1[$i]=$tab1[$i]\n";
}
// ajout en début de tableau
array_unshift($tab1, -1);
// parcours - 7
print "tab1 a " . count($tab1) . " éléments\n";
for ($i = 0; $i < count($tab1); $i++) {
print "tab1[$i]=$tab1[$i]\n";
}
|
Les résultats :
1 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 | tab1 a 6 éléments
tab1[0]=0
tab1[1]=1
tab1[2]=2
tab1[3]=3
tab1[4]=4
tab1[5]=5
tab1 a 6 éléments
Deprecated: The each() function is deprecated. This message will be suppressed on further calls in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_04.php on line 14
tab1[0]=0
tab1[1]=1
tab1[2]=2
tab1[3]=3
tab1[4]=4
tab1[5]=5
tab1 a 8 éléments
tab1[0]=0
tab1[1]=1
tab1[2]=2
tab1[3]=3
tab1[4]=4
tab1[5]=5
tab1[6]=6
tab1[7]=7
tab1 a 7 éléments
tab1[0]=0
tab1[1]=1
tab1[2]=2
tab1[3]=3
tab1[4]=4
tab1[5]=5
tab1[6]=6
tab1 a 6 éléments
tab1[0]=1
tab1[1]=2
tab1[2]=3
tab1[3]=4
tab1[4]=5
tab1[5]=6
tab1 a 7 éléments
tab1[0]=1
tab1[1]=2
tab1[2]=3
tab1[3]=4
tab1[4]=5
tab1[5]=6
tab1[6]=-2
tab1 a 8 éléments
tab1[0]=-1
tab1[1]=1
tab1[2]=2
tab1[3]=3
tab1[4]=4
tab1[5]=5
tab1[6]=6
tab1[7]=-2
|
Commentaires
Le programme ci-dessus montre des opérations de manipulation d’un tableau de valeurs. Il existe deux notations pour les tableaux en PHP :
1 2 | $tableau=array("un",2,"trois")
$contraires=array("petit"=>"grand", "beau"=>"laid", "cher"=>"bon marché")
|
Le tableau 1 est appelé tableau et le tableau 2 un dictionnaire ou tableau associatif où les éléments sont notés clé => valeur. La notation $contraires***[« beau »]* désigne la valeur associée à la clé « beau ». C’est donc ici la chaîne « laid ». Le tableau 1 n’est qu’une variante du dictionnaire et pourrait être noté :
1 | $tableau=array(0=>"un",1=>2,2=>"trois")
|
On a ainsi $tableau***[2]*= »trois ». Finalement, il n’y a que des dictionnaires. Dans le cas d’un tableau classique de n éléments, les clés sont les nombres entiers de l’intervalle [0,n-1].
- ligne 14 : la fonction each($tableau) permet de parcourir un dictionnaire. A chaque appel, elle rend une paire (clé,valeur) de celui-ci. Comme le montre la ligne 10 des résultats, la fonction each est désormais obsolète dans PHP 7 ;
- ligne 13 : la fonction reset($dictionnaire) positionne la fonction each sur la première paire (clé,valeur) du dictionnaire.
- ligne 14 : la boucle while s’arrête lorsque la fonction each rend une paire vide à la fin du dictionnaire. C’est une conversion implicite qui agit ici : la paire vide est convertie en le booléen FALSE ;
- ligne 18 : la notation $tableau[]=valeur ajoute l’élément valeur comme dernier élément de $tableau ;
- ligne 23 : le tableau est parcouru avec un foreach. Cet élément syntaxique permet de parcourir un dictionnaire, donc un tableau, selon deux syntaxes :
1 2 | foreach($dictionnaire as $clé=>$valeur)
foreach($tableau as $valeur)
|
La première syntaxe ramène une paire (clé,valeur) à chaque itération alors que la seconde syntaxe ne ramène que l’élément valeur du dictionnaire.
ligne 28 : la fonction array_pop($tableau) supprime le dernier élément de $tableau ;
ligne 35 : la fonction array_shift($tableau) supprime le premier élément de $tableau ;
ligne 42 : la fonction array_push($tableau,valeur) ajoute valeur comme dernier élément de $tableau ;
ligne 49 : la fonction array_unshift($tableau,valeur) ajoute valeur comme premier élément de $tableau ;
Le dictionnaire ou tableau associatif
Le script [bases-06.php] est le suivant :
1 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 | <?php
// dictionnaires
$conjoints = ["Pierre" => "Gisèle", "Paul" => "Virginie", "Jacques" => "Lucette", "Jean" => ""];
// parcours - 1
print "Nombre d'éléments du dictionnaire : " . count($conjoints) . "\n";
reset($conjoints);
while (list($clé, $valeur) = each($conjoints)) {
print "conjoints[$clé]=$valeur\n";
}
// tri du dictionnaire sur la clé
ksort($conjoints);
// parcours - 2
reset($conjoints);
while (list($clé, $valeur) = each($conjoints)) {
print "conjoints[$clé]=$valeur\n";
}
// liste des clés du dictionaire
$clés = array_keys($conjoints);
for ($i = 0; $i < count($clés); $i++) {
print "clés[$i]=$clés[$i]\n";
}
// liste des valeurs du dictionaire
$valeurs = array_values($conjoints);
for ($i = 0; $i < count($valeurs); $i++) {
print "valeurs[$i]=$valeurs[$i]\n";
}
// recherche d'une clé
existe($conjoints, "Jacques");
existe($conjoints, "Lucette");
existe($conjoints, "Jean");
// suppression d'une clé-valeur
unset($conjoints["Jean"]);
print "Nombre d'éléments du dictionnaire : " . count($conjoints) . "\n";
foreach ($conjoints as $clé => $valeur) {
print "conjoints[$clé]=$valeur\n";
}
// fin
exit;
function existe($conjoints, $mari) {
// vérifie si la clé $mari existe dans le dictionnaire $conjoints
if (isset($conjoints[$mari])) {
print "La clé [$mari] existe associée à la valeur [$conjoints[$mari]]\n";
} else {
print "La clé [$mari] n'existe pas\n";
}
}
|
Les résultats :
1 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 | Nombre d'éléments du dictionnaire : 4
Deprecated: The each() function is deprecated. This message will be suppressed on further calls in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_05.php on line 8
conjoints[Pierre]=Gisèle
conjoints[Paul]=Virginie
conjoints[Jacques]=Lucette
conjoints[Jean]=
conjoints[Jacques]=Lucette
conjoints[Jean]=
conjoints[Paul]=Virginie
conjoints[Pierre]=Gisèle
clés[0]=Jacques
clés[1]=Jean
clés[2]=Paul
clés[3]=Pierre
valeurs[0]=Lucette
valeurs[1]=
valeurs[2]=Virginie
valeurs[3]=Gisèle
La clé [Jacques] existe associée à la valeur [Lucette]
La clé [Lucette] n'existe pas
La clé [Jean] existe associée à la valeur []
Nombre d'éléments du dictionnaire : 3
conjoints[Jacques]=Lucette
conjoints[Paul]=Virginie
conjoints[Pierre]=Gisèle
|
Commentaires
Le code précédent applique à un dictionnaire ce qui a été vu auparavant pour un simple tableau. Nous ne commentons que les nouveautés :
ligne 12 : la fonction ksort (key sort) permet de trier un dictionnaire dans l’ordre naturel de la clé ;
ligne 19 : la fonction array_keys($dictionnaire) rend la liste des clés du dictionnaire sous forme de tableau ;
ligne 24 : la fonction array_values($dictionnaire) rend la liste des valeurs du dictionnaire sous forme de tableau ;
ligne 43 : la fonction isset($variable) rend TRUE si la variable $variable a été définie, FALSE sinon ;
ligne 33 : la fonction unset($variable) supprime la variable $variable.
Les tableaux à plusieurs dimensions
Le script [bases-07.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php
// tableaux classiques multidimensionnels
// initialisation
$multi = array(array(0, 1, 2), array(10, 11, 12, 13), array(20, 21, 22, 23, 24));
// parcours
for ($i1 = 0; $i1 < count($multi); $i1++) {
for ($i2 = 0; $i2 < count($multi[$i1]); $i2++) {
print "multi[$i1][$i2]=" . $multi[$i1][$i2] . "\n";
}
}
// dictionnaires multidimensionnels
// initialisation
$multi = array("zéro" => array(0, 1, 2), "un" => array(10, 11, 12, 13), "deux" => array(20, 21, 22, 23, 24));
// parcours
foreach ($multi as $clé => $valeur) {
for ($i2 = 0; $i2 < count($valeur); $i2++) {
print "multi[$clé][$i2]=" . $multi[$clé][$i2] . "\n";
}
}
|
Résultats :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | multi[0][0]=0
multi[0][1]=1
multi[0][2]=2
multi[1][0]=10
multi[1][1]=11
multi[1][2]=12
multi[1][3]=13
multi[2][0]=20
multi[2][1]=21
multi[2][2]=22
multi[2][3]=23
multi[2][4]=24
multi[zéro][0]=0
multi[zéro][1]=1
multi[zéro][2]=2
multi[un][0]=10
multi[un][1]=11
multi[un][2]=12
multi[un][3]=13
multi[deux][0]=20
multi[deux][1]=21
multi[deux][2]=22
multi[deux][3]=23
multi[deux][4]=24
|
Commentaires
- ligne 5 : les éléments du tableau $multi sont eux-mêmes des tableaux ;
- ligne 14 : le tableau $multi devient un dictionnaire (clé,valeur) où chaque valeur est un tableau ;
Les chaînes de caractères¶
Notation¶
Le script [bases-08.php] est le suivant :
1 2 3 4 5 6 7 | <?php
// notation des chaînes
$chaine1 = "un";
$chaine2 = 'un';
print "[$chaine1,$chaine2]\n";
?>
|
Résultats :
1 | [un,un]
|
Comparaison¶
Le script [bases-09.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php
// respect strict du type des paramètres des fonctions
declare(strict_types=1);
// fonction de comparaison
function compareModele2Chaine(string $chaine1, string $chaine2): void {
// compare chaine1 et chaine2
if ($chaine1 === $chaine2) {
print "[$chaine1] est égal à [$chaine2]\n";
} else {
print "[$chaine1] est différent de [$chaine2]\n";
}
}
// tests de comparaisons de chaînes
compareModele2Chaine("abcd", "abcd");
compareModele2Chaine("", "");
compareModele2Chaine("1", "");
exit;
|
Résultats :
1 2 3 | [abcd] est égal à [abcd]
[] est égal à []
[1] est différent de []
|
Commentaires
ligne 9 du code : on aurait pu utiliser le comparateur == plutôt que ===. Ce dernier opérateur est plus contraignant en ce sens qu’il impose que les deux opérandes soient de même type. A noter qu’ici, il pouvait être remplacé par l’opérateur == puisque le type des deux paramètres est fixé à string dans la signature de la fonction ;
Liens entre chaînes et tableaux
Le script [bases-10.php] est le suivant :
1 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 | <?php
// chaîne vers tableau
$chaine = "1:2:3:4";
$tab = explode(":", $chaine);
// parcours tableau
print "tab a " . count($tab) . " éléments\n";
for ($i = 0; $i < count($tab); $i++) {
print "tab[$i]=$tab[$i]\n";
}
// tableau vers chaîne
$chaine2 = implode(":", $tab);
print "chaine2=$chaine2\n";
// ajoutons un champ vide
$chaine .= ":";
print "chaîne=$chaine\n";
$tab = explode(":", $chaine);
// parcours tableau
print "tab a " . count($tab) . " éléments\n";
for ($i = 0; $i < count($tab); $i++) {
print "tab[$i]=$tab[$i]\n";
} // on a maintenant 5 éléments, le dernier étant vide
// ajoutons de nouveau un champ vide
$chaine .= ":";
print "chaîne=$chaine\n";
$tab = explode(":", $chaine);
// parcours tableau
print "tab a " . count($tab) . " éléments\n";
for ($i = 0; $i < count($tab); $i++) {
print "tab[$i]=$tab[$i]\n";
} // on a maintenant 6 éléments, les deux derniers étant vides
|
Résultats :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | tab a 4 éléments
tab[0]=1
tab[1]=2
tab[2]=3
tab[3]=4
chaine2=1:2:3:4
chaîne=1:2:3:4:
tab a 5 éléments
tab[0]=1
tab[1]=2
tab[2]=3
tab[3]=4
tab[4]=
chaîne=1:2:3:4::
tab a 6 éléments
tab[0]=1
tab[1]=2
tab[2]=3
tab[3]=4
tab[4]=
tab[5]=
|
Commentaires
ligne 5 : la fonction explode($séparateur,$chaine) permet de récupérer les champs de $chaine séparés par $séparateur. Ainsi explode(« : »,$chaine) permet de récupérer sous forme de tableau les éléments de $chaine qui sont séparés par la chaîne « : » ;
ligne 12 : la fonction implode($séparateur,$tableau) fait l’opération inverse de la fonction explode. Elle rend une chaîne de caractères formée des éléments de $tableau séparés par $séparateur ;
Les expressions régulières
Le script [bases-11.php] est le suivant :
1 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 | <?php
// type strict pour les paramètres de fonctions
declare (strict_types=1);
// expressions régulières en php
// récupérer les différents champs d'une chaîne
// le modèle : une suite de chiffres entourée de caractères quelconques
// on ne veut récupérer que la suite de chiffres
$modèle = "/(\d+)/";
// on confronte la chaîne au modèle
compareModele2Chaine($modèle, "xyz1234abcd");
compareModele2Chaine($modèle, "12 34");
compareModele2Chaine($modèle, "abcd");
// le modèle : une suite de chiffres entourée de caractères quelconques
// on veut la suite de chiffres ainsi que les champs qui suivent et précèdent
$modèle = "/^(.*?)(\d+)(.*?)$/";
// on confronte la chaîne au modèle
compareModele2Chaine($modèle, "xyz1234abcd");
compareModele2Chaine($modèle, "12 34");
compareModele2Chaine($modèle, "abcd");
// le modèle - une date au format jj/mm/aa
$modèle = "/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/";
compareModele2Chaine($modèle, "10/05/97");
compareModele2Chaine($modèle, " 04/04/01 ");
compareModele2Chaine($modèle, "5/1/01");
// le modèle - un nombre décimal
$modèle = "/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/";
compareModele2Chaine($modèle, "187.8");
compareModele2Chaine($modèle, "-0.6");
compareModele2Chaine($modèle, "4");
compareModele2Chaine($modèle, ".6");
compareModele2Chaine($modèle, "4.");
compareModele2Chaine($modèle, " + 4");
// fin
exit;
// --------------------------------------------------------------------------
function compareModele2Chaine(string $modèle, string $chaîne): void {
// compare la chaîne $chaîne au modèle $modèle
// on confronte la chaîne au modèle
$champs = [];
$correspond = preg_match($modèle, $chaîne, $champs);
// affichage résultats
print "\nRésultats($modèle,$chaîne)\n";
if ($correspond) {
for ($i = 0; $i < count($champs); $i++) {
print "champs[$i]=$champs[$i]\n";
}
} else {
print "La chaîne [$chaîne] ne correspond pas au modèle [$modèle]\n";
}
}
|
Résultats :
1 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 | Résultats(/(\d+)/,xyz1234abcd)
champs[0]=1234
champs[1]=1234
Résultats(/(\d+)/,12 34)
champs[0]=12
champs[1]=12
Résultats(/(\d+)/,abcd)
La chaîne [abcd] ne correspond pas au modèle [/(\d+)/]
Résultats(/^(.*?)(\d+)(.*?)$/,xyz1234abcd)
champs[0]=xyz1234abcd
champs[1]=xyz
champs[2]=1234
champs[3]=abcd
Résultats(/^(.*?)(\d+)(.*?)$/,12 34)
champs[0]=12 34
champs[1]=
champs[2]=12
champs[3]= 34
Résultats(/^(.*?)(\d+)(.*?)$/,abcd)
La chaîne [abcd] ne correspond pas au modèle [/^(.*?)(\d+)(.*?)$/]
Résultats(/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/,10/05/97)
champs[0]=10/05/97
champs[1]=10
champs[2]=05
champs[3]=97
Résultats(/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/, 04/04/01 )
champs[0]= 04/04/01
champs[1]=04
champs[2]=04
champs[3]=01
Résultats(/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/,5/1/01)
La chaîne [5/1/01] ne correspond pas au modèle [/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/]
Résultats(/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/,187.8)
champs[0]=187.8
champs[1]=
champs[2]=187.8
Résultats(/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/,-0.6)
champs[0]=-0.6
champs[1]=-
champs[2]=0.6
Résultats(/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/,4)
champs[0]=4
champs[1]=
champs[2]=4
Résultats(/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/,.6)
champs[0]=.6
champs[1]=
champs[2]=.6
Résultats(/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/,4.)
champs[0]=4.
champs[1]=
champs[2]=4.
Résultats(/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/, + 4)
champs[0]= + 4
champs[1]=+
champs[2]=4
|
Commentaires
- nous utilisons ici les expression régulières pour récupérer les divers champs d’une chaîne de caractères. Les expressions régulières permettent de dépasser les limites de la fonction implode. Le principe est de comparer une chaîne de caractères à une autre chaîne appelée modèle à l’aide de la fonction preg_match :
1 | $correspond = preg_match($modèle, $chaîne, $champs);
|
La fonction preg_match rend un booléen TRUE si le modèle peut être trouvé dans la chaîne. Si oui, $champs***[0]* représente la sous-chaîne correspondant au modèle. Par ailleurs, si modèle contient des sous-modèles entre parenthèses, $champs***[1]* est le morceau de $chaîne correspondant au 1er sous-modèle, $champs***[2]* est le morceau de $chaîne correspondant au 2e sous-modèle, etc…
Considérons le 1er exemple. Le modèle est défini ligne 10 : il désigne une suite de un ou plusieurs (+) chiffres (d) placés n’importe où dans une chaîne. Par ailleurs, le modèle définit un sous-modèle entouré de parenthèses ;
- ligne 12 : le modèle /(d+)/ (suite d’un ou plusieurs chiffres n’importe où dans la chaîne) est comparé à la chaîne « xyz1234abcd ». On voit que la sous-chaîne 1234 correspond au modèle. On aura donc $champs[0] égal à « 1234 ». Par ailleurs, le modèle a des sous-modèles entre parenthèses. On aura $champs[1]= »1234 » ;
- ligne 13 : le modèle /(d+)/ est comparé à la chaîne « 12 34 ». On voit que les sous-chaînes 12 et 34 correspondent au modèle. La comparaison s’arrête à la première sous-chaîne correspondant au modèle. On aura donc, $champs[0]=12 et $champs[1]=12 ;
- ligne 14 : le modèle /(d+)/ est comparé à la chaîne « abcd ». Aucune correspondance n’est trouvée ;
Explicitons les modèles utilisés dans la suite du code :
1 | $modèle = "/^(.*?)(\d+)(.*?)$/";
|
correspond à début de chaîne (^), puis 0 ou plusieurs (*) caractères quelconques (.) puis 1 ou plusieurs (+) chiffres, puis de nouveau 0 ou plusieurs (*) caractères quelconques (.). Le modèle (.*) désigne 0 ou plusieurs caractères quelconques. Un tel modèle va correspondre à n’importe quelle chaîne. Ainsi le modèle /^(.*)(d+)(.*)$/ ne sera-t-il jamais trouvé car le premier sous-modèle (.*) va absorber toute la chaîne. Le modèle (.*?)(d+) désigne lui 0 ou plusieurs caractères quelconques jusqu’au sous-modèle suivant (?), ici \d+. Donc les chiffres ne sont maintenant plus absorbés par le modèle (.*). Le modèle ci-dessus correspond donc à [début de chaîne (^), une suite de caractères quelconques (.*?), une suite d’un ou plusieurs chiffres (d+), une suite de caractères quelconques (.*?), la fin de la chaîne ($)].
1 | $modèle = "/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/";
|
correspond à [début de chaîne (^), 2 chiffres (dd), le caractère / (/), 2 chiffres, /, 2 chiffres, une suite de 0 ou plusieurs espaces (s*), la fin de chaîne ($)].
1 | $modèle = "/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/";
|
correspond à début de chaîne (^), 0 ou plusieurs espaces (s*), un signe + ou - [+|-] présent 0 ou 1 fois (?), une suite de 0 ou plusieurs espaces (s*), 1 ou plusieurs chiffres suivis d’un point décimal suivi de zéro ou plusieurs chiffres (d+.d*) ou (|) un point décimal (.) suivi d’un ou plusieurs chiffres (d+) ou (|) un ou plusieurs chiffres (d+), une suite de 0 ou plusieurs espaces (s*)].
Note : le terme [espace] dans les expressions régulières désigne un ensemble de caractères : blanc, saut de ligne \n, tabulation \t, retour à la ligne \r, saut de page \f…
Les fonctions¶
Mode de passage des paramètres¶
Le script [base-12.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php
// mode de pasage des paramètres d'une fonction
// respect strict du type des paramères
declare(strict_types=1);
function f(int &$i, int $j): void {
// $i sera obtenu par référence
// $j sera obtenu par valeur
$i++;
$j++;
print "f[i,j]=[$i,$j]\n";
}
// tests
$i = 0;
$j = 0;
// $i et $j sont passés à la fonction f
f($i, $j);
print "test[i,j]=[$i,$j]\n";
|
Résultats :
1 2 | f[i,j]=[1,1]
test[i,j]=[1,0]
|
Commentaires
Le code ci-dessus montre les deux modes de passage de paramètres à une fonction. Prenons l’exemple suivant :
1 2 3 4 5 6 7 | function f(&$a,$b){
…
}
// programme principal
$i=10; $j=20;
f($i,$j);
|
- ligne 1 : définit les paramètres formels $a et $b de la fonction f. Celle-ci manipule ces deux paramètres formels et rend un résultat ;
- ligne 7 : appel de la fonction f avec deux paramètres effectifs $i et $j. Les liens entre les paramètres formels ($a,$b) et les paramètres effectifs ($i,$j) sont définis par les lignes 1 et 7 :
- &$a : le signe & indique que le paramètre formel $a prendra pour
- valeur l’adresse du paramètre effectif $i. Dit autrement, $a et $i sont deux références sur un même emplacement mémoire. Manipuler le paramètre formel $a revient à manipuler le paramètre effectif $i. C’est ce que montre l’exécution du code. Ce mode de passage convient aux paramètres de sortie et aux données volumineuses telles que les tableaux et dictionnaires. On appelle ce mode passage, passage par référence.
- $b : le paramètre formel $b prendra pour valeur celle du paramètre
- effectif $j. C’est un passage par valeur. Les paramètres formels et effectifs sont deux variables différentes. Manipuler le paramètre formel $b n’a aucune incidence sur le paramètre effectif $j. C’est ce que montre l’exécution du code. Ce mode de passage convient aux paramètres d’entrée.
- Soit la fonction échange qui admet deux paramètres formels $a
- et $b. La fonction échange la valeur de ces deux paramètres. Ainsi lors d’un appel échange ($i,$j), le code appelant s’attend à ce que les valeurs des deux paramètres effectifs soient échangées. Ce sont donc des paramètres de sortie (ils sont modifiés). On écrira donc :
1 | function échange(&$a,&$b){….}
|
Le script suivant [base-13.php] montre d’autres exemples :
1 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 | <?php
// types en mode strict
// declare(strict_types = 1);
// mode de passage des paramètres
function f(&$i, $j) {
// $i sera obtenu par référence
// $j sera obtenu par valeur
$i++;
$j++;
print "f[i,j]=[$i,$j]\n";
}
function g(int &$i, int $j) : void {
// $i sera obtenu par référence
// $j sera obtenu par valeur
$i++;
$j++;
print "g[i,j]=[$i,$j]\n";
}
// tests
$i = 0;
$j = 0;
// $i et $j sont passés à la fonction f
f($i, $j);
print "test[i,j]=[$i,$j]\n";
// $i et $j sont passés à la fonction g
g($i, $j);
print "test[i,j]=[$i,$j]\n";
// on passe des paramètres incorrects à f
$a=5.3;
$b=6.2;
f($a, $b);
print "test[a,b]=[$a,$b]\n";
// on passe des paramètres incorrects à f
$a=5.3;
$b=6.2;
g($a, $b);
print "test[a,b]=[$a,$b]\n";
|
Commentaires
- lignes 8-14 : la fonction f étudiée dans le paragraphe précédent mais on n’a pas typé les paramètres ;
- lignes 16-22 : la fonction g fait la même chose que la fonction f mais on précise le type des paramètres attendus – c’est une nouveauté PHP 7. On attend deux paramètres de type int. On veut voir ce qui se passe lorsque le paramètre effectif passé à la fonction n’a pas le type attendu par celle-ci ;
- lignes 25-26 : $i et $j sont deux entiers ;
- lignes 28-29 : appel de la fonction f avec des paramètres du type attendu ;
- lignes 31-32 : appel de la fonction g avec des paramètres du type attendu ;
- lignes 34-35 : les variables $a et $b sont de type float ;
- lignes 36-37 : appel de la fonction f avec des paramètres qui ne sont pas du type attendu ;
- lignes 41-42 : appel de la fonction g avec des paramètres qui ne sont pas du type attendu ;
Résultats
1 2 3 4 5 6 7 8 | f[i,j]=[1,1]
test[i,j]=[1,0]
g[i,j]=[2,1]
test[i,j]=[2,0]
f[i,j]=[6.3,7.2]
test[a,b]=[6.3,6.2]
g[i,j]=[6,7]
test[a,b]=[6,6.2]
|
- les lignes 5-6 montrent que la fonction f a accepté les deux paramètres de type float et a travaillé avec ;
- les lignes 7-8 montrent que la fonction g a accepté les deux paramètres de type float mais qu’elle les a transformés en type int (ligne 7) ;
Maintenant décommentons la ligne 4 :
1 | declare(strict_types = 1);
|
Cette instruction indique que les types des paramètres formels doivent être respectés. Si ce n’est pas le cas, une erreur est signalée. Les résultats de l’exécution deviennent alors :
1 2 3 4 5 6 7 8 9 10 11 12 | f[i,j]=[1,1]
test[i,j]=[1,0]
g[i,j]=[2,1]
test[i,j]=[2,0]
f[i,j]=[6.3,7.2]
test[a,b]=[6.3,6.2]
Fatal error: Uncaught TypeError: Argument 1 passed to g() must be of the type integer, float given, called in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_111.php on line 40 and defined in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_111.php:15
Stack trace:
#0 C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_111.php(40): g(5.3, 6.2)
#1 {main}
thrown in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_111.php on line 15
|
lignes 9-10 : l’interpréteur PHP 7 a lancé une exception pour indiquer que le 1er paramètre passé à la fonction g n’avait pas le bon type. Il est recommandé d’être strict sur les types des paramètres, chaque fois que c’est possible, pour détecter les erreurs d’appel de fonctions ;
Résultats rendus par une fonction
Le script [base-15.php] est le suivant :
1 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 | <?php
// types en mode strict
declare(strict_types=1);
// résultats rendus par une fonction
// une fonction peut rendre plusieurs valeurs dans un tableau
list($res1, $res2, $res3) = f1(10);
print "[$res1,$res2,$res3]\n";
$res = f1(10);
for ($i = 0; $i < count($res); $i++) {
print "f1 : res[$i]=$res[$i]\n";
}
// une fonction peut rendre un objet
$res = f2(10);
print "f2 : [$res->res1,$res->res2,$res->res3]\n";
// objet de quelle nature ?
print "nature de l'objet : ";
var_dump($res);
print "\n";
// on fait la même chose avec la fonction f3
$res = f3(10);
print "f3 : [$res->res1,$res->res2,$res->res3]\n";
// objet de quelle nature ?
print "nature de l'objet : ";
var_dump($res);
print "\n";
// fin
exit;
// fonction f1
function f1(int $valeur): array {
// rend un tableau ($valeur+1,$valeur+2,$valeur+3)
return array($valeur + 1, $valeur + 2, $valeur + 3);
}
// fonction f2
function f2(int $valeur): object {
// rend un objet ($valeur+1,$valeur+2,$valeur+3)
$res->res1 = $valeur + 1;
$res->res2 = $valeur + 2;
$res->res3 = $valeur + 3;
// rend l'objet
return $res;
}
// fonction f3 - fait la même chose que la fonction f2
function f3(int $valeur): object {
// rend un objet ($valeur+1,$valeur+2,$valeur+3)
$res = new stdclass();
$res->res1 = $valeur + 1;
$res->res2 = $valeur + 2;
$res->res3 = $valeur + 3;
// rend l'objet
return $res;
}
|
Résultats
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | [11,12,13]
f1 : res[0]=11
f1 : res[1]=12
f1 : res[2]=13
Warning: Creating default object from empty value in C:\Data\st-2019\dev\php7\php5-exemples\exemples\bases\base-15.php on line 43
f2 : [11,12,13]
nature de l'objet : object(stdClass)#1 (3) {
["res1"]=>
int(11)
["res2"]=>
int(12)
["res3"]=>
int(13)
}
f3 : [11,12,13]
nature de l'objet : object(stdClass)#2 (3) {
["res1"]=>
int(11)
["res2"]=>
int(12)
["res3"]=>
int(13)
}
|
Commentaires
- le programme précédent montre qu’une fonction PHP peut rendre un ensemble de résultats et non un seul, sous la forme d’un tableau ou d’un objet. La notion d’objet est explicitée un peu plus loin ;
- lignes 35-38 : la fonction f1 rend plusieurs valeurs sous la forme d’un tableau (array) ;
- lignes 41-48 : la fonction f2 rend plusieurs valeurs sous la forme d’un objet (object) ;
- lignes 51-59 : la fonction f3 est identique à la fonction f2 si ce n’est qu’elle crée explicitement un objet, ligne 53 ;
- la ligne 6 des résultats émet un avertissement (warning) indiquant
que PHP a été obligé de créer un objet par défaut à la ligne 43 du
code, ç-a-d lors de l’utilisation de la notation [$res→res1]. La
fonction var_dump de la ligne 20 du code donne accès à la nature
de l’objet et à son contenu. Dans les résultats, on voit que :
- ligne 8 : l’objet créé par défaut est de type stdClass ;
- lignes 9-10 : la propriété res1 est de type entier et a la valeur 11 ;
- etc…
- pour éviter le warning de la ligne 6 des résultats, on crée explicitement, ligne 53 de la fonction f3, un objet de type stdClass ;
Les fichiers texte¶
Le script [bases-16.php] est le suivant :
1 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 | <?php
// respect strict du type des paramères des fonctions
declare (strict_types=1);
// exploitation séquentielle d'un fichier texte
// celui-ci est un ensemble de lignes de la forme login:pwd:uid:gid:infos:dir:shell
// chaque ligne est mis dans un dictionnaire sous la forme login => uid:gid:infos:dir:shell
// on fixe le nom du fichier
$INFOS = "infos.txt";
// on l'ouvre en création
if (!$fic = fopen($INFOS, "w")) {
print "Erreur d'ouverture du fichier $INFOS en écriture\n";
exit;
}
// on génère un contenu arbitraire
for ($i = 0; $i < 100; $i++) {
fputs($fic, "login$i:pwd$i:uid$i:gid$i:infos$i:dir$i:shell$i\n");
}
// on ferme le fichier
fclose($fic);
// on l'exploite - fgets garde la marque de fin de ligne
// cela permet de ne pas récupérer une chaîne vide lors de la lecture d'une ligne blanche
// on l'ouvre en lecture
if (!$fic = fopen($INFOS, "r")) {
print "Erreur d'ouverture du fichier $INFOS en lecture\n";
exit;
}
// les lignes font moins de 1000 caractères
// la lecture de la ligne s'arrête sur la marque de fin de ligne
// ou celle de fin de fichier
while ($ligne = fgets($fic, 1000)) {
// on supprime la marque de fin de ligne si elle existe
$ligne = cutNewLineChar($ligne);
// on met la ligne dans un tableau
$infos = explode(":", $ligne);
// on récupère le login
$login = array_shift($infos);
// on néglige le pwd
array_shift($infos);
// on crée une entrée dans le dictionnaire
$dico[$login] = $infos;
}
// on le ferme
fclose($fic);
// exploitation du dictionnaire
afficheInfos($dico, "login10");
afficheInfos($dico, "X");
// fin
exit;
// --------------------------------------------------------------------------
function afficheInfos(array $dico, string $clé): void {
// affiche la valeur associée à clé dans le dictionnaire $dico si elle existe
if (isset($dico[$clé])) {
// valeur existe - est-ce un tableau ?
$valeur = $dico[$clé];
if (is_array($valeur)) {
print "[$clé," . join(":", $valeur) . "]\n";
} else {
// $valeur n'est pas un tableau
print "[$clé,$valeur]\n";
}
} else {
// $clé n'est pas une clé du dictionnaire $dico
print "la clé [$clé] n'existe pas\n";
}
}
// --------------------------------------------------------------------------
function cutNewLinechar(string $ligne): string {
// on supprime la marque de fin de ligne de $ligne si elle existe
$L = strlen($ligne); // longueur ligne
while (substr($ligne, $L - 1, 1) == "\n" or substr($ligne, $L - 1, 1) == "\r") {
$ligne = substr($ligne, 0, $L - 1);
$L--;
}
// fin
return($ligne);
}
|
Le fichier infos.txt :
1 2 3 4 5 6 | login0:pwd0:uid0:gid0:infos0:dir0:shell0
login1:pwd1:uid1:gid1:infos1:dir1:shell1
login2:pwd2:uid2:gid2:infos2:dir2:shell2
…
login98:pwd98:uid98:gid98:infos98:dir98:shell98
login99:pwd99:uid99:gid99:infos99:dir99:shell99
|
Les résultats :
1 2 | [login10,uid10:gid10:infos10:dir10:shell10]
la clé [X] n'existe pas
|
Commentaires
- ligne 12 : fopen(nom_fichier, »w ») ouvre le fichier nom_fichier en écriture (w=write). Si le fichier n’existe pas, il est créé. S’il existe, il est vidé. Si la création échoue, fopen rend la valeur false. Dans l’instruction if(!$fic = fopen($INFOS, « w »)) {…}, il y a deux opérations successives : 1) $fic=fopen(..) 2) if( ! $fic) {…} ;
- ligne 18 : fputs($fic,$chaîne) écrit chaîne dans le fichier $fic. $chaine est écrite avec la marque de fin de ligne \n derrière ;
- ligne 21 : fclose($fic) ferme le fichier $fic ;
- ligne 26 : fopen(nom_fichier, »r ») ouvre le fichier nom_fichier en lecture (r=read). Si l’ouverture échoue (le fichier n’existe pas par exemple), fopen rend la valeur false ;
- ligne 34 : fgets($fic,1000) lit la ligne suivante du fichier dans la limite de 1000 caractères. Dans l’opération while($ligne = fgets($fic, 1000)) {…}, il y a deux opérations successives 1) $ligne=fgets(…) 2) while ( ! $ligne). Après que le dernier caractère du fichier a été lu, la fonction fgets rend la valeur false et la boucle while s’arrête. La fonction fgets tente ici de lire au plus 1000 caractères mais s’arrête dès qu’une marque de fin de ligne est rencontrée. Ici toutes les lignes ayant moins de 1000 caractères, [fgets] lit une ligne de texte, caractère de fin de ligne inclus. La fonction cutNewLineChar des lignes 75-84 élimine les éventuels caractères de fin de ligne ;
- ligne 77 : la fonction strlen($chaîne) rend le nombre de caractères de $*chaîne *;
- ligne 78 : la fonction substr($ligne, $position, $taille) rend $taille caractères de $ligne, pris à partir du caractère n° $position, le 1er caractère ayant le n° 0. Sur les machines windows, la marque de fin de ligne est « rn ». Sur les machines Unix, c’est la chaîne « n » ;
- ligne 40 : la fonction array_shift($tableau) élimine le 1er élément de $tableau et le rend comme résultat. On néglige ici le résultat rendu par array_shift ;
- ligne 62 : la fonction is_array($variable) rend true si $variable est un tableau, false sinon ;
- ligne 63 : la fonction join fait la même chose que la fonction implode déjà rencontrée ;
Encodage / décodage jSON¶
L’encodage / décodage jSON (JavaScript Object Notation) est quelque chose que nous allons utiliser intensivement dans l’exercice qui sert de fil rouge au document. Les scripts [json-01.php, json-02.php, json-03.php] expliquent ce qui est à savoir pour la suite.
Le script [json-01.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <?php
$array1 = ["nom" => "séléné", "prénom" => "bénédicte", "âge" => 34];
// encodage json du tableau array1 avec caractères Unicode échappés
print "encodage json du tableau array1 avec caractères Unicode échappés\n";
$json1 = json_encode($array1);
print "json1=$json1\n";
// encodage json du tableau array1 avec caractères Unicode non échappés
print "encodage json du tableau array1 avec caractères Unicode non échappés\n";
$json2 = json_encode($array1, JSON_UNESCAPED_UNICODE);
print "json2=$json2\n";
// décodage jSON dans tableau associatif
print "décodage jSON de json2 dans tableau associatif\n";
$array2 = json_decode($json2, true);
var_dump($array2);
foreach ($array2 as $key => $value) {
print "$key:$value\n";
}
// décodage jSON dans objet
print "décodage jSON de json2 dans objet stdClass\n";
$array2 = json_decode($json2);
var_dump($array2);
print "prénom=$array2->prénom\n";
print "nom=$array2->nom\n";
print "âge=$array2->âge\n";
|
Résultats
1 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 | encodage json du tableau array1 avec caractères Unicode échappés
json1={"nom":"s\u00e9l\u00e9n\u00e9","pr\u00e9nom":"b\u00e9n\u00e9dicte","\u00e2ge":34}
encodage json du tableau array1 avec caractères Unicode non échappés
json2={"nom":"séléné","prénom":"bénédicte","âge":34}
décodage jSON de json2 dans tableau associatif
array(3) {
["nom"]=>
string(9) "séléné"
["prénom"]=>
string(11) "bénédicte"
["âge"]=>
int(34)
}
nom:séléné
prénom:bénédicte
âge:34
décodage jSON de json2 dans objet stdClass
object(stdClass)#1 (3) {
["nom"]=>
string(9) "séléné"
["prénom"]=>
string(11) "bénédicte"
["âge"]=>
int(34)
}
prénom=bénédicte
nom=séléné
âge=34
|
Commentaires
- ligne 6 du code : la fonction [json_encode] transforme son paramètre en chaîne de caractères jSON ;
- ligne 2 des résultats : la chaîne jSON produite. Les caractères Unicode éâ ont été remplacés par leur code Unicode qui commence par \u ;
- ligne 10 du code : on refait la même chose en demandant cette fois-ci à ce que les caractères Unicode soient conservés tels quels ;
- ligne 4 des résultats : la chaîne jSON résultante. Elle est beaucoup plus lisible ;
- lignes 14-18 du code : on fait l’opération inverse. On transforme une chaîne jSON en tableau associatif ;
- lignes 6-13 des résultats : on voit qu’on a récupéré un tableau associatif ;
- lignes 19-25 du code : on transforme une chaîne jSON en objet de type [stdClass] ;
- lignes 18-25 des résultats : on voit qu’on a récupéré un objet de type [stdClass] ;
- lignes 23-25 du code : l’attribut A d’un objet O est noté [O→A] ;
On peut encoder en jSON des tableaux à plusieurs niveaux comme le montre le script [json-02.php] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | <?php
$array = ["nom" => "séléné", "prénom" => "bénédicte", "âge" => 34,
"mari" => ["nom" => "icariù", "prénom" => "ignacio", "âge" => 35],
"enfants" => [
["prénom" => "angèle", "age" => 8],
["prénom" => "andré", "age" => 2],
]];
// encodage jSON du tableau à plusieurs niveaux
print "encodage jSON d'un tableau à plusieurs niveaux\n";
$json = json_encode($array, JSON_UNESCAPED_UNICODE);
print "json=$json\n";
|
Résultats
1 2 | encodage jSON d'un tableau à plusieurs niveaux
json={"nom":"séléné","prénom":"bénédicte","âge":34,"mari":{"nom":"icariù","prénom":"ignacio","âge":35},"enfants":[{"prénom":"angèle","age":8},{"prénom":"andré","age":2}]}
|
Commentaires
Dans la chaîne jSON :
- les tableaux non associatifs sont entourés de crochets [] ;
- les tableaux associatifs sont entourés d’accolades {} ;
Le script [json-03.php] montre comment exploiter le fichier jSON [famille.json] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {
"épouse": {
"nom": "séléné",
"prénom": "bénédicte",
"âge": 34
},
"mari": {
"nom": "icariù",
"prénom": "ignacio",
"âge": 35
},
"enfants": [
{
"prénom": "angèle",
"age": 8
},
{
"prénom": "andré",
"age": 2
}
]
}
|
Le script [json-03.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | <?php
// lecture du fichier jSON
$json = file_get_contents("famille.json");
// décodage json en objet
$famille1 = json_decode($json);
print "----famille1\n";
var_dump($famille1);
// décodage json en tableau associatif
print "----famille2\n";
$famille2 = json_decode($json, true);
var_dump($famille2);
|
Commentaires
- ligne 4 : la fonction [file_get_contents] lit le contenu du fichier nommé [famille.json] et le met dans la variable [$json] ;
- la variable est ensuite décodée en objet (lignes 5-8) et en tableau associatif (lignes 9-12) ;
Résultats
1 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 | ----famille1
object(stdClass)#2 (3) {
["épouse"]=>
object(stdClass)#1 (3) {
["nom"]=>
string(9) "séléné"
["prénom"]=>
string(11) "bénédicte"
["âge"]=>
int(34)
}
["mari"]=>
object(stdClass)#3 (3) {
["nom"]=>
string(7) "icariù"
["prénom"]=>
string(7) "ignacio"
["âge"]=>
int(35)
}
["enfants"]=>
array(2) {
[0]=>
object(stdClass)#4 (2) {
["prénom"]=>
string(7) "angèle"
["age"]=>
int(8)
}
[1]=>
object(stdClass)#5 (2) {
["prénom"]=>
string(6) "andré"
["age"]=>
int(2)
}
}
}
----famille2
array(3) {
["épouse"]=>
array(3) {
["nom"]=>
string(9) "séléné"
["prénom"]=>
string(11) "bénédicte"
["âge"]=>
int(34)
}
["mari"]=>
array(3) {
["nom"]=>
string(7) "icariù"
["prénom"]=>
string(7) "ignacio"
["âge"]=>
int(35)
}
["enfants"]=>
array(2) {
[0]=>
array(2) {
["prénom"]=>
string(7) "angèle"
["age"]=>
int(8)
}
[1]=>
array(2) {
["prénom"]=>
string(6) "andré"
["age"]=>
int(2)
}
}
}
|
Commentaires
- lignes 1-38 : l’objet issu du décodage du fichier jSON [famille.json] ;
- lignes 39-76 : le tableau associatif issu du décodage du fichier jSON [famille.json] ;
Exercice d’application – versions 1 et 2¶
Le problème¶
Le tableau ci-dessus permet de calculer l’impôt dans le cas simplifié d’un contribuable n’ayant que son seul salaire à déclarer. Comme l’indique la note (1), l’impôt ainsi calculé est l’impôt avant trois mécanismes :
- le plafonnement du quotient familial qui intervient pour les hauts revenus ;
- la décôte et la réduction d’impôts qui interviennent pour les faibles revenus ;
Ainsi le calcul de l’impôt comprend les étapes suivantes [http://impotsurlerevenu.org/comprendre-le-calcul-de-l-impot/1217-calcul-de-l-impot-2019.php] :
On se propose d’écrire un programme permettant de calculer l’impôt d’un contribuable dans le cas simplifié d’un contribuable n’ayant que son seul salaire à déclarer :
Calcul de l’impôt brut¶
L’impôt brut peut être calculé de la façon suivante :
On calcule d’abord le nombre de parts du contribuable :
- chaque parent amène 1 part ;
- les deux premiers enfants amènent chacun 1/2 part ;
- les enfants suivants amènent une part chacun :
Le nombre de part est donc :
- nbParts=1+nbEnfants*0,5+(nbEnfants-2)*0,5 si le salarié n’est pas marié ;
- nbParts=2+nbEnfants*0,5+(nbEnfants-2)*0,5 s’il est marié ;
où nbEnfants est son nombre d’enfants ;
- on calcule le revenu imposable R=0.9*S où S est le salaire annuel ;
- on calcule le quotient familial QF=R/nbParts ;
- on calcule l’impôt brut I d’après les données suivantes (2019) :
9964 | 0 | 0 |
27519 | 0.14 | 1394.96 |
73779 | 0.3 | 5798 |
156244 | 0.41 | 13913.69 |
0 | 0.45 | 20163.45 |
Chaque ligne a 3 champs : champ1, champ2, champ3. Pour calculer l’impôt I, on recherche la première ligne où QF<=champ1 et on prend les valeurs de cette ligne. Par exemple, pour un salarié marié avec deux enfants et un salaire annuel S de 50000 euros :
Revenu imposable : R=0,9*S=45000
Nombre de parts : nbParts=2+2*0,5=3
Quotient familial : QF=45000/3=15000
La 1re ligne où QF<=champ1 est la suivante :
1 | 27519 0.14 1394.96
|
L’impôt I est alors égal à 0.14*R – 1394,96*nbParts=[0,14*45000-1394,96*3]=2115. L’impôt est arrondi à l’euro inférieur.
Si la relation QF<=champ1 dès la 1re ligne, alors l’impôt est nul.
Si QF est tel que la relation QF<=champ1 n’est jamais vérifiée, alors ce sont les coefficients de la dernière ligne qui sont utilisés. Ici :
1 | 0 0.45 20163.45
|
ce qui donne l’impôt brut I=0.45*R – 20163,45*nbParts.
Plafonnement du quotient familial¶
Pour savoir si le plafonnement du quotient familial QF s’applique, on refait le calcul de l’impôt brut sans les enfants. Toujours pour le salarié marié avec deux enfants et un salaire annuel S de 50000 euros :
Revenu imposable : R=0,9*S=45000
Nombre de parts : nbParts=2 (on ne compte plus les enfants)
Quotient familial : QF=45000/2=22500
La 1re ligne où QF<=champ1 est la suivante :
1 | 27519 0.14 1394.96
|
L’impôt I est alors égal à 0.14*R – 1394,96*nbParts=[0,14*45000-1394,96*2]=3510.
Gain maximal lié aux enfants : 1551 * 2 = 3102 euros
Impôt minimal : 3510-3102 = 408 euros
L’impôt brut avec 3 parts déjà calculé 2115 euros est supérieur à l’impôt minimal 408 euros, donc le plafonnement familial ne s’applique pas ici.
De façon générale, l’impôt brut est sup(impôt1, impôt2) où :
[impôt1] : est l’impôt brut calculé avec les enfants ;
[impôt2] : est l’impôt brut calculé sans les enfants et diminué du gain maximal (ici 1551 euros par demi-part) lié aux enfants ;
Calcul de la décôte
Toujours pour le salarié marié avec deux enfants et un salaire annuel S de 50000 euros :
L’impôt brut (2115) issu de l’étape précédente est inférieur à 2627 euros pour un couple (1595 euros pour un célibataire) : la décôte s’applique donc. Elle est obtenue avec le calcul suivant :
décôte= seuil (couple=1970/célibataire=1196)-0,75* Impôt brut
décôte=1970-0,75*2115=383,75 arrondi à 384 euros.
Nouvel Impôt brut= 2115-384= 1731 euros
Calcul de la réduction d’impôts¶
Au-dessous d’un certain seuil, une réduction de 20 % est faite sur l’impôt brut issu des calculs précédents. En 2019, les seuils sont les suivants :
- célibataire : 21037 euros ;
- couple : 42074 euros ; ( le chiffre 37968 utilisé dans l’exemple ci-dessus semble erroné) ;
Ce seuil est augmenté de la valeur : 3797 * (nombre de demi-parts amenées par les enfants).
Toujours pour le salarié marié avec deux enfants et un salaire annuel S de 50000 euros :
son revenu imposable (45000 euros) est inférieur au seuil (42074+2*3797)=49668 euros ;
il a donc droit à une réduction réduction de 20 % de son impôt : 1731 * 0,2= 346,2 euros arrondi à 347 euros ;
l’impôt brut du contribuable devient : 1731-347= 1384 euros ;
Calcul de l’impôt net
Notre calcul s’arrêtera là : l’impôt net à payer sera de 1384 euros. Dans la réalité, le contribuable peut bénéficier d’autres réductions notamment pour des dons à des organismes d’intérêt public ou général.
Cas des hauts revenus¶
Notre exemple précédent correspond à la majorité des cas de salariés. Cependant le calcul de l’impôt est différent dans le cas des hauts revenus.
Plafonnement de la réduction de 10 % sur les revenus annuels¶
Dans la plupart des cas, le revenu imposable est obtenu par la formule : R=0,9*S où S est le salaire annuel. On appelle cela la réduction des 10 %. Cette réduction est plafonnée. En 2019 :
- elle ne peut être supérieure à 12502 euros ;
- elle ne peut être inférieure à 437 euros ;
Prenons le cas d’un salarié non marié sans enfants et un salaire annuel de 200000 euros :
la réduction de 10 % est de 20000 euros > 12502 euros. Elle est donc ramenée à 12502 euros ;
Plafonnement du quotient familial
Prenon un cas où le plafonnement familial présenté au paragraphe lien intervient. Prenons le cas d’un couple avec trois enfants et des revenus annuels de 100000 euros. Reprenons les étapes du calcul :
- l’abattement de 10 % est de 10000 euros < 12502 euros. Le revenu imposable R est donc 100000-10000=90000 euros ;
- le couple a nbParts=2+0,5*2+1=4 parts ;
- son quotient familial est donc QF= R/nbParts=90000/4=22500 euros ;
- son impôt brut I1 avec enfants est I1=0,14*90000-1394,96*4= 7020 euros ;
- son impôt brut I2 sans enfants :
- QF=90000/2=45000 euros ;
- I2=0,3*90000-5798*2=15404 euros ;
- la règle du plafonnement du quotient familial dit que le gain amené par les enfants ne peut dépasser (1551*4 demi-parts)=6204 euros. Or ici, il est I2-I1=15404-7020= 8384 euros, donc supérieur à 6204 euros ;
- l’impôt brut est donc recalculé comme I3=I2-6204=15404-6204= 9200 euros ;
Ce couple n’aura ni décôte, ni réduction et son impôt final sera de 9200 euros.
Chiffres officiels¶
Le calcul de l’impôt est complexe. Tout au long du document, les tests seront faits avec les exemples suivants. Les résultats sont ceux du simulateur de l’administration fiscale [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm] :
Contribuable | Résultats officiels | Résultats de l’algorithme du document |
---|---|---|
Couple avec 2 enfants et des revenus annuels de 55555 euros | Impôt=2815 euros Taux d’imposition=14 % |
Impôt=2814 euros Taux d’imposition=14 % |
Couple avec 2 enfants et des revenus annuels de 50000 euros | Impôt=1385 euros Décôte=720 euros Réduction=0 euros Taux d’imposition=14 % |
Impôt=1384 euros Décôte=384 euros Réduction=347 euros Taux d’imposition=14 % |
Couple avec 3 enfants et des revenus annuels de 50000 euros | Impôt=0 euro Décôte=384 euros Réduction=346 euros Taux d’imposition=14 % |
Impôt=0 euro Décôte=720 euros Réduction=0 euro Taux d’imposition=14 % |
Célibataire avec 2 enfants et des revenus annuels de 100000 euros | Impôt=19884 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=41 % |
Impôt=19884 euros Surcôte=4480 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=41 % |
Célibataire avec 3 enfants et des revenus annuels de 100000 euros | Impôt=16782 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=41 % |
Impôt=16782 euros Surcôte=7176 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=41 % |
Couple avec 3 enfants et des revenus annuels de 100000 euros | Impôt=9200 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=30 % |
Impôt=9200 euros Surcôte=2180 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=30 % |
Couple avec 5 enfants et des revenus annuels de 100000 euros | Impôt=4230 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=14 % |
Impôt=4230 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=14 % |
Célibataire sans enfants et des revenus annuels de 100000 euros | Impôt=22986 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=41 % |
Impôt= 22986 euros Surcôte=0 euro Décôte=0 euro Réduction=0 euro Taux d’imposition=41 % |
Couple avec 2 enfants et des revenus annuels de 30000 euros | Impôt=0 euro Décôte=0 euro Réduction=0 euro Taux d’imposition=0 % |
Impôt=0 euro Décôte=0 euro Réduction=0 euro Taux d’imposition=0 % |
Célibataire sans enfants et des revenus annuels de 200000 euros | Impôt=64211 euro Décôte=0 euro Réduction=0 euro Taux d’imposition=45 % |
Impôt= 64210 euros Surcôte=7498 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=45 % |
Couple avec 3 enfants et des revenus annuels de 200000 euros | Impôt=42843 euro Décôte=0 euro Réduction=0 euro Taux d’imposition=41 % |
Impôt=42842 euros Surcôte=17283 euros Décôte=0 euro Réduction=0 euro Taux d’imposition=41 % |
Ci-dessus, on appelle surcôte, ce que paient en plus les hauts revenus à cause de deux phénomènes :
- le plafonnement de l’abattement de 10 % sur les revenus annuels ;
- le plafonnement du quotient familial ;
Cet indicateur n’a pu être vérifié car le simulateur de l’administration fiscale ne le donne pas.
On voit que l’algorithme du document donne un impôt juste à chaque fois, avec cependant une marge d’erreur de 1 euro. Cette marge d’erreur provient des arrondis. Toutes les sommes d’argent sont arrondies parfois à l’euro supérieur, parfois à l’euro inférieur. Comme je ne connaissais pas les règles officielles, les sommes d’argent de l’algorithme du document ont été arrondies :
- à l’euro supérieur pour les décôtes et réductions ;
- à l’euro inférieur pour les surcôtes et l’impôt final ;
Dans la suite, des tests seront établis pour vérifier la validité des résultats. Ils seront faits avec les exemples du tableau précédent avec une marge d’erreur acceptée de 1 euro.
L’arborescence des scripts¶
Version 1¶
L’algorithme¶
Nous présentons un premier programme où :
- les données nécessaires au calcul de l’impôt sont codées en dur dans le code sous forme de tableaux et de constantes ;
- les données des contribuables (marié, enfants, salaire) sont dans un premier fichier texte [taxpayersdata.txt] ;
- les résultats du calcul de l’impôt (marié, enfants, salaire, impôt) sont mémorisés dans un second fichier texte [resultats.txt] ;
Le script [version-01/main.php] est le suivant :
1 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 | <?php
// types stricts pour les paramètres de fonctions
declare(strict_types=1);
// constantes globales
define("PLAFOND_QF_DEMI_PART", 1551);
define("PLAFOND_REVENUS_CELIBATAIRE_POUR_REDUCTION", 21037);
define("PLAFOND_REVENUS_COUPLE_POUR_REDUCTION", 42074);
define("VALEUR_REDUC_DEMI_PART", 3797);
define("PLAFOND_DECOTE_CELIBATAIRE", 1196);
define("PLAFOND_DECOTE_COUPLE", 1970);
define("PLAFOND_IMPOT_COUPLE_POUR_DECOTE", 2627);
define("PLAFOND_IMPOT_CELIBATAIRE_POUR_DECOTE", 1595);
define("ABATTEMENT_DIXPOURCENT_MAX", 12502);
define("ABATTEMENT_DIXPOURCENT_MIN", 437);
// définition des constantes locales
$DATA = "taxpayersdata.txt";
$RESULTATS = "resultats.txt";
$limites = array(9964, 27519, 73779, 156244, 0);
$coeffR = array(0, 0.14, 0.3, 0.41, 0.45);
$coeffN = array(0, 1394.96, 5798, 13913.69, 20163.45);
// lecture des données
$data = fopen($DATA, "r");
if (!$data) {
print "Impossible d'ouvrir en lecture le fichier des données [$DATA]\n";
exit;
}
// ouverture fichier des résultats
$résultats = fopen($RESULTATS, "w");
if (!$résultats) {
print "Impossible de créer le fichier des résultats [$RESULTATS]\n";
exit;
}
// on exploite la ligne courante du fichier des données
while ($ligne = fgets($data, 100)) {
// on enlève l'éventuelle marque de fin de ligne
$ligne = cutNewLineChar($ligne);
// on récupère les 3 champs marié:enfants:salaire qui forment $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// on calcule l'impôt
$result = calculImpot($marié, (int) $enfants, (float) $salaire, $limites, $coeffR, $coeffN);
// on inscrit le résultat dans le fichier des résultats
$result = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $result;
fputs($résultats, \json_encode($result, JSON_UNESCAPED_UNICODE) . "\n");
// donnée suivante
}
// on ferme les fichiers
fclose($data);
fclose($résultats);
// fin
exit;
// --------------------------------------------------------------------------
function cutNewLinechar(string $ligne): string {
// on supprime la marque de fin de ligne de $ligne si elle existe
$L = strlen($ligne); // longueur ligne
while (substr($ligne, $L - 1, 1) === "\n" or substr($ligne, $L - 1, 1) === "\r") {
$ligne = substr($ligne, 0, $L - 1);
$L--;
}
// fin
return($ligne);
}
// calcul de l'impôt
// --------------------------------------------------------------------------
function calculImpot(string $marié, int $enfants, float $salaire, array $limites, array $coeffR, array $coeffN): array {
…
// résultat
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
function calculImpot2(string $marié, int $enfants, float $salaire, array $limites, array $coeffR, array $coeffN): 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
function getRevenuImposable(float $salaire): float {
…
// résultat
return floor($revenuImposable);
}
// calcule une décôte éventuelle
function getDecote(string $marié, float $salaire, float $impots): float {
…
// résultat
return ceil($décôte);
}
// calcule une réduction éventuelle
function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
/…
// résultat
return ceil($réduction);
}
|
Le fichier des données taxpayersdata.txt (marié, enfants, salaire) :
1 2 3 4 5 6 7 8 9 10 11 | oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3,100000
oui,3,100000
oui,5,100000
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000
|
Les fichier résultats.txt (marié, enfants, salaire, impôt, surcôte, décôte, réduction, taux d’imposition) des résultats obtenus :
1 2 3 4 5 6 7 8 9 10 11 | {"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}
{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}
{"marié":"oui","enfants":"3","salaire":"200000","impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}
|
Commentaires
- ligne 4 : on force le respect strict du type des paramètres des fonctions ;
- lignes 7-16 : définition de toutes les constantes nécessaire au calcul de l’impôt ;
- ligne 19 : le nom du fichier texte contenant les données des contribuables (marié, enfants, salaire) ;
- ligne 20 : le nom du fichier texte contenant les résultats (marié, enfants, salaire, impôt) du calcul de l’impôt ;
- lignes 21-23 : les trois tableaux des données définissant les différentes tranches d’imposition du calcul de l’impôt ;
- lignes 26-30 : ouverture en lecture [r] du fichier des données contribuables. La fonction [fopen] rend le booléen FALSE si l’ouverture n’a pu se faire ;
- lignes 33-37 : ouverture en écriture [w] du fichier des résultats ;
- lignes 40-51 : boucle de lecture des lignes (marié, enfants, salaire) du fichier des données contribuables ;
- ligne 40 : la fonction [fgets] lit 100 caractères et s’arrête à la 1re marque de fin de ligne rencontrée. Ici toutes les lignes font moins de 100 caractères. Si une marque de fin de ligne a été rencontrée, elle est incluse dans la chaîne rendue. Lorsque la fin du fichier est rencontrée, la fonction [fgets] rend la valeur FALSE ;
- ligne 42 : la marque de fin de ligne est enlevée ;
- ligne 44 : les composantes (marié, enfants, salaire) de la ligne sont récupérées ;
- ligne 46 : l’impôt est calculé. Le résultat est rendu sous la forme d’un tableau associatif (ligne 76) ;
- ligne 48 : au tableau récupéré précédemment, on rajoute les clés [marié, enfants, salaire] ;
- ligne 49 : le résultat est mémorisé dans le fichier des résultats sous la forme d’une chaîne jSON ;
- lignes 53-54 : une fois le fichier des données contribuables exploité totalement, les fichiers sont fermés ;
- ligne 60 : la fonction qui supprime la marque de fin de ligne d’une ligne $ligne. La marque de fin de ligne est la chaîne « rn » sur les systèmes windows, « n » sur les systèmes Unix. Le résultat est la chaîne d’entrée sans sa marque de fin de ligne.
- lignes 63-64 : substr($chaîne,$début,$taille) est la sous-chaîne de $chaîne commençant au caractère $début et ayant au plus $taille caractères ;
La fonction [calculImpot] est la suivante :
1 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 | // constantes globales
define("PLAFOND_QF_DEMI_PART", 1551);
// calcul de l'impôt
// --------------------------------------------------------------------------
function calculImpot(string $marié, int $enfants, float $salaire, array $limites, array $coeffR, array $coeffN): array {
// $marié : oui, non
// $enfants : nombre d'enfants
// $salaire : salaire annuel
// $limites, $coeffR, $coeffN : les tableaux des données permettant le calcul de l'impôt
//
// calcul de l'impôt avec enfants
$result1 = calculImpot2($marié, $enfants, $salaire, $limites, $coeffR, $coeffN);
$impot1 = $result1["impôt"];
// calcul de l'impôt sans les enfants
if ($enfants != 0) {
$result2 = calculImpot2($marié, 0, $salaire, $limites, $coeffR, $coeffN);
$impot2 = $result2["impôt"];
// application du plafonnement du quotient familial
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
$impot2 = $impot2 - $enfants * PLAFOND_QF_DEMI_PART;
} else {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
$impot2 = $impot2 - 2 * PLAFOND_QF_DEMI_PART - ($enfants - 2) * 2 * PLAFOND_QF_DEMI_PART;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// on prend l'impôt le plus fort avec le taux et la surcôte qui vont avec
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// calcul d'une éventuelle décôte
$décôte = getDecote($marié, $salaire, $impot);
$impot -= $décôte;
// calcul d'une éventuelle réduction d'impôts
$réduction = getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// résultat
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
function calculImpot2(string $marié, int $enfants, float $salaire, array $limites, array $coeffR, array $coeffN): array {
…
// résultat
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=salaireAnnuel-abattement
// l'abattement de 10 % a un min et un max
function getRevenuImposable(float $salaire): float {
…
}
// calcule une décôte éventuelle
function getDecote(string $marié, float $salaire, float $impots): float {
…
// résultat
return ceil($décôte);
}
// calcule une réduction éventuelle
function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
…
// résultat
return ceil($réduction);
}
|
Commentaires
- ligne 10 : l’impôt brut est calculé avec les enfants. On obtient un
résultat sous la forme [« impôt » => $impôt, « surcôte » => $surcôte,
« taux » => $coeffR[$i]] avec :
- [‘impôt’] : l’impôt brut ;
- [‘surcôte’] : le montant de la surcôte s’il y a. Celle-ci existe lorsque l’abattement de 10 % dépasse le seuil de 12502 euros ;
- [‘taux’] : le taux d’imposition du contribuable ;
- ligne 11 : l’impôt [impot1] brut à payer ;
- lignes 13-14 : si le contribuable a au moins un enfant, le calcul de l’impôt est refait avec les mêmes données mais avec 0 enfant. Ce second calcul est nécessaire pour voir si la réduction amenée par les enfants (nbParts*coeffN) est supérieure à un certain seuil ;
- ligne 15 : l’impôt brut [impot2] à payer ;
- lignes 16-23 : pour l’impôt brut [impot2], on fait jouer maintenant les enfants : chaque 1/2 part amenée par les enfants permet une réduction de [PLAFOND_QF_DEMI_PART] euros ;
- lignes 25-26 : cas où le contribuable n’a pas d’enfants. Dans ce cas, le calcul de [impot2] est inutile. Il est égal à [impot1] ;
- lignes 29-37 : deux impôts bruts ont été calculés [impot1, impot2]. L’administration fiscale retient le plus fort des deux. On obtient un impôt brut [impot] ;
- lignes 39-40 : le montant brut [impot] peut subir une décôte ;
- lignes 42-43 : le montant brut [impot] peut subir une réduction ;
- ligne 45 : [impot] est désormais l’impôt net à payer. On rend les résultats ;
La fonction [calculImpot2] est la suivante :
1 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 | // --------------------------------------------------------------------------
function calculImpot2(string $marié, int $enfants, float $salaire, array $limites, array $coeffR, array $coeffN): array {
// $marié : oui, non
// $enfants : nombre d'enfants
// $salaire : salaire annuel
// $limites, $coeffR, $coeffN : les tableaux des données permettant le calcul de l'impôt
//
// nombre de parts
$marié = strtolower($marié);
if ($marié === "oui") {
$nbParts = $enfants / 2 + 2;
} else {
$nbParts = $enfants / 2 + 1;
}
// 1 part par enfant à partir du 3e
if ($enfants >= 3) {
// une demi-part de + pour chaque enfant à partir du 3e
$nbParts += 0.5 * ($enfants - 2);
}
// revenu imposable
$revenuImposable = getRevenuImposable($salaire);
// surcôte
$surcôte = floor($revenuImposable - 0.9 * $salaire);
// pour des pbs d'arrondi
if ($surcôte < 0) {
$surcôte = 0;
}
// quotient familial
$quotient = $revenuImposable / $nbParts;
// est mis à la fin du tableau limites pour arrêter la boucle qui suit
$limites[count($limites) - 1] = $quotient;
// calcul de l'impôt
$i = 0;
while ($quotient > $limites[$i]) {
$i++;
}
// du fait qu'on a placé $quotient à la fin du tableau $limites, la boucle précédente
// ne peut déborder du tableau $limites
// maintenant on peut calculer l'impôt
$impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
// 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
function getRevenuImposable(float $salaire): float {
// résultat
return floor($revenuImposable);
}
|
Commentaires
- on applique ici le calcul de l’impôt dit au barême progressif ;
- ligne 9 : strtolower($chaîne) rend $chaîne en minuscules ;
- lignes 10-19 : calcul du nombre de parts du contribuable ;
- ligne 21 : on calcule le revenu imposable à l’aide d’une fonction. En effet, on a vu que ce n’est pas toujours 0.9*revenusAnnuels. L’abattement de 10 % est en effet limité à 12502 euros ;
- ligne 23 : calcul de l’éventuelle surcôte si le revenu imposable est supérieur à 0.9*revenusAnnuels ;
- lignes 25-27 : corrige le fait qu’à cause d’erreurs d’arrondis, on a parfois [$surcôte=-1] ;
- ligne 29 : le quotient familial ;
- lignes 30-36 : ce quotient permet de trouver la tranche d’imposition du contribuable ;
- ligne 40 : une fois la tranche d’imposition du contribuable trouvée, son impôt brut peut être calculé. La fonction floor($x) rend la valeur entière immédiatement inférieure à [$x] ;
- ligne 42 : on rend les informations calculées ;
La fonction [getRevenuImposable] est la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // constantes globales
define("ABATTEMENT_DIXPOURCENT_MAX", 12502);
define("ABATTEMENT_DIXPOURCENT_MIN", 437);
// revenuImposable=salaireAnnuel-abattement
// l'abattement a un min et un max
function getRevenuImposable(float $salaire): float {
// abattement de 10% du salaire
$abattement = 0.1 * $salaire;
// cet abattement ne peut dépasser ABATTEMENT_DIXPOURCENT_MAX
if ($abattement > ABATTEMENT_DIXPOURCENT_MAX) {
$abattement = ABATTEMENT_DIXPOURCENT_MAX;
}
// l'abattement ne peut être inférieur à ABATTEMENT_DIXPOURCENT_MIN
if ($abattement < ABATTEMENT_DIXPOURCENT_MIN) {
$abattement = ABATTEMENT_DIXPOURCENT_MIN;
}
// revenu imposable
$revenuImposable = $salaire - $abattement;
// résultat
return floor($revenuImposable);
}
|
Commentaires
- ligne 5 : l’abattement normal est de 10 % du salaire annuel ;
- lignes 7-9 : l’abattement ne peut dépasser l’abattement maximal [ABATTEMENT_DIXPOURCENT_MAX] ;
- lignes 10-13 : l’abattement ne peut être inférieur à l’abattement minimal [ABATTEMENT_DIXPOURCENT_MIN] ;
- ligne 15 : calcul du revenu imposable ;
La fonction [getDecote] est la suivante :
1 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 | // constantes globales
define("PLAFOND_DECOTE_CELIBATAIRE", 1196);
define("PLAFOND_DECOTE_COUPLE", 1970);
define("PLAFOND_IMPOT_COUPLE_POUR_DECOTE", 2627);
define("PLAFOND_IMPOT_CELIBATAIRE_POUR_DECOTE", 1595);
// calcule une décôte éventuelle
function getDecote(string $marié, float $salaire, float $impots): float {
// au départ, une décôt nulle
$décôte = 0;
// montant maximal d'impôt pour avoir la décôte
$plafondImpôtPourDécôte = $marié === "oui" ? PLAFOND_IMPOT_COUPLE_POUR_DECOTE : PLAFOND_IMPOT_CELIBATAIRE_POUR_DECOTE;
if ($impots < $plafondImpôtPourDécôte) {
// montant maximal de la décôte
$plafondDécôte = $marié === "oui" ? PLAFOND_DECOTE_COUPLE : PLAFOND_DECOTE_CELIBATAIRE;
// décôte théorique
$décôte = $plafondDécôte - 0.75 * $impots;
// la décôte ne peut dépasser le montant de l'impôt
if ($décôte > $impots) {
$décôte = $impots;
}
// pas de décôte <0
if ($décôte < 0) {
$décôte = 0;
}
}
// résultat
return ceil($décôte);
}
|
Commentaires
- ligne 6 : montant maximal de l’impôt brut pour avoir droit à une décôte. Ce montant est différent pour les célibataires et les couples ;
- ligne 7 : si le contribuable a droit à la décôte ;
- ligne 11 : la formule de la décôte. [plafondDécôte] est le montant maximal de la décôte. Ce montant maximal est calculé ligne 9. Là encore il dépend de la situation du contribuable, marié ou célibataire ;
- lignes 13-15 : la décôte ne peut être supérieure à l’impôt brut à payer. C’est le cas par exemple si [impots] vaut 0 en ligne 11 ;
- lignes 17-19 : pour éviter un arrondi à -1 ;
La fonction [getRéduction] est la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // constantes globales
define("PLAFOND_REVENUS_CELIBATAIRE_POUR_REDUCTION", 21037);
define("PLAFOND_REVENUS_COUPLE_POUR_REDUCTION", 42074);
define("VALEUR_REDUC_DEMI_PART", 3797);
// calcule une réduction éventuelle
function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
// le plafond des revenus pour avoir droit à la réduction de 20%
$plafondRevenuPourRéduction = $marié === "oui" ? PLAFOND_REVENUS_COUPLE_POUR_REDUCTION : PLAFOND_REVENUS_CELIBATAIRE_POUR_REDUCTION;
$plafondRevenuPourRéduction += $enfants * VALEUR_REDUC_DEMI_PART;
if ($enfants > 2) {
$plafondRevenuPourRéduction += ($enfants - 2) * VALEUR_REDUC_DEMI_PART;
}
// revenu imposable
$revenuImposable = getRevenuImposable($salaire);
// réduction
$réduction = 0;
if ($revenuImposable < $plafondRevenuPourRéduction) {
// réduction de 20%
$réduction = 0.2 * $impots;
}
// résultat
return ceil($réduction);
}
|
Commentaires
lignes 4-10 : pour avoir droit à une réduction d’impôt, il faut que le revenu imposable (ligne 10) soit inférieur à un plafond calculé lignes 4-8 ;
lignes 13-16 : s’il remplit les conditions, le contribuable a droit à une réduction d’impôt de 20 % (ligne 15) ;
Résultats
Le fichier des données taxpayersdata.txt (marié, enfants, salaire) :
1 2 3 4 5 6 7 8 9 10 11 | oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3,100000
oui,3,100000
oui,5,100000
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000
|
Les fichier résultats.txt (marié, enfants, salaire, impôt, surcôte, décôte, réduction, taux d’imposition) des résultats obtenus :
1 2 3 4 5 6 7 8 9 10 11 | {"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}
{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}
{"marié":"oui","enfants":"3","salaire":"200000","impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}
|
Les résultats obtenus sont conformes à ceux obtenus avec le simulateur de l’administration fiscale.
Conclusion¶
L’algorithme de calcul de l’impôt, même dans des cas réputés simples, est complexe. Nous ne reviendrons plus dessus. Au fil des versions, son cœur restera le même malgré quelques changements de présentation. On ne commentera alors que ces derniers.
Version 2¶
Les modifications¶
Dans la version précédente, les données nécessaires au calcul de l’impôt étaient codées en dur sous la forme de constantes et de tableaux. Cette méthode est à prohiber. Dans la nouvelle version, ces données sont externalisées dans un fichier jSON :
Le contenu du fichier [taxadmindata.json] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | {
"limites": [9964, 27519, 73779, 156244, 0],
"coeffR": [0, 0.14, 0.3, 0.41, 0.45],
"coeffN": [0, 1394.96, 5798, 13913.69, 20163.45],
"PLAFOND_QF_DEMI_PART": 1551,
"PLAFOND_REVENUS_CELIBATAIRE_POUR_REDUCTION": 21037,
"PLAFOND_REVENUS_COUPLE_POUR_REDUCTION": 42074,
"VALEUR_REDUC_DEMI_PART": 3797,
"PLAFOND_DECOTE_CELIBATAIRE": 1196,
"PLAFOND_DECOTE_COUPLE": 1970,
"PLAFOND_IMPOT_COUPLE_POUR_DECOTE": 2627,
"PLAFOND_IMPOT_CELIBATAIRE_POUR_DECOTE": 1595,
"ABATTEMENT_DIXPOURCENT_MAX": 12502,
"ABATTEMENT_DIXPOURCENT_MIN": 437
}
|
La nouvelle version [version-02/main.php] est la suivante :
1 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 152 153 154 155 156 | <?php
// respect strict des types déclarés des paramètres des fonctions
declare (strict_types=1);
// définition des constantes
$TAXPAYERSDATA = "taxpayersdata.txt";
$RESULTATS = "resultats.txt";
$TAXADMINDATA = "taxadmindata.json";
// on récupère le contenu du fichier des données fiscales
$fileContents = \file_get_contents($TAXADMINDATA);
$erreur = FALSE;
// erreur ?
if (!$fileContents) {
// on note l'erreur
$erreur = TRUE;
$message = "Le fichier des données [$TAXADMINDATA] n'existe pas";
}
if (!$erreur) {
// on récupère le code jSON du fichier de configuration dans un tableau associatif
$taxAdminData = \json_decode($fileContents, true);
// erreur ?
if (!$taxAdminData) {
// on note l'erreur
$erreur = TRUE;
$message = "Le fichier de données jSON [$TAXADMINDATA] n'a pu être exploité correctement";
}
}
// erreur ?
if ($erreur) {
print "$message\n";
exit;
}
// ouverture fichier des résultats
$résultats = fopen($RESULTATS, "w");
if (!$résultats) {
print "Impossible de créer le fichier des résultats [$RESULTATS]\n";
// sortie
exit;
}
// ouverture fichier des données contribuables
$taxpayersdata = fopen($TAXPAYERSDATA, "r");
if (!$taxpayersdata) {
print "Impossible d'ouvrir le fichier des contribuables [$TAXPAYERSDATA]\n";
// sortie
exit;
}
// on exploite la ligne courante du fichier des données
while ($ligne = fgets($taxpayersdata, 100)) {
// on enlève l'éventuelle marque de fin de ligne
$ligne = cutNewLineChar($ligne);
// on récupère les 3 champs marié:enfants:salaire qui forment $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// on calcule l'impôt
$result = calculImpot($taxAdminData, $marié, (int) $enfants, (int) $salaire);
// on inscrit le résultat dans le fichier des résultats
$result = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $result;
fputs($résultats, \json_encode($result, JSON_UNESCAPED_UNICODE) . "\n");
// donnée suivante
}
// on ferme les fichiers
fclose($taxpayersdata);
fclose($résultats);
// fin
exit;
// --------------------------------------------------------------------------
function cutNewLinechar(string $ligne) {
…
// fin
return($ligne);
}
// calcul de l'impôt
// --------------------------------------------------------------------------
function calculImpot(array $taxAdminData, string $marié, int $enfants, float $salaire) {
// $marié : oui, non
// $enfants : nombre d'enfants
// $salaire : salaire annuel
// $taxAdminData : données de l'administration fiscale
//
// calcul de l'impôt avec enfants
$result1 = calculImpot2($taxAdminData, $marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// calcul de l'impôt sans les enfants
if ($enfants != 0) {
$result2 = calculImpot2($taxAdminData, $marié, 0, $salaire);
$impot2 = $result2["impôt"];
// application du plafonnement du quotient familial
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
$impot2 = $impot2 - $enfants * $taxAdminData["PLAFOND_QF_DEMI_PART"];
} else {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
$impot2 = $impot2 - 2 * $taxAdminData["PLAFOND_QF_DEMI_PART"] - ($enfants - 2) * 2 * $taxAdminData["PLAFOND_QF_DEMI_PART"];
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// on prend l'impôt le plus fort avec le taux et la surcôte qui vont avec
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// calcul d'une éventuelle décôte
$décôte = getDecote($taxAdminData, $marié, $salaire, $impot);
$impot -= $décôte;
// calcul d'une éventuelle réduction d'impôts
$réduction = getRéduction($taxAdminData, $marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// résultat
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
function calculImpot2(array $taxAdminData, string $marié, int $enfants, float $salaire) {
// $marié : oui, non
…
// 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
function getRevenuImposable(array $taxAdminData, float $salaire): float {
…
// résultat
return floor($revenuImposable);
}
// calcule une décôte éventuelle
function getDecote(array $taxAdminData, string $marié, float $salaire, float $impots): float {
…
// résultat
return ceil($décôte);
}
// calcule une réduction éventuelle
function getRéduction(array $taxAdminData, string $marié, float $salaire, int $enfants, float $impots): float {
…
// résultat
return ceil($réduction);
}
|
Commentaires
- lignes 11-19 : on essaie de lire le contenu du fichier jSON nommé [TAXADMINDATA] ;
- lignes 21-30 : si on a réussi à lire le fichier jSON, son contenu est décodé dans le tableau associatif [$taxAdminData] ;
- lignes 32-36 : si on a rencontré une erreur dans une des deux opérations précédentes, on écrit un message d’erreur sur la console et on s’arrête ;
- la différence avec la version 01 est qu’ici les données (tableaux et
constantes) de l’administration fiscale sont dans le tableau
associatif [$taxAdminData] alors que dans la version 01, elles
étaient dans des tableaux et constantes globales. C’est la globalité
de ces constantes qui fait la différence entre les deux versions :
- dans la version 01, les constantes étaient connues dans toutes les fonction de [main.php] ;
- pour arriver au même résultat dans la version 02, il faut passer le tableau associatif [$taxAdminData] en paramètre à toutes les fonctions (lignes 83, 129, 138, 145, 152) ;
- chaque fonction de la version 02 doit utiliser le contenu du tableau [$taxAdminData] ;
- lignes 83-126 : dans la fonction [calculerImpot], là où on utilisait des constantes globales ou les tableaux [limites, coeffR, coeffN], on utilise désormais le contenu du tableau [$taxAdminData] reçu en paramètre (lignes 99, 102) ;
- toutes les autres fonctions sont réécrites de la même façon ;
Les résultats obtenus sont les mêmes que ceux obtenus dans la version précédente.
Conclusion¶
La version 02 est bien plus souple que la version 01. En 2020, l’algorithme de calcul de l’impôt sera probablement le même qu’en 2019. Seules les tranches d’imposition et les constantes de calcul auront changé. Il suffira alors de mettre à jour le fichier [taxadmindata.json]. Avec la version 01, il faut aller dans le code modifier les tranches d’imposition et les constantes de calcul. Or il est probable que les gens qui ont à changer les valeurs des tranches d’imposition et des constantes de calcul n’ont pas accès au code de l’algorithme.
Les classes¶
Vocabulaire : Une classe class est un type PHP. Une variable de ce type est appelée objet. Un objet est une instance (exemplaire) de classe.
L’arborescence des scripts¶
Toute variable peut devenir un objet doté d’attributs¶
Le script [classes-01.php] est le suivant :
1 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 | <?php
// un objet générique
// $obj1=new stdClass();
// tout variable peut avoir des attributs par construction
$obj1->attr1 = "un";
$obj1->attr2 = 100;
// affiche l'objet
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// modifie l'objet
$obj1->attr2 += 100;
// affiche l'objet
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// copie la valeur de objet1 (adresse de l'objet pointé) dans objet2
// les deux variables sont alors différentes mais pointent sur le même objet
$obj2 = $obj1;
// modifie obj2
$obj2->attr2 = 0;
// affiche les deux objets
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
// change l'objet pointé par obj1
$obj1 = new stdClass();
print "obj1 :\n";
print_r($obj1);
print "obj2 :\n";
print_r($obj2);
// affecte la référence (l'adresse) de objet2 à objet3
// $obj2 et $obj3 sont alors une seule et même variable
$obj3 = &$obj2;
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// modifie obj3
$obj3->attr2 = 10;
// affiche les deux objets
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
print "objet3=[$obj3->attr1,$obj3->attr2]\n";
// change l'objet pointé par obj2
$obj2 = new stdClass();
$obj2->attr3 = "deux";
$obj2->attr4 = 20;
// affiche les deux objets $obj2 et $obj3
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// un objet est-il un dictionnaire ?
print count($obj3) . "\n";
while (list($attribut, $valeur) = each($obj3)) {
print "obj3[$attribut]=$valeur\n";
}
// fin
exit;
|
Résultats :
1 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 | Warning: Creating default object from empty value in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 6
objet1=[un,100]
objet1=[un,200]
objet1=[un,0]
objet2=[un,0]
obj1 :
stdClass Object
(
)
obj2 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
obj2 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
obj3 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
objet2=[un,10]
objet3=[un,10]
obj2 :
stdClass Object
(
[attr3] => deux
[attr4] => 20
)
obj3 :
stdClass Object
(
[attr3] => deux
[attr4] => 20
)
Warning: count(): Parameter must be an array or an object that implements Countable in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 50
1
Deprecated: The each() function is deprecated. This message will be suppressed on further calls in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 51
obj3[attr3]=deux
obj3[attr4]=20
|
Commentaires
- ligne 6 : la notation $obj->attr désigne l’attribut attr de la variable $obj. S’il n’existe pas il est créé, faisant ainsi de la variable $obj, un objet avec attributs. Nous avons vu que PHP crée alors par défaut un objet de type stdClass ;
- ligne 16 : l’expression $obj2=$obj1, lorsque $obj1 est un objet, est une copie d’objets par référence : $obj2 et $obj1 sont des références (adresses) sur un même objet. L’objet lui-même peut être modifié par l’une ou l’autre des références ;
- lignes 23-27 : visent à montrer que $obj1 et $obj2 sont deux
variables différentes : elles ne sont pas à la même adresse mémoire :
- $obj2=$obj1 a copié la valeur de $obj1 dans la variable $obj2 (opération 1 ci-dessus). La valeur de $obj1 est l’adresse d’un l’objet. Ainsi $obj1 et $obj2 pointent sur le même objet. Lorsqu’on manipule une variable $obj et que celle-ci pointe sur un objet, PHP manipule l’objet pointé par la variable $obj. D’après le schéma ci-dessous, on voit qu’on peut modifier l’objet pointé soit via $obj1 soit via $obj2. C’est ce que montrent les lignes 4 et 5 des résultats ;
- ligne 30 : l’expression $obj3=&$obj2 fait que $obj2 et $obj3 sont
à la même adresse [1 ci-dessous]. On pourrait dire que les deux
variables sont des alias du même emplacement mémoire. Elles pointent
toutes les deux sur un objet, Objet A ci-dessous [2] ;
- l’opération $obj2=new stdClass() fait qu’un nouvel objet, Objet B est créé [3 ci-dessous] et l’adresse de ce nouvel objet est affectée à la variable $obj2. Puisque $obj2 et $obj3 sont deux alias du même emplacement mémoire, $obj3 pointe également sur le nouvel objet Objet B. C’est ce que montrent les lignes 16-27 et 30-41 des résultats ;
- lignes 52-54 : montrent qu’un objet peut être parcouru comme un dictionnaire. Les clés du dictionnaire sont les noms des attributs et les valeurs du dictionnaire, les valeurs de ces mêmes attributs ;
- ligne 51 : la fonction count peut être appliquée à un objet (moyennant un warning) mais ne donne pas, comme on aurait pu s’y attendre, le nombre d’attributs. Donc un objet présente des similitudes avec un dictionnaire mais n’en est pas un ;
Une classe Personne sans attributs déclarés¶
Le script [classes-02.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php
class Personne {
// attributs de la classe
// non déclarés - peuvent être créés dynamiquement
// méthode
function identite() {
// a priori, utilise des attributs inexistants
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// les attributs sont publics et peuvent être créés dynamiquement
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// appel d'une méthode
print "personne=" . $p->identite() . "\n";
// fin
exit;
|
Résultats :
1 | personne=[Paul,Langevin,48]
|
Commentaires
- lignes 3-13 : définissent une classe Personne. Une classe est un moule à partir duquel on crée des objets. Elle regroupe des attributs et des fonctions appelées méthodes. Il n’y a pas obligation à déclarer les attributs ;
- lignes 8-11 : la méthode identité affiche la valeur de trois attributs non déclarés dans la classe. Le mot clé $this désigne l’objet auquel on applique la méthode ;
- ligne 17 : on crée un objet $p de type Personne. Le mot clé new sert à créer un nouvel objet. L’opération rend une référence sur l’objet créé (donc une adresse). Diverses écritures sont possibles : new Personne(), new Personne, new personne. Le nom de la classe est insensible à la casse ;
- lignes 18-20 : les trois attributs nécessaires à la méthode identité sont créés dans l’objet $p ;
- ligne 22 : la méthode identité de la classe Personne est appliquée sur l’objet $p. Dans le code (lignes 8-11) de la méthode identité, $this référence le même objet que $p ;
La classe Personne avec attributs déclarés¶
Le script [classes-03.php] est le suivant :
1 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 | <?php
class Personne {
// attributs de la classe
var $prenom;
var $nom;
var $age;
// méthode
function identite() {
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// les attributs sont publics
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// appel d'une méthode
print "personne=" . $p->identite() . "\n";
// fin
exit;
|
Résultats :
1 | personne=[Paul,Langevin,48]
|
Commentaires
- lignes 6-8 : les attributs de la classe sont explicitement déclarés avec le mot clé var ;
La classe Personne avec un constructeur¶
Les exemples précédents montraient des classes Personne exotiques telles qu’on pouvait les trouver dans PHP 4. Il n’est pas conseillé de suivre ces exemples. Nous présentons maintenant une classe Personne [classes-04.php] correspondant aux bonnes pratiques de PHP 7 :
1 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
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
class Personne {
// attributs de la classe
private $prenom;
private $nom;
private $age;
// getters and setters
public function getPrenom(): string {
return $this->prenom;
}
public function getNom(): string {
return $this->nom;
}
public function getAge(): int {
return $this->age;
}
public function setPrenom(string $prenom): void {
$this->prenom = $prenom;
}
public function setNom(string $nom): void {
$this->nom = $nom;
}
public function setAge(int $age): void {
$this->age = $age;
}
// constructeur
public function __construct(string $prenom, string $nom, int $age) {
// on passe par les set
$this->setPrenom($prenom);
$this->setNom($nom);
$this->setAge($age);
}
// méthode toString
public function __toString(): string {
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// création d'un objet Personne
$p = new Personne("Paul", "Langevin", 48);
// identité de cette personne
print "personne=$p\n";
// on change l'âge
$p->setAge(14);
// identité de la personne
print "personne=$p\n";
// fin
exit;
|
Résultats :
1 2 | personne=[Paul,Langevin,48]
personne=[Paul,Langevin,14]
|
Commentaires
- lignes 6-50 : la classe Personne ;
- lignes 7-9 : les attributs privés (private) de la classe. Ces attributs ne sont visibles qu’à l’intérieur de la classe. Les autres mots clés utilisables sont :
- parce que les attributs sont privés, on ne peut y accéder de l’extérieur de la classe. On ne peut donc écrire le code suivant :
1 2 | $p=new Personne() ;
$p->nom="Landru" ;
|
Ici, on est en-dehors de la classe Personne. Comme l’attribut nom est privé, la ligne 2 est incorrecte. Pour initialiser les champs privés de l’objet $p, il y a deux moyens :
- utiliser les méthodes publiques set et get (le nom de ces méthodes peut être quelconque) des lignes 12-34. On pourra alors écrire :
1 2 | $p=new Personne() ;
$p->setNom("Landru") ;
|
- utiliser le constructeur des lignes 37-42. On écrira alors :
1 | $p=new Personne("Michel","Landru",44) ;
|
L’écriture ci-dessus, appelle automatiquement la méthode de la classe Personne appelée __construct ;
- ligne 59 : cette ligne affiche la personne $p sous la forme d’une chaîne de caractères. Pour ce faire, la méthode de la classe Personne appelée __toString (lignes 45-47) est utilisée ;
- toutes les méthodes de la classe (fonctions) ont été préfixées par le mot clé public qui indique que la fonction est visible en-dehors de la classe. Les autres mots clés utilisables sont, comme pour les attributs et avec la même signification, private et protected. Sans attribut explicite de visibilité, la fonction est de visibilité implicite public ;
La classe Personne avec contrôles de validité dans le constructeur¶
Le constructeur d’une classe est le bon endroit pour vérifier que les valeurs d’initialisation de l’objet sont corrects. Mais un objet peut être également initialisé par ses méthodes set ou équivalentes. Pour éviter de mettre à deux endroits différents les mêmes vérifications, on pourra mettre ces dernières dans les méthodes set. Si une valeur d’initialisation d’un objet se révèle incorrecte, on lancera une exception. Voici un exemple.
On déplace tout d’abord la définition de la classe Personne dans son propre fichier [Personne.php] :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms;
namespace Exemples;
// classe Personne
class Personne {
// attributs de la classe
private $prenom;
private $nom;
private $age;
// getters et setters
public function getPrenom(): string {
return $this->prenom;
}
public function getNom(): string {
return $this->nom;
}
public function getAge(): int {
return $this->age;
}
public function setPrenom(string $prénom): void {
// le prénom doit être non vide
$prénom = trim($prénom);
if ($prénom === "") {
throw new \Exception("Le prénom doit être non vide");
} else {
$this->prenom = $prénom;
}
}
public function setNom(string $nom): void {
// le nom doit être non vide
$nom = trim($nom);
if ($nom === "") {
throw new \Exception("Le nom doit être non vide");
} else {
$this->nom = $nom;
}
}
public function setAge(int $âge): void {
// l'âge doit être valide
if ($âge < 0) {
throw new \Exception("L'âge doit être un entier positif ou nul");
} else {
$this->age = $âge;
}
}
// constructeur
public function __construct(string $prenom, string $nom, int $age) {
// on passe par les set
$this->setPrenom($prenom);
$this->setNom($nom);
$this->setAge($age);
}
// méthode
public function initWithPersonne(Personne $p): void {
// initialise l'objet courant avec une personne $p
$this->__construct($p->prenom, $p->nom, $p->age);
}
// méthode toString
function __toString(): string {
return "[$this->prenom,$this->nom,$this->age]";
}
}
|
Commentaires
- ligne 4 : on demande à ce que le type des paramètres des fonctions soit respecté lorsqu’il est déclaré ;
- ligne 7 : définit un espace de noms (namespace). Le nom complet (on dit qualifié) de la classe Personne est alors\ExemplesPersonne. Notez le caractère \ commençant le nom qualifié : on a alors un nom qualifié absolu. Si ce caractère est absent, on a un nom qualifié relatif (relatif à l’espace de noms courant). Ainsi si deux classes A et B font partie du même espace de noms E, dans le code de la classe A on pourra atteindre la classe B par la notation relative B. Si la classe A fait partie de l’espace de noms E1 et B de l’espace de noms E2, dans le code de A, B sera atteint par la notation absolue \E2B. Définir une classe à l’intérieur d’un espace de noms n’est pas obligatoire mais Netbeans émet un warning si on ne le fait pas. Donc on le fera. Par ailleurs, les espaces de noms devraient correspondre à l’arborescence des fichiers. Ainsi la classe A dans un espace de noms E1 devrait être dans un fichier E1/A.php. Ce n’est pas obligatoire mais là encore Netbeans émet un avertissement si on ne le fait pas. Sur l’exemple de la classe [ExemplesPersonne], Netbeans émet un avertissement parce que l’arborescence du fichier [Personne.php] est [exemples/classes/Personne.php] et ne correspond donc pas à l’espace de noms. Il ne faut pas confondre arborescence et espace de noms. Le nom pleinement qualifié d’une classe utilise un espace de noms et n’a rien à voir avec l’arborescence du fichier PHP de la classe). Le lien arborescence / espace de noms est facultatif et peut ne pas être observé, comme nous l’avons fait ici;
- lignes 12-14 : les trois attributs privés de la classe ;
- lignes 29-37 : initialisation de l’attribut prenom et vérification de la valeur d’initialisation ;
- ligne 31 : la fonction trim($chaine) élimine les espaces qui se trouvent en début et fin de $chaine. Ainsi trim(« abcd «) est la chaîne «abcd» et trim « « est la chaîne vide ;
- ligne 32 : si le prénom est vide, alors on lance une exception (ligne 33) sinon le prénom est mémorisé (ligne 35). Pour lancer une exception on a utilisé ici la classe prédéfinie [Exception]. On est obligés ici d’utiliser son nom absolu [Exception]. Si on utilise son nom relatif [Exception] alors cette classe sera cherchée dans l’espace de noms du moment, ç-à-d l’espace de noms [Exemples] de la classe Personne. Ainsi l’interpréteur PHP cherchera une classe de nom absolu [ExemplesException] qui n’existe pas ;
La classe [Personne] est utilisée par le script [classes-05.php] suivant :
1 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 | <?php
// respect strict du type des paramètres des fonctions
declare(strict_types=1);
// inclusion définition de la classe Personne
require_once __DIR__."/Personne.php";
// nom qualifié de la classe Personne
use \Exemples\Personne;
// test
// création d'un objet Personne
$p = new Personne("Paul", "Langevin", 48);
// identité de cette personne
print "Exemple1, personne=$p\n";
// création d'un objet Personne erroné
try {
$p = new Personne("xx", "yy", "zz");
} catch (\Exception $e1) {
print "Exemple2, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
print "Exemple2, erreur : " . $e2->getMessage() . "\n";
}
// création d'un objet Personne erroné
try {
$p = new Personne("", "yy", 10);
} catch (\Exception $e1) {
print "Exemple3, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
print "Exemple3, erreur : " . $e2->getMessage() . "\n";
}
// fin
exit;
|
Commentaires
- ligne 7 : le script va utiliser la classe [Personne]. Il faut alors dire à l’interpréteur PHP où il peut trouver la définition de cette classe. C’est le rôle des instructions [include fichier] et [require fichier]. Ici on a utilisé l’instruction [include]. La différence entre les deux instructions est la suivante : si l’instruction [include fichier] rencontre des erreurs lors du chargement de [fichier] une erreur de niveau [E_WARNING] est émise mais l’exécution continue alors que [require] dans le même cas génère une erreur fatale et l’exécution du script s’arrête. Chacune des deux instructions a une variante [include_once] et [require_once]. Ces deux variantes permettent de gérer le cas des inclusions multiples d’un même fichier. On peut imaginer ici un projet constitué de plusieurs scripts PHP dont plusieurs référencent la classe [Personne]. Leur exécution va alors provoquer plusieurs fois l’inclusion du fichier [Personne.php] et provoquer une erreur car une classe ne peut être définie deux fois. La solution est d’utiliser les variantes [_once] qui assurent que le fichier ne sera inclus qu’une fois dans le script global du projet ;
- ligne 7 : la constante [__DIR__] est une constante PHP qui désigne le nom complet du dossier dans lequel se trouve le script contenant la constante [__DIR__]. Ainsi l’expression de la ligne 17 :
1 | require_once __DIR__."/Personne.php";
|
sera équivalente à quelque chose comme :
1 | require_once ‘C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes/Personnes.php’
|
Dans le chemin du fichier, on peut utiliser indifféremment les signes / et \ ;
- ligne 14 : on utilise la classe [Personne] que nous venons de
définir. Le script [classes-05.php] n’a pas d’espaces de noms. La
ligne 14 utilise le nom relatif de la classe [Personne] sans
espace de noms. En l’absence d’espace de noms de la classe
[Personne], celle-ci est cherchée dans le script
[classes-05.php] lui même et ne sera donc pas trouvée. Il y a
deux solutions à ce problème :
- utiliser le nom complet de la classe [ExemplesPersonne] ;
- utiliser l’instruction use de la ligne 10. Celle-ci indique que le code qui suit utilise la classe [ExemplesPersonne] ;
- ligne 10 : l’instruction use permet à l’interpréteur de savoir que la classe [Personne] référencée ligne 14 est en réalité la classe [ExemplesPersonne]. Ceci dit, où l’interpréteur va-t-il trouver le code de cette classe ? C’est la ligne 7 qui le lui dit. Celle-ci indique que pour exécuter le script courant il faut également charger le script [Personne.php]. On a utilisé ici le nom relatif du fichier. Il sera donc cherché dans le dossier qui contient le script [classes-05.php]. Il faut donc que les scripts [Personne.php] et [classes-05.php] soient dans le même dossier. C’est le cas ici où ils sont tous les deux dans le dossier [exemples/classes]. L’instruction de la ligne 10 est équivalente à :
1 | use \Exemples\Personne as Personne;
|
L’instruction [use] ci-dessus dit que l’alias [Personne] désigne la classe [ExemplesPersonne] ;
- ligne 14 : un objet [Personne] est créé. C’est la méthode [__construct] de la classe [Personne] qui va être ici implicitement exécutée ;
- ligne 16 : fait afficher la Personne $p. Pour être affichée, la valeur de la variable $p doit être transformée en chaîne de caractères. Implicitement c’est la méthode [Personne.__toString] qui est alors exécutée. Celle-ci doit donc rendre une chaîne de caractères ;
- nous avons vu que le constructeur de la classe [Personne] pouvait lancer une exception de type [Exception]. Il faut donc gérer celle-ci. Aussi le code de la ligne 14 est-il incomplet. Il faut utiliser celui des lignes 18-24 pour gérer correctement l’exception qui peut se produire. Ici on en produit volontairement une en passant un âge qui n’est pas un entier. Dans ce cas particulier l’exception qui se produit est lancée par l’interpréteur PHP et non par le code de la classe [Personne]. En effet, la signature de la méthode [Personne.__construct] est la suivante :
1 | function __construct(string $prenom, string $nom, int $age)
|
Il faut donc que le paramètre [age] passé au constructeur soit de type entier. Si ce n’est pas le cas, l’interpréteur PHP lance une erreur de type [TypeError]. Par ailleurs, les méthodes [set] de la classe [Personne] lancent, elles, une exception de type [Exception]. Comme le constructeur qui les appelle n’a pas de structure try / catch, l’exception remonte d’un niveau, au code qui a appelé le constructeur, ç-à-d le code du script [classes-05.php]. Finalement, le script [classes-05.php] peut recevoir deux types d’exception : \Exception ou \TypeError. On notera que lorsque le développeur est sûr que certaines exceptions ne peuvent se produire, il n’utilisera pas les options catch correspondantes. Ici elles sont systématiquement utilisées par unique souci de démonstration. Les options catch seront cependant utilisées pour toute exception possible, même peu probable ;
Pour cette raison, la structure try des lignes 18-24 a deux catch pour gérer séparément les deux types d’exception ;
- ligne 20 : on peut écrire indifféremment [Exception] ou
[Exception] :
- le 1re version utilise le nom relatif de la classe, relatif à l’espace de noms du script. Celui-ci n’en a pas. Son espace de noms est alors la racine des espaces de noms : . Donc écrire ici [Exception] revient à écrire [Exception]. Or la classe [Exception] se trouve bien dans l’espace de noms [] ;
Il est préférable d’utiliser le nom absolu des exceptions prédéfinies de PHP dans un script n’ayant pas d’espaces de noms lui-même. Ainsi si on décide de donner un espace de noms à ce script, l’écriture des noms absolus de classes reste valide alors que dans l’autre cas, le changement d’espace de noms va provoquer des erreurs sur les noms relatifs des classes ;
- ligne 21 : lorsqu’il y a exception, la méthode [Exception→getMessage] permet d’obtenir le message d’erreur de l’exception. Il en est de même pour une erreur de type [TypeError]. Dans la méthode [Personne.setPrenom], on a écrit :
1 2 3 4 5 6 7 8 9 | public function setPrenom(string $prénom) {
// le prénom doit être non vide
$prénom = trim($prénom);
if ($prénom === "") {
throw new \Exception("Le prénom doit être non vide");
} else {
$this->prenom = $prénom;
}
}
|
Ligne 5, une exception est lancée avec le message d’erreur [Le prénom doit être non vide]. C’est ce que récupèrera la méthode [Exception→getMessage] de la ligne 29 du script [classes-05.php].
Résultats :
1 2 3 | Exemple1, personne=[Paul,Langevin,48]
Exemple2, erreur : Argument 3 passed to Exemples\Personne::__construct() must be of the type integer, string given, called in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_18.php on line 19
Exemple3, erreur : Le prénom doit être non vide
|
Ajout d’une méthode faisant office de second constructeur¶
En PHP 7, il n’est pas possible d’avoir plusieurs constructeurs avec des paramètres différents qui permettraient de construire un objet de diverses façons. On peut alors utiliser des méthodes faisant office de constructeur :
1 2 3 4 5 | // méthode
public function initWithPersonne(Personne $p) {
// initialise l'objet courant avec une personne $p
$this->__construct($p->prenom, $p->nom, $p->age);
}
|
Commentaires
- lignes 2-5 : la méthode initWithPersonne permet d’affecter à l’objet courant les valeurs des attributs d’un autre objet Personne. Ici, elle fait appel au constructeur __construct mais ce n’est pas obligatoire. Elle pourrait initialiser elle-même les attributs de la classe [Personne] ;
La nouvelle classe [Personne] est utilisée par le script [classes-06.php] suivant :
1 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 | <?php
// inclusion définition de la classe Personne
require_once __DIR__."/Personne.php";
// déclaration du nom qualifié de la classe Personne
use \Exemples\Personne;
// test
// création d'un objet Personne
try {
$p = new Personne("Paul", "Langevin", 48);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// identité de cette personne
print "personne=$p\n";
// création d'une seconde personne
try {
$p2 = new Personne("Laure", "Adeline", 67);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// initialisation de la première avec les valeurs de la seconde
try {
$p->initWithPersonne($p2);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// vérification
print "personne=$p\n";
// fin
exit;
|
- lignes 14, 23, 30 : il est fréquent qu’après une exception, on doive arrêter l’exécution d’un script console si l’erreur rencontrée est irrécupérable. Ce n’est pas le cas dans un script web : on n’arrête pas l’exécution du script mais on fait afficher une page d’erreur. Si on est dans une fonction, ce n’est pas l’instruction exit qui sera utilisée mais return : on n’arrête pas l’exécution du script (exit) mais on sort de la fonction (return) après avoir positionné une erreur ;
Résultats :
1 2 | personne=[Paul,Langevin,48]
personne=[Laure,Adeline,67]
|
Un tableau d’objets [Personne]¶
L’exemple suivant [classes-07.php] montre qu’on peut avoir des tableaux d’objets.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
// test
// création d'un tableau d'objets Personne
// pour faciliter la compréhension du code, on ne gère pas l'éventuelle exception
$groupe = [new Personne("Paul", "Langevin", 48), new Personne("Sylvie", "Lefur", 70)];
// identité de ces personnes
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// fin
exit;
|
Résultats :
1 2 | groupe[0]=[Paul,Langevin,48]
groupe[1]=[Sylvie,Lefur,70]
|
Commentaires
- ligne 9 : création d’un tableau de 2 personnes ;
- ligne 12 : parcours du tableau ;
- ligne 13 : $groupe***[$i]* est un objet de type Personne. La méthode [Personne.__toString] est utilisée pour l’afficher ;
Création d’une classe dérivée de la classe Personne¶
On crée dans un fichier [Enseignant.php] la classe [Enseignant] suivante :
1 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 | <?php
// respect strict des types déclarés des paramètres de fonctions
declare (strict_types=1);
// espace de noms
namespace Exemples;
// une classe dérivée de personne
class Enseignant extends Personne {
// attributs
private $discipline; // discipline enseignée
// getter et setter
public function getDiscipline(): string {
return $this->discipline;
}
public function setDiscipline(string $discipline): void {
$this->discipline = $discipline;
}
// constructeur
public function __construct(string $prénom, string $nom, int $âge, string $discipline) {
// attributs parent
parent::__construct($prénom, $nom, $âge);
// autres attributs
$this->setDiscipline($discipline);
}
// surcharge de la fonction __toString de la classe parente
public function __toString(): string {
return "[" . parent::__toString() . ",$this->discipline]";
}
}
|
Commentaires
- ligne 7 : la classe [Enseignant] fait partie elle aussi de l’espace de noms [Exemples] ;
- ligne 10 : la classe Enseignant dérive (extends) de la classe Personne. La classe dérivée Enseignant hérite des attributs et des méthodes de sa classe mère ;
- ligne 12 : la classe Enseignant ajoute un nouvel attribut discipline qui lui est propre ;
- ligne 25 : le constructeur de la classe Enseignant reçoit 4 paramètres :
- 3 pour initialiser sa classe parent (prénom, nom, âge), ligne 27 ;
- 1 pour sa propre initialisation (discipline), ligne 29 ;
- ligne 27 : la classe dérivée a accès aux méthodes et constructeurs de sa classe parent via le mot clé parent ::. Ici on passe les paramètres (prénom, nom, âge) au constructeur de la classe parent ;
- lignes 33-35 : la méthode __toString de la classe dérivée utilise la méthode __toString de la classe parent ;
La classe [Enseignant] est utilisée par le script [classes-08.php] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php
// inclusion de la définition des deux classes
require_once __DIR__."/Personne.php";
require_once __DIR__."/Enseignant.php";
// déclaration des deux classes utilisées
use \Exemples\Personne;
use \Exemples\Enseignant;
// test
// création d'un tableau d'objets Personne et dérivés
// pour la simplicité de l'exemple, on ne gère pas les exceptions
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70));
// identité de ces personnes
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// fin
exit;
|
Commentaires
- lignes 4-5 : il nous faut dire à l’interpréteur PHP où se trouvent les deux classes [Enseignant, Personne] ;
- lignes 7-8 : déclaration des noms complets des deux classes. Ceci nous permettra de les désigner dans le code simplement par leur nom sans le suffixe de leurs espaces de noms ;
- ligne 13 : on crée un tableau comportant un type [Personne] et un type [Enseignant] ;
- lignes 16-18 : affichent les éléments du tableau ;
- ligne 17 : la méthode __toString de chaque élément $groupe***[$i]* va être appelée. La classe Personne a une méthode __toString. La classe Enseignant en a deux : celle de sa classe parent et la sienne propre. On peut se demander laquelle va être appelée. L’exécution montre que c’est celle de la classe Enseignant qui a été appelée. C’est toujours ainsi : lorsqu’une méthode est appelée sur un objet, celle-ci est cherchée dans l’ordre suivant : dans l’objet lui-même, dans sa classe parent s’il en a une, puis dans la classe parent de la classe parent, etc … La recherche s’arrête dès que la méthode a été trouvée.
Résultats :
1 2 | groupe[0]=[[Paul,Langevin,48],anglais]
groupe[1]=[Sylvie,Lefur,70]
|
Création d’une seconde classe dérivée de la classe Personne¶
L’exemple suivant crée une classe Etudiant dérivée de la classe Personne, dans le fichier [Etudiant.php] :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Exemples;
class Etudiant extends Personne {
// attributs
private $formation; // formation suivie
// getter et setter
public function getFormation(): string {
return $this->formation;
}
public function setFormation(string $formation): void {
$this->formation = $formation;
}
// constructeur
public function __construct(string $prénom, string $nom, int $âge, string $formation) {
// attributs parent
parent::__construct($prénom, $nom, $âge);
// autres attributs
$this->setFormation($formation);
}
// surcharge de la fonction __toString de la classe parente
public function __toString(): string {
return "[" . parent::__toString() . ",$this->formation]]";
}
}
|
Cette classe est utilisée par le script [classes-09.php] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php
// inclusion et définition des classes utilisées par le script
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
require_once __DIR__."/Enseignant.php";
use \Exemples\Enseignant;
require_once __DIR__."/Etudiant.php";
use \Exemples\Etudiant;
// test
// création d'un tableau d'objets personne et dérivés
// pour faciliter la compréhension de l'exemple, on ne gère pas les exceptions
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70), new Etudiant("Steve", "Boer", 23, "iup2 qualité"));
// identité de ces personnes
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// fin
exit;
|
Résultats :
1 2 3 | groupe[0]=[[Paul,Langevin,48],anglais]
groupe[1]=[Sylvie,Lefur,70]
groupe[2]=[[Steve,Boer,23],iup2 qualité]
|
Relation du constructeur d’une classe dérivée avec celui de la classe parent¶
Dans certains langages orientés objet, le constructeur d’une classe dérivée appelle automatiquement celui de sa classe parent. Le code suivant [classes-16.php] montre qu’avec PHP 7 ce n’est pas le cas :
1 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 | <?php
class Classe1 {
// constructeur
public function __construct() {
print "constructeur de la classe Classe1\n";
}
}
class Classe2 extends Classe1 {
// constructeur
public function __construct() {
// le constructeur de la classe parent n'est pas appelé implicitement
print "constructeur de la classe Classe2\n";
}
}
class Classe3 extends Classe1 {
// constructeur
public function __construct() {
// appel explicite du constructeur de la classe parent
parent::__construct();
// code propre à Classe3
print "constructeur de la classe Classe3\n";
}
}
// tests
print "test1---------\n";
new Classe2();
print "test2---------\n";
new Classe3();
|
Résultats
1 2 3 4 5 | test1---------
constructeur de la classe Classe2
test2---------
constructeur de la classe Classe1
constructeur de la classe Classe3
|
Redéfinition d’une méthode de la classe parent¶
Nous avons déjà vu qu’une méthode de la classe parent pouvait être redéfinie dans une classe fille. Ainsi la méthode [__toString] de la classe [Personne] (cf lien) a été redéfinie dans les classes filles [Enseignant] (cf. lien) et [Etudiant] (cf. lien). Le script [classes-13.php] illustre de nouveau le concept :
1 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 | <?php
// respect strict du type des paramètres des fonctions
declare(strict_types=1);
// classe principale
class Classe1 {
public function f(): int {
return 1;
}
function g(): int {
return 2;
}
}
// classe dérivée
class Classe2 extends Classe1 {
// on redéfinit la fonction f de la classe parent
public function f(): int {
return parent::f() + 10;
}
}
// code
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c1=new Classe1();
print $c1->f()."\n";
|
Commentaires
- lignes 7-17 : la classe [Classe1] définit deux méthodes f et g ;
- lignes 20-27 : la classe [Classe2] étend la classe [Classe1] et redéfinit la méthode f de celle-ci ;
Résultats
1 2 3 | 11
2
1
|
Commentaires
- la ligne 30 du code crée un objet $c2 de type [Classe2] ;
- la ligne 31 du code appelle la méthode f de l’objet $c2. Comme celle-ci existe, elle est exécutée ;
- la ligne 32 du code appelle la méthode g de l’objet $c2. Comme celle-ci n’existe pas, elle est recherchée dans la classe parent où elle est trouvée et exécutée ;
- la ligne 33 du code crée un objet $c1 de type [Classe1] ;
- la ligne 34 du code appelle la méthode f de l’objet $c1. Comme celle-ci existe, elle est exécutée ;
Passage d’un objet en paramètre d’une fonction¶
Considérons le script [classes-14.php] suivant :
1 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 | <?php
// respect strict du type des paramètres des fonctions
declare(strict_types=1);
// classe principale
class Classe1 {
public function f(): int {
return 1;
}
function g(): int {
return 2;
}
}
// classe dérivée
class Classe2 extends Classe1 {
// on redéfinit la fonction f de la classe parent
public function f(): int {
return parent::f() + 10;
}
}
// le paramètre de la fonction est de type Classe1 ou dérivé
function doSomething(Classe1 $c1): void {
print $c1->f() + $c1->g() . "\n";
}
// code
// on crée un objet de type Classe2 dérivé de Classe1
$c2 = new Classe2();
// on appelle doSomething avec
doSomething($c2);
|
Commentaires
- lignes 7-17 : la classe [Classe1] ;
- ligne 20-27 : une classe [Classe2] dérivée de [Classe1] ;
- ligne 30 : une fonction qui attend un paramètre de type [Classe1]. Lorsque le type attendu est une classe alors le paramètre effectif peut être un objet du type attendu **ou dérivé **;
- lignes 35-38 : on appelle la fonction [doSomething] avec un paramètre de type [Classe2] alors que c’est le type [Classe1] qui est attendu ;
Résultats
1 | 13
|
Classes abstraites¶
Une classe abstraite est une classe incomplète qui ne peut être instanciée. Elle doit obligatoirement être dérivée pour être utilisable.
A quoi sert une classe abstraite ? On a parfois des classes qui partagent une ou des méthodes mais qui se différencient par d’autres méthodes ou d’autres attributs. Il est alors souhaitable de rassembler tout ce qui est commun dans une classe parent. Pour l’instant on n’a pas besoin de classe abstraite. Mais supposons que les classes filles ne diffèrent que par une méthode M : la signature de la méthode serait la même dans toutes les classes filles mais son implémentation diffèrerait. Pour imposer aux classes filles d’implémenter la méthode M :
- on va déclarer la signature de la méthode M dans la classe parent. Comme celle-ci ne sait pas comment l’implémenter, on préfixe la méthode par le mot clé abstract : cela signifie que l’implémentation de la méthode M est reportée sur les classes filles ;
- parce que la classe parent n’est pas totalement implémentée, elle est elle-aussi déclarée abstraite avec le même mot clé abstract. Ceci fait que la classe ne peut plus être instanciée. Il faut obligatoirement créer une classe fille qui définira l’implémentation de la méthode M, pour que le corps de la classe parent soit utilisable ;
Voici un exemple [classes-15.php] :
1 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 | <?php
// respect strict du type des paramètres des fonctions
declare(strict_types=1);
// classe principale abstraite
abstract Class Classe1 {
// méthode connue de toutes les classes dérivées
public function f(): int {
return 1;
}
// méthode g abstraite - sera définie par les classes dérivées
abstract function g(): int;
}
// classe dérivée
Class Classe2 extends Classe1 {
// la méthode g de la classe parent doit être définie
public function g(): int {
return parent::f() + 10;
}
}
// classe dérivée
Class Classe3 extends Classe1 {
// la méthode g de la classe parent doit être définie
public function g(): int {
return parent::f() + 20;
}
// on peut redéfinir la méthode f de la classe parent
public function f(): int {
return 2;
}
}
// code
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c3 = new Classe3();
print $c3->f() . "\n";
print $c3->g() . "\n";
|
Commentaires
- lignes 7-16 : la classe [Classe1] est abstraite (ligne 7) car elle ne sait pas implémenter la méthode g de la ligne 15. Elle doit donc être obligatoirement dérivée pour être utilisable ;
- lignes 19-26 : la classe [Classe2] étend la classe [Classe1] et redéfinit la méthode g de sa classe parent (lignes 22-24) ;
- lignes 29-41 : la classe [Classe3] étend la classe [Classe1] et redéfinit la méthode g de sa classe parent (lignes 32-34) ;
- lignes 37-39 : la classe [Classe3] redéfinit la méthode f de sa classe parent ;
- lignes 44-49 : on crée deux objets de type [Classe2] et [Classe3] et on appelle leurs méthodes f et g ;
Résultats
1 2 3 4 | 1
11
2
21
|
Classes finales¶
Une classe finale est une classe qu’on ne peut dériver. Considérons le script [classes-11.php] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php
// espace de noms
namespace Exemples;
// classe non dérivable
final Class Classe1 {
}
// classe dérivée
Class Classe2 extends Classe1 {
}
// code - doit provoquer une erreur
new Classe2();
|
Commentaires
- lignes 7-9 : le mot clé final, fait de la classe [Classe1] une classe finale qu’on ne peut dériver ;
- lignes 12-14 : la classe [Classe2] étend la classe finale [Classe1], ce qui est une erreur ;
- ligne 17 : l’erreur ne sera signalée qu’à l’exécution du script lorsqu’on essaiera de manipuler un objet de type [Classe2] ;
Résultats
1 | Fatal error: Class Exemples\Classe2 may not inherit from final class (Exemples\Classe1) in C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes\classes-11.php on line 14
|
Méthodes finales¶
Une méthode finale est une méthode qu’on ne peut redéfinir par dérivation. Voici un exemple [classes-12.php] :
1 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 | <?php
// respect strict du type des paramètres des fonctions
declare(strict_types=1);
// espace de noms
namespace Exemples;
// classe principale
Class Classe1 {
// cette méthode ne peut être redéfinie dans une classe dérivée
public final function f(): int {
return 1;
}
}
// classe dérivée
Class Classe2 extends Classe1 {
public function f(): int {
return 2;
}
}
// code - doit provoquer une erreur
new Classe2();
|
Commentaires
- ligne 13 : la méthode f de la classe [Classe1] est déclarée finale par le mot clé final ;
- ligne 20 : la classe [Classe2] étend classe [Classe1] ;
- lignes 22-23 : on redéfinit la fonction f de la classe parent [Classe1]. Cela doit provoquer une erreur ;
- ligne 29 : on crée un objet de type [Classe2] pour forcer l’interpréteur PHP à inspecter la classe [Classe2] ;
Résultats
1 | Fatal error: Cannot override final method Exemples\Classe1::f() in C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes\classes-12.php on line 26
|
Méthodes et attributs statiques¶
Une méthode statique est une méthode liée à la classe dans laquelle elle est définie et non aux objets instances de la classe. Ainsi si la classe C déclare une méthode statique M, pour utiliser cette dernière on écrira :
- C::M si on est à l’extérieur de la classe ;
- self::M si on est dans la classe ;
Voici un exemple [classes-17.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php
class Classe1 {
// méthode statique
static function say(string $message): void {
print "$message\n";
}
}
// test -------------------
Classe1::say("hello");
|
Commentaires
- ligne 6 : la méthode [say] est déclarée statique avec le mot clé static ;
- ligne 13 : appel de la méthode statique [say] avec la notation : Classe1::say ;
Résultats
1 | hello
|
Considérons maintenant le code suivant [classes-18.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php
class Classe1 {
// attribut statique
private static $nbObjects = 0;
public function __construct() {
print "constructeur Classe1\n";
self::$nbObjects++;
}
// méthode statique
static function say(): void {
print self::$nbObjects ." objets de type [Classe1] ont été construits\n";
}
}
// test -------------------
new Classe1();
new Classe1();
Classe1::say();
|
Commentaires
- ligne 5 : on déclare un attribut statique qui va compter le nombre d’instances de la classe [Classe1] créées. Ce n’est pas un attribut qui peut appartenir à une instance de la classe. En effet, si deux objets O1 et O2 sont créés, aucun des deux n’a connaissance de l’autre. Avoir un compteur dans l’instance n’a pas de sens : lorsqu’un nouvel objet est créé, dans quelle instance va-t-on incrémenter un compteur ? On serait obliger d’incrémenter le compteur d’un objet particulier délaissant les compteurs des autres instances. Un attribut statique est un attribut de classe et non d’instance de la classe ;
- lignes 7-10 : c’est dans le constructeur qu’on va compter les objets créés puisque la création de chaque nouvel objet provoque l’exécution du constructeur ;
- ligne 14 : on notera la notation self::$nbObjects pour indiquer qu’on fait référence à un attribut statique de la classe dans laquelle se trouve le code exécuté ;
- lignes 13-15 : la méthode statique [say] a pour rôle d’afficher le nombre d’objets créés ;
- lignes 20-22 : on crée deux objets et on fait afficher le compteur d’objets ;
Résultats
1 2 3 | constructeur Classe1
constructeur Classe1
2 objets de type [Classe1] ont été construits
|
Visibilité entre classe Parent et classe Fille¶
Examinons le script [classes-19.php] suivant :
1 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 | <?php
class SomeParent {
// attribut
private $attributeOfParent = 4;
// méthode
public function doTest(): void {
// qui appelle ?
print "parent :\n";
var_dump($this);
// affichage parent
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// attribut
private $attributeOfChild = 14;
// méthode
public function doTest(): void {
// affichage enfant
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// parent
parent::doTest();
}
}
// script principal
print "---test1\n";
(new SomeParent())->doTest();
print "---test2\n";
(new SomeChild())->doTest();
|
Commentaires
- lignes 3-17 : la classe [SomeParent] ;
- lignes 19-32 : la classe fille [SomeChild]. On voit qu’elle étend la classe [SomeParent] (ligne 19) ;
- ligne 5 : la classe [SomeParent] n’a qu’un attribut ;
- lignes 8-17 : la méthode [SomeParent::doTest] a pour but
d’afficher deux attributs :
- [$attributeOfParent] qui appartient à la classe [SomeParent] ;
- [$attributeOfChild] qui appartient à la classe [SomeChild] (ligne 21) ;
- lignes 10-11 : on affiche l’identité de l’appelant : on va en effet
appeler la méthode de deux façons différentes :
- à partir de la classe parent [SomeParent] ;
- à partir de la classe fille [SomeChild] ;
- lignes 13-14 : affichage des deux attributs ;
- lignes 19-32 : la classe fille [SomeChild] qui étend la classe [SomeParent] (ligne 19) ;
- ligne 21 : la classe [SomeChild] n’a qu’un attribut ;
- lignes 24 : la méthode [SomeChild::doTest] a pour but d’afficher
deux attributs :
- [$attributeOfParent] qui appartient à la classe [SomeParent] ;
- [$attributeOfChild] qui appartient à la classe [SomeChild] ;
- lignes 26-27 : affichage des deux attributs ;
- ligne 30 : appel de la méthode [doTest] de la classe parent qui va à son tour afficher les deux attributs ;
- ligne 36 : la méthode [SomeParent::doTest] est appelée ;
- ligne 38 : la méthode [SomeChild::doTest] est appelée ;
Dans le 1er test, la visibilité des deux attributs est [private]. On peut donc s’attendre à ce que la classe fille ne voit pas l’attribut de sa classe parent. Il faudrait que celui-ci ait au moins la visibilité [protected]. Mais qu’en est-il de l’attribut de la classe fille ? Est-il visible dans la classe parent ?
Voici les résultats de ce 1er test :
1 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 | ---------------------------test1
parent :
object(SomeParent)#1 (1) {
["attributeOfParent":"SomeParent":private]=>
int(4)
}
parent : attributeOfParent=4
Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 14
parent : attributeOfChild=
---------------------------test2
Notice: Undefined property: SomeChild::$attributeOfParent in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 26
child : attributeOfParent=
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
["attributeOfChild":"SomeChild":private]=>
int(14)
["attributeOfParent":"SomeParent":private]=>
int(4)
}
parent : attributeOfParent=4
Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php:14
Stack trace:
#0 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php(30): SomeParent->doTest()
#1 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php(39): SomeChild->doTest()
#2 {main}
thrown in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 14
|
Commentaires
- lignes 1-10 : résultats du 1er test où la méthode [SomeParent::doTest] est appelée ;
- lignes 3-6 : on voit que l’objet qui appelle la méthode est de type [SomeParent] ;
- ligne 7 : affichage de l’attribut [$attributeOfParent] ;
- lignes 9-10 : on voit que l’attribut [SomeParent::$attributeOfChild] n’existe pas. Il n’est donc pas affiché ;
- lignes 11-30 : résultats du 2e test où la méthode [SomeChild::doTest] est appelée ;
- lignes 13-14 : on voit que l’attribut [SomeChild::$attributeOfParent] n’existe pas. Il n’est donc pas affiché. C’est normal : l’attribut [SomeParent::$attributeOfParent] est [private] et donc pas connu dans la classe fille ;
- ligne 15 : affichage de l’attribut [$attributeOfChild] ;
- lignes 16-30 : on est dans la méthode [SomeParent::doTest] appelée par la classe fille ;
- lignes 17-22 : on voit que [$this] est de type [SomeChild] avec deux attributs privés ;
- ligne 23 : de façon très étonnante, [$this] de type [SomeChild] voit ici l’attribut du parent [$attributeOfParent] ;
- lignes 25-30 : de façon tout aussi étonnante, [$this] de type [SomeChild] ne voit pas son attribut [$attributeOfChild] ;
Ce résultat est très étonnant : bien que les lignes 17-21 indiquent que [$this] est de type [SomeChild], ce [$this] à l’intérieur de la méthode [SomeParent::doTest] se comporte comme s’il était une instance de la classe [SomeParent] et non de la classe [SomeChild].
Faisons un nouveau test avec le script [classes-20.php]. L’attribut [$attributeOfParent] a maintenant une visibilité [protected] (ligne 5) :
1 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 | <?php
class SomeParent {
// attribut
protected $attributeOfParent = 4;
// méthode
public function doTest(): void {
// qui appelle ?
print "parent :\n";
var_dump($this);
// affichage parent
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// attribut
private $attributeOfChild = 14;
// méthode
public function doTest(): void {
// affichage enfant
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// parent
parent::doTest();
}
}
// script principal
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();
|
Résultats
1 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 | ---------------------------test1
parent :
object(SomeParent)#1 (1) {
["attributeOfParent":protected]=>
int(4)
}
parent : attributeOfParent=4
Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php on line 14
parent : attributeOfChild=
---------------------------test2
child : attributeOfParent=4
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
["attributeOfChild":"SomeChild":private]=>
int(14)
["attributeOfParent":protected]=>
int(4)
}
parent : attributeOfParent=4
Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php:14
Stack trace:
#0 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php(30): SomeParent->doTest()
#1 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php(39): SomeChild->doTest()
#2 {main}
thrown in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php on line 14
|
Commentaires
- ligne 12 : la classe [SomeChild] voit désormais l’attribut de son parent [$attributeOfParent]. C’est normal puisque celui-ci a maintenant une portée [protected] ;
- dans la méthode [someParent::doTest], l’objet [$this] est de type [SomeChild] (lignes 15-20). Il voit l’attribut de son parent [$attributeOfparent] (ligne 21) mais toujours pas son propre attribut [$attributeOfChild] (lignes 23-28) ;
Dans le 3e essai, l’attribut [$attributeOfChild] a lui également une portée [protected] :
1 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 | <?php
class SomeParent {
// attribut
protected $attributeOfParent = 4;
// méthode
public function doTest(): void {
// qui appelle ?
print "parent :\n";
var_dump($this);
// affichage parent
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// attribut
protected $attributeOfChild = 14;
// méthode
public function doTest(): void {
// affichage enfant
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// parent
parent::doTest();
}
}
// script principal
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();
|
Les résultats de l’exécution sont les suivants :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | ---------------------------test1
parent :
object(SomeParent)#1 (1) {
["attributeOfParent":protected]=>
int(4)
}
parent : attributeOfParent=4
Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-21.php on line 14
parent : attributeOfChild=
---------------------------test2
child : attributeOfParent=4
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
["attributeOfChild":protected]=>
int(14)
["attributeOfParent":protected]=>
int(4)
}
parent : attributeOfParent=4
parent : attributeOfChild=14
|
- ligne 22 : cette fois-ci, à l’intérieur du parent, [$this] de type [SomeChild] (lignes 15-20) voit l’attribut protégé [$attributeOfChild] de sa propre classe [SomeChild].
Que retenir de ces tests ?
- que le [$this] instance d’une classe parent, utilisée dans une
méthode de la classe parent voit :
- les attributs et méthodes de la classe parent quelque soit leur visibilité ;
- ne voit rien des attributs et méthodes de ses classes filles ;
C’est le comportement attendu.
- que le [$this] instance d’une classe fille, utilisée dans une
méthode de la classe fille voit :
- les attributs et méthodes de la classe parent s’ils ont au moins la visibilité [protected]. Ceux qui ont la visibilité [private] ne sont pas vus ;
- les attributs et méthodes de la classe fille quelque soit leur visibilité ;
C’est le comportement attendu.
- que le [$this] instance d’une classe fille, utilisée dans une
méthode de la classe parent voit :
- les attributs et méthodes de la classe parent quelque soit leur visibilité ;
- les attributs et méthodes de sa propre classe uniquement s’ils ont au moins la visibilité [protected]. Ceux qui ont la visibilité [private] ne sont pas vus ;
C’est un comportement inattendu.
Encodage jSON d’une classe¶
Dans une classe la méthode [__toString] est souvent présente : elle est censée rendre une chaîne de caractères représentant l’objet qui l’appelle. Il peut être tentant que cette chaîne soit une chaîne jSON. Nous explorons cette voie maintenant.
Nous utiliserons la classe [Personne] suivante :
1 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 | <?php
class Personne {
// attributs
private $nom;
private $prénom;
private $âge;
private $enfants;
// setter global
public function setFromArray(array $arrayOfAttributes): Personne {
// initialisation de certains attributs de la classe
foreach ($arrayOfAttributes as $attribute => $value) {
$this->$attribute = $value;
}
// on retourne l'objet
return $this;
}
// getters
public function getNom() {
return $this->nom;
}
public function getPrénom() {
return $this->prénom;
}
public function getÂge() {
return $this->âge;
}
public function getEnfants() {
return $this->enfants;
}
// __toString
public function __toString(): string {
// on identifie l'objet
var_dump($this);
// on récupère ses attributs
$attributes = \get_object_vars($this);
var_dump($attributes);
// on rend la chaîne jSON des attributs
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
}
|
Commentaires
- lignes 5-8 : les quatre attributs de la classe ;
- lignes 20-35 : les getters qui permettent d’avoir la valeur de ces attributs ;
- lignes 11-18 : un setter global qui permet d’initialiser les attributs à partir d’un tableau associatif [$arrayOfAttributes] ayant des clés identiques aux attributs de la classe ;
- lignes 38-46 : la méthode [__toString] de la classe ;
- ligne 42 : la fonction PHP [get_object_vars] permet d’obtenir la valeur des attributs de la classe sous la forme d’un tableau associatif [‘nom’=>’nom1’, ‘prénom’=>’prénom1, ‘âge’=>âge1, ‘enfants’=>[]] ;
- ligne 45 : on rend la chaîne jSON de ce tableau d’attributs ;
Examinons le script [json-01.php] qui exploite la classe [Personne] :
1 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 | <?php
// classe Personne
require "Personne.php";
// instanciation du père
$père = new Personne();
// initialisation du père
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// instanciation et initialisation enfant1
$enfant1 = (new Personne())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 17
]);
// instanciation et initialisation enfant2
$enfant2 = (new Personne())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// initialisation enfants du père
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// affichage éléments du père
$enfant1=($père->getEnfants())[0];
$enfant2=($père->getEnfants())[1];
print "------------------------enfant1\n";
print "enfant1=$enfant1\n";
print "------------------------enfant2\n";
print "enfant2=$enfant2\n";
print "------------------------père\n";
print "père=$père\n";
|
Commentaires
- lignes 6-13 : on initialise un objet [Personne] [$père] avec la méthode [Personne::setFromArray] qui permet d’initialiser un objet [Personne] avec un tableau ayant des clés identiques aux attributs de la classe [Personne] ;
- lignes 14-19 : on initialise un objet [Personne] [$enfant1] de la même façon ;
- lignes 21-25 : on initialise un objet [Personne] [$enfant2] ;
- lignes 27-29 : on initialise l’attribut [$père→enfants] avec un tableau des deux enfants ;
- lignes 32-33 : on affecte les deux enfants au père ;
- ligne 35 : l’opération [print] va chercher à transformer l’objet [$enfant1] en chaîne de caractères. Pour cela, elle utilise la méthode [__toString] de l’objet. On espère donc voir la chaîne jSON de l’objet ;
- lignes 38-39 : on fait de même avec le père ;
Les résultats sont les suivants :
1 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 | ------------------------enfant1
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(7) "Sylvain"
["âge"]=>
int(17)
["enfants"]=>
NULL
}
enfant1={"nom":"Bertholomé","prénom":"Sylvain","âge":17,"enfants":null}
------------------------enfant2
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(10) "Géraldine"
["âge"]=>
int(12)
["enfants"]=>
NULL
}
enfant2={"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}
------------------------père
object(Personne)#1 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Dieudonné"
["âge":"Personne":private]=>
int(58)
["enfants":"Personne":private]=>
array(2) {
[0]=>
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
[1]=>
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
}
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(10) "Dieudonné"
["âge"]=>
int(58)
["enfants"]=>
array(2) {
[0]=>
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
[1]=>
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
}
}
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{},{}]}
|
Commentaires
- lignes 2-11 : l’objet [$enfant1] ;
- lignes 12-21 : le tableau des attributs de l’objet [$enfant1]. On les a bien tous ;
- ligne 22 : on a bien la chaîne jSON de l’objet [$enfant1] ;
- lignes 23-44 : idem pour l’objet [$enfant2] ;
- lignes 45-112 : pour le père c’est un peu différent puisque son attribut [enfants] n’est pas NULL comme il l’était chez les enfants ;
- ligne 112 : on voit que dans la chaîne jSON du père, il manque les enfants ;
- lignes 79-111 : on voit que dans le tableau d’attributs du père, l’enfant 1 (lignes 89-98) est resté un objet, de même pour l’enfant 2 (lignes 99-110). En clair, l’expression [get_object_vars($this)] où [$this] représente le père n’est pas récursive : si un attribut de la classe [Personne] est lui-même un objet, l’expression [get_object_vars($this)] n’essaie pas d’obtenir son tableau d’attributs ;
On peut améliorer les choses. On modifie la classe [Personne] en la classe [Personne2] suivante :
1 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 | <?php
class Personne2 {
// attributs
private $nom;
private $prénom;
private $âge;
private $enfants;
// setter global
public function setFromArray(array $arrayOfAttributes): Personne2 {
…
// on retourne l'objet
return $this;
}
// getters
public function getNom() {
return $this->nom;
}
…
// __toString
public function __toString(): string {
// on récupère les attributs de l'objet
$attributes = $this->getAttributes($this);
$enfants = $attributes["enfants"];
if ($enfants != NULL) {
$attributes["enfants"] = [$enfants[0]->getAttributes(), $enfants[1]->getAttributes()];
}
// on rend la chaîne JSON des attributs
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
public function getAttributes(): array {
return \get_object_vars($this);
}
}
|
Commentaires
- lignes 36-38 : la fonction [getAttributes] rend le tableau des attributs de l’objet qui l’appelle ;
- lignes 25-34 : la fonction [__toString] ;
- ligne 27 : on récupère les attributs de la classe [Personne] dans le tableau [$attributes] ;
- ligne 28 : on sait, d’après l’exemple précédent, que [$attributes[« enfants »]] est un tableau de deux objets de type [Personne] ;
- lignes 29-31 : les deux objets sont remplacés par leur tableau d’attributs ;
- ligne 33 : il ne reste plus qu’à encoder en jSON le tableau des attributs construit ;
Le script [json-02.php] exploite la classe [Personne2] de la façon suivante :
1 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 | <?php
// classe Personne2
require "Personne2.php";
// instanciation du père
$père = new Personne2();
// initialisation
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// instanciation et initialisation enfant1
$enfant1 = (new Personne2())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 17
]);
// instanciation et initialisation enfant2
$enfant2 = (new Personne2())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// initialisation enfants du père
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// affichage père
print "------------------------père\n";
print "père=$père\n";
|
Le script [json-02.php] est identique au script [json-01.php] si ce n’est que la classe [Personne2] a remplacé la classe [Personne].
Les résultats de l’exécution sont les suivants :
1 2 | ------------------------père
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{"nom":"Bertholomé","prénom":"Sylvain","âge":17,"enfants":null},{"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}]}
|
Cette fois, on a bien obtenu les enfants avec le père.
La solution précédente n’est pas satisfaisante car les enfants peuvent avoir eux-mêmes des enfants. On retrouve alors le problème précédent.
La classe [Personne3] résoud ce problème de la façon suivante :
1 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 | <?php
class Personne3 {
// attributs
private $nom;
private $prénom;
private $âge;
private $enfants;
// setter global
public function setFromArray(array $arrayOfAttributes): Personne3 {
…
}
// getters
…
// __toString
public function __toString(): string {
// on rend la chaîne JSON des attributs
$attributes = [];
$this->getRecursiveAttributes($attributes, $this, []);
// chaîne JSON des attributs
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
public function getAttributes(): array {
return \get_object_vars($this);
}
private function getRecursiveAttributes(array &$attributes, $value, $keys): void {
// analyse de la valeur [$value]
// $keys est un tableau [key1, key2, .., keyn]
// $value=$attributes[key1][key2]….[keyn]
// si [$value] est un objet on utilise sa méthode [getAttributes]
if (\is_object($value)) {
// attributs de l'objet [$value]
$objectAttributes = $value->getAttributes();
// que fait-on du résultat ?
if ($keys) {
// dans [$attributes], on va remplacer $value par le tableau de ses attributs
// il faut construire l'élément $attributes[key1][key2]…[keyn]
// où $keys est le tableau [key1, key2, .., keyn]
// on prend la référence du tableau [$attributes]
$attribute = &$attributes;
// on scanne le tableau des clés
foreach ($keys as $key) {
// on prend la référence de l'attribut
$attribute = &$attribute[$key];
}
// ici $attribut et $attributes[key1][key2]…[key(n)] sont identiques
// ils partagent le même emplacement mémoire
// l'objet [$value] est remplacé par son tableau d'attributs;
// il faut écrire $attributes[key1][key2]…[keyn]=$objectAttributes
// ce qui équivaut à $attribute = $objectAttributes
$attribute = $objectAttributes;
} else {
// pas de clés - on est au début de l'exploration de l'objet
// $objectAttributes représente les attributs de 1er niveau de la classe
$attributes += $objectAttributes;
}
// peut-être que dans [$objectAttributes] il y a encore des objets
// on explore les attributs de [$objectAttributes]
$this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
} else {
if (\is_array($value)) {
// on a un tableau - on analyse chacun de ses éléments
foreach ($value as $key => $élément) {
// on rajoute la clé courante au tableau $keys
\array_push($keys, $key);
// on analyse $élément
$this->getRecursiveAttributes($attributes, $élément, $keys);
// on enlève du tableau $keys la clé qui vient d'être analysée
\array_pop($keys);
}
}
}
}
|
Commentaires
- lignes 21-22 : cette fois-ci, la méthode [__toString] demande les attributs de sa classe et demande à ce que ce soit fait récursivement : si un attribut est un objet ou un tableau d’objets, alors chaque objet doit être remplacé par son tableau d’attributs dans le tableau d’attributs final de la classe ;
- lignes 31-78 : la fonction [getRecursiveAttributes] fait ce travail. On a commenté son code. L’écriture d’une fonction récursive est souvent quelque chose de complexe. C’est le cas ici. Le lecteur ne perdra rien s’il ne le comprend pas. Il existe des bibliothèques qui font ce travail. L’appel récursif a lieu aux lignes 64 et 72 ;
- l’intérêt de ce code est qu’il n’a pas été écrit pour la seule classe [Personne3]. Il est valable pour toute classe ayant des attributs ayant pour valeurs des types d’objets différents, tant que les classes utilisées par la classe principale, ont comme elle la méthode [getAttributes] des lignes 27-29
Le script [json-03.php] utilise la classe [Personne3] de la façon suivante :
1 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 | <?php
// classe Personne3
require "Personne3.php";
// instanciation du père
$père = new Personne3();
// initialisation
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// instanciation et initialisation enfant1
$enfant1 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 27
]);
// instanciation et initialisation enfant2
$enfant2 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// initialisation enfants du père
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// instanciation et initialisation enfant11
$enfant11 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Gaëtan",
"âge" => 2
]);
// instanciation et initialisation enfant12
$enfant12 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Mathilde",
"âge" => 1
]);
// initialisation enfants de enfant1
$enfant1->setFromArray([
"enfants" => [$enfant11, $enfant12]
]);
// affichage père
print "------------------------père\n";
print "père=$père\n";
|
- lignes 30-45 : on donne deux enfants à [$enfant1] ;
Les résultats de l’exécution sont les suivants :
1 2 | ------------------------père
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{"nom":"Bertholomé","prénom":"Sylvain","âge":27,"enfants":[{"nom":"Bertholomé","prénom":"Gaëtan","âge":2,"enfants":null},{"nom":"Bertholomé","prénom":"Mathilde","âge":1,"enfants":null}]},{"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}]}
|
Si on met en forme ce résultat, on obtient la chose suivante :
1 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 | père={
"nom": "Bertholomé",
"prénom": "Dieudonné",
"âge": 58,
"enfants": [
{
"nom": "Bertholomé",
"prénom": "Sylvain",
"âge": 27,
"enfants": [
{
"nom": "Bertholomé",
"prénom": "Gaëtan",
"âge": 2,
"enfants": null
},
{
"nom": "Bertholomé",
"prénom": "Mathilde",
"âge": 1,
"enfants": null
}
]
},
{
"nom": "Bertholomé",
"prénom": "Géraldine",
"âge": 12,
"enfants": null
}
]
}
|
On a bien récupéré les chaîne jSON de tous les objets [Personne] formant le père.
Les interfaces¶
Une interface est une structure définissant des prototypes de méthodes. Une classe implémentant une interface doit définir le code de toutes les méthodes de l’interface.
L’arborescence des scripts¶
Une première interface¶
Nous définissons une interface [IExemple] dans un script [IExemple.php]. Dans ce même script, nous définissons deux classes implémentant l’interface [IExemple] :
1 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 | <?php
// respect strict des types des paramètres des fonctions
declare(strict_types=1);
// espace de noms
namespace Exemples;
// une interface
interface IExemple {
// les méthodes de l'interface
public function ajouter(int $i, int $j): int;
public function soustraire(int $i, int $j): int;
}
// implémentation 1 de l'interface IExemple
class Classe1 implements IExemple {
public function ajouter(int $a, int $b): int {
return $a + $b + 10;
}
public function soustraire(int $a, int $b): int {
return $a - $b + 20;
}
}
// implémentation 2 de l'interface IExemple
class Classe2 implements IExemple {
public function ajouter(int $a, int $b) : int{
return $a + $b + 100;
}
public function soustraire(int $a, int $b) : int {
return $a - $b + 200;
}
}
|
Commentaires
- lignes 10-14 : l’interface IExemple définit deux méthodes : la fonction ajouter (ligne 12) et la fonction soustraire (ligne 13). L’interface ne définit pas le code de ces méthodes. Ce sont les classes implémentant l’interface qui vont le faire ;
- ligne 17 : la classe Classe1 implémente (implements) l’interface IExemple. Elle définit donc un code pour les méthodes ajouter (ligne 18) et soustraire (ligne 22) ;
- lignes 29-38 : idem pour la classe Classe2 ;
Nous utilisons l’interface [IExemple] dans le script [interfaces-01.php] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php
require_once "IExemple.php";
use \Exemples\Classe1;
use \Exemples\Classe2;
use \Exemples\IExemple;
// fonction
function calculer(int $a, int $b, IExemple $interface) : void {
print $interface->ajouter($a, $b)."\n";
print $interface->soustraire($a, $b)."\n";
}
// ------------------- main
// création de 2 objets de type Classe1 et Classe2
$c1 = new Classe1();
$c2 = new Classe2();
// appel de la fonction calculer
calculer(4, 3, $c1);
calculer(14, 13, $c2);
|
Commentaires
- lignes 3-6 : inclusion et définition des classes et interfaces utilisées par le script ;
- ligne 3 : au lieu de l’instruction include nous avons utilisé ici l’instruction require_once. L’instruction include inclut un fichier dans le script en cours même si celui-ci a déjà été inclus alors que l’instruction require_once ne l’inclut qu’une fois. Donc pour ce qui est de l’inclusion de code, on préfèrera l’instruction **require_once **;
- lignes 9-12 : on définit une fonction ;
- ligne 9 : nous avons déclaré ici le type des trois paramètres. Si à l’exécution, le paramètre effectif n’est pas du type attendu, une erreur est lancée. Le 3e paramètre est de type [IExemple]. Cela veut dire que le paramètre effectif doit être une instance de classe implémentant l’interface [IExemple], donc dans notre exemple, une instance des classes [Classe1] ou [Classe2] ;
- lignes 10-11 : nous utilisons les méthodes [ajouter, soustraire] de l’interface [IExemple] ;
- ligne 19 : le 3e paramètre effectif est de type [Classe1] compatible avec le type [IExemple] du 3e paramètre formel de la fonction ;
- ligne 20 : idem pour la classe [Classe2] ;
Résultats
1 2 3 4 | 17
21
127
201
|
Dérivation d’une interface¶
De la même façon qu’une classe fille peut étendre une classe parent, une interface fille peut étendre une interface parent.
Considérons l’interface [IExemple2] définie dans le fichier [IExemple2.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php
// respect strict des types des paramètres des fonctions
declare(strict_types=1);
// espace de noms
namespace Exemples;
// inclusion de la définition de l'interface IExemple
require_once __DIR__."/IExemple.php";
// une interface qui dérive de IExemple
interface IExemple2 extends IExemple {
// la méthode de l'interface
public function multiplier(int $i, int $j): int;
}
// implémentation de l'interface IExemple2
class Classe3 extends Classe2 implements IExemple2 {
public function multiplier(int $a, int $b): int {
return $a * $b;
}
}
|
Commentaires
- ligne 6 : l’interface [IExemple2] sera dans le même espace de noms que l’interface [Iexemple] ;
- ligne 9 : on inclut le fichier de définition de l’interface [Iexemple] ;
- lignes 12-15 : définissent l’interface [IExemple2] qui étend (hérite) l’interface [Iexemple] (extends) et lui ajoute une nouvelle méthode (ligne 14);
- lignes 18-24 : la classe [Classe3] étend la classe [Classe2]. Comme celle-ci implémente l’interface [IExemple], ce sera également le cas de la classe [Classe3]. En effet, [Classe3] hérite de toutes les méthodes publiques et protégées de la classe [Classe2], donc des méthodes ajouter et soustraire. Par ailleurs, on indique que [Classe3] implémente l’interface [IExemple2]. A ce titre, elle doit également implémenter la méthode multiplier, ce que ne fait pas [Classe2]. C’est ce que font les lignes 20-22;
L’interface [IExemple2] est exploitée par le script [interfaces-02] suivant:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php
// respect strict des types des paramètres des fonctions
declare(strict_types=1);
// inclusion et qualifications des classes et interfaces nécessaires au script
require_once __DIR__."/Iexemple2.php";
use \Exemples\IExemple2;
use \Exemples\Classe3;
// fonction
function calculer(int $a, int $b, IExemple2 $interface) {
print $interface->ajouter($a, $b) . "\n";
print $interface->soustraire($a, $b) . "\n";
print $interface->multiplier($a, $b) . "\n";
}
// ------------------- main
// création d'1 objet de type Classe3 qui implémente IExemple2
$c3 = new Classe3();
// appel de la fonction calculer
calculer(4, 3, $c3);
|
Commentaires
- ligne 12 : le 3e paramètre de la fonction [calculer] est de type [IExemple2] ;
- lignes 13-15 : on utilise les trois méthodes de l’interface [IExemple2] ;
- ligne 22 : on appelle la fonction [calculer] avec comme 3e paramètre un objet de type [IExemple2] (ligne 20) ;
Résultats
1 2 3 | 107
201
12
|
Passage d’une interface en paramètre d’une fonction¶
Nous avons vu au paragraphe que lorsque le type attendu pour un paramètre de fonction est une classe alors le paramètre effectif peut être un objet du type attendu ou dérivé. Il en est de même pour les interfaces comme le montre le script suivant [interfaces-03.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php
// respect strict des types des paramètres des fonctions
declare(strict_types=1);
// inclusion et qualifications des classes et interfaces nécessaires au script
require_once __DIR__."/IExemple.php";
use \Exemples\IExemple;
require_once __DIR__."/IExemple2.php";
use \Exemples\Classe3;
// fonction
function calculer(int $a, int $b, IExemple $interface) {
print $interface->ajouter($a, $b) . "\n";
print $interface->soustraire($a, $b) . "\n";
}
// ------------------- main
// création d'1 objet de type Classe3 qui implémente IExemple2 et donc IExemple
$c3 = new Classe3();
// appel de la fonction calculer
calculer(4, 3, $c3);
|
Commentaires
- ligne 13, la fonction [calculer] attend comme troisième paramètre un type [IExemple]. Cela signifie que le type du paramètre effectif pourra être de type [IExemple] ou dérivé ;
- ligne 20 : on instancie un objet $c3 de type [Classe3], classe qui implémente l’interface [IExemple2] qui elle-même étend l’interface [IExemple]. Donc finalement $c3 implémente l’interface [IExemple] ;
- ligne 22 : on appelle la fonction [calculer] avec comme troisième paramètre un objet $c3 de type [Classe3]. Par le jeu des héritages de classes et des implémentations, on a vu que l’objet $c3 implémentait le type [IExemple]. On peut donc l’utiliser comme troisième paramètre ;
Résultats
1 2 | 107
201
|
Les exceptions et erreurs¶
Lorsqu’une méthode d’une classe rencontre une erreur irrécupérable (fichier inexistant, base de données non connectée, connexion réseau hors service), elle n’affiche pas une erreur sur une console (fichier, base de données) mais lance une exception. Toutes les exceptions étendent la classe [Exception]. Outre des exceptions, le fonctionnement interne de PHP émet également des erreurs dont la classe de base est la classe [Error]. Les deux classes implémentent l’interface PHP [Throwable].
L’arborescence des scripts¶
L’interface [Throwable]¶
L’interface [Throwable] est la suivante :
Le rôle des méthodes de l’interface est le suivant :
Les exceptions prédéfinies dans PHP 7¶
PHP 7 définit plusieurs classes d’exceptions :
- en [1], les exceptions prédéfinies dans PHP ;
- en [2], les exceptions de la bibliothèque SPL (Standard PHP Library) de PHP 7. la bibliothèque SPL est une collection de classes et d’interfaces destinées à résoudre des problèmes rencontrés fréquemment par les développeurs.
Les erreurs prédéfinies dans PHP 7¶
PHP 7 définit plusieurs classes d’erreurs :
La classe [Error] est la classe parent de toutes les erreurs prédéfinies dans PHP. La classe [ErrorException] permet d’encapsuler une instance de la classe [Error] dans une instance de la classe [Exception]. Ceci permet d’uniformiser la gestion des erreurs en ne traitant que des exceptions.
Exemple 1¶
Le premier exemple [exceptions-01.php] montre à la fois des erreurs PHP et une exception :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php
// affichage de toutes les erreurs
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// code --------
$var=[];
// clé inconnue
print $var["abcd"];
// division par zéro
$var=7/0;
var_dump($var);
// tableau à bornes fixes
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// indice en-dehors des bornes
$array[5]=8;
|
Commentaires
- ligne 4 : on demande à PHP de signaler toutes les erreurs. Le second paramètre est le niveau d’erreurs demandé :
- ligne 5 : on demande d’afficher les erreurs sur la console ;
- ligne 9 : on accède à un élément inexistant du tableau [$var] ;
- ligne 11 : on fait une division par zéro ;
- ligne 14 : on crée une instance de la classe [SplFixedArray]. Cette classe permet de créer un tableau à bornes fixes et à indices entiers ;
- ligne 18 : on accède à un élément inexistant du tableau ;
Résultats
1 2 3 4 5 6 7 8 9 | Notice: Undefined index: abcd in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 9
Warning: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 11
float(INF)
Fatal error: Uncaught RuntimeException: Index invalid or out of range in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php:18
Stack trace:
#0 {main}
thrown in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 18
|
Commentaires
- ligne 1 des résultats : accéder à une clé inexistante d’un tableau provoque une erreur PHP de niveau [E_NOTICE]. Cela n’interrompt pas l’exécution du script ;
- ligne 3 des résultats : diviser un nombre par zéro provoque une erreur PHP de niveau [E_WARNING]. Cela n’interrompt pas l’exécution du script ;
- lignes 6-9 des résultats : accéder à un indice inexistant d’un tableau [SplFixedArray] provoque une exception de type [RuntimeException] et interrompt l’exécution du script ;
Gérer les exceptions¶
Le script [exceptions-02.php] montre comment gérer les exceptions :
1 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 | <?php
// on affiche toutes les erreurs
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// on entoure le code par un try / catch
try {
$var = [];
// clé inconnue
print $var["abcd"];
// division par zéro
$var = 7 / 0;
var_dump($var);
// tableau à bornes fixes
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// indice en-dehors des bornes
$array[5] = 8;
// vérification
print "ce message ne sera pas affiché\n";
} catch (\Throwable $ex) {
// \Throwable est l'interface implémentée par la plupart des erreurs et exceptions
// on affiche l'exception
print "erreur, message : " . $ex->getMessage() . ", type : " . get_class($ex) . "\n";
}
|
Commentaires
- le script est celui qui a été présenté au paragraphe précédent. Seulement maintenant on a entouré le code des lignes 8-19 susceptible de provoquer des erreurs par une structure try / catch : si le code des lignes 8-21 provoque (lance) une exception ou une erreur, celle-ci sera gérée par la clause catch des lignes 22-26 ;
- ligne 22 : le paramètre de la clause [catch] est le type d’exception ou d’erreur que l’on veut gérer. En mettant comme type [Throwable] qui est une interface, on indique qu’on veut gérer toute instance de classe implémentant l’interface [Throwable]. Comme toutes les classes d’erreurs et d’exceptions implémentent cette interface, la clause [catch] gère ici toute erreur / exception encapsulée dans une classe ;
- ligne 19 : l’instruction qui déclenche l’erreur et donne naissance à l’exception. Dès qu’il y a exception, il y a branchement sur la clause [catch]. Le code derrière la ligne 19 ne sera donc pas exécuté ;
Résultats
1 2 3 4 5 | Notice: Undefined index: abcd in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-02.php on line 10
Warning: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-02.php on line 12
float(INF)
erreur, message : Index invalid or out of range, type : RuntimeException
|
Commentaires des résultats
- lignes 1 et 3 : on retrouve les erreurs de niveau [E_NOTICE] et [E_WARNING]. Ces erreurs ne sont pas des exceptions et ne sont donc pas gérées par la clause [catch] ;
- ligne 5 : on a le message d’erreur écrit dans la clause [catch]. Il s’est donc produit une exception dérivée de [Exception] ou une erreur dérivée de [Error]. Nous voyons ici qu’il s’agit de la classe [RuntimeException] ;
Paramètres de la clause [catch]¶
Examinons le script [exceptions-03.php] suivant :
1 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 | <?php
// on affiche toutes les erreurs
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// un tableau à bornes fixes
$array = new \SplFixedArray(5);
try {
// indice en-dehors des bornes
$array[5] = 8;
} catch (\Throwable $ex) {
// affichage message d'erreur
print "Erreur 1 : " . $ex->getMessage() . "\n";
}
try {
// indice en-dehors des bornes
$array[5] = 8;
} catch (\Exception $ex) {
// affichage message d'erreur
print "Erreur 2 : " . $ex->getMessage() . "\n";
}
try {
// indice en-dehors des bornes
$array[5] = 8;
} catch (\RuntimeException $ex) {
// affichage message d'erreur
print "Erreur 3 : " . $ex->getMessage() . "\n";
}
try {
// division par 0
intdiv(5, 0);
} catch (\Throwable $ex) {
// affichage message d'erreur
print "Erreur 4 : " . $ex->getMessage() . "\n";
}
try {
// division par 0
intdiv(5, 0);
} catch (\DivisionByzeroError $ex) {
// affichage message d'erreur
print "Erreur 5 : " . $ex->getMessage() . "\n";
}
try {
// division par 0
intdiv(5, 0);
} catch (\Error $ex) {
// affichage message d'erreur
print "Erreur 6 : " . $ex->getMessage() . "\n";
}
try {
// division par 0
intdiv(5, 0);
} catch (\Exception $ex) {
// affichage message d'erreur
print "Erreur 6 : " . $ex->getMessage() . "\n";
}
|
Commentaires
- lignes 8-31 : 3 façons différentes de gérer l’exception générée par
l’utilisation d’un indice incorrect avec la classe
[SplFixedArray]. Nous avons vu que cette erreur générait une
exception de type [RuntimeException] ;
- ligne 12 : gère une erreur de type [Throwable]. C’est valide puisque le type [RuntimeException] dérive du type [Exception] qui implémente l’interface [Throwable] ;
- ligne 20 : gère une erreur de type [Exception]. C’est valide puisque le type [RuntimeException] dérive du type [Exception] ;
- ligne 28 : gère une erreur de type [RuntimeException]. C’est la méthode à privilégier puisque c’est le type exact de l’exception générée ;
- lignes 32-62 : 4 façons différentes de gérer l’exception générée par
la fonction [intdiv] lorsqu’on passe à celle-ci un diviseur égal
à 0. La fonction [ intdiv ( int $dividend , int $divisor ) : int]
fait la division entière $dividend / $divisor. Lorsque le diviseur
est nul, l’exception [DivisionByzeroError] est lancée ;
- ligne 35 : on intercepte toute erreur implémentant l’interface [Throwable]. C’est valide ;
- ligne 43 : on intercepte le type exact de l’erreur : c’est la méthode à privilégier ;
- ligne 51 : on intercepte le type [Error]. C’est valide puisque la classe [DivisionByzeroError] étend la classe [Error] ;
- ligne 59 : on intercepte le type [Exception]. C’est invalide car la classe [DivisionByzeroError] n’a aucun lien avec la classe [Exception] ;
Résultats
1 2 3 4 5 6 7 8 9 10 11 12 | Erreur 1 : Index invalid or out of range
Erreur 2 : Index invalid or out of range
Erreur 3 : Index invalid or out of range
Erreur 4 : Division by zero
Erreur 5 : Division by zero
Erreur 6 : Division by zero
Fatal error: Uncaught DivisionByZeroError: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-03.php:58
Stack trace:
#0 C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-03.php(58): intdiv(5, 0)
#1 {main}
thrown in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-03.php on line 58
|
Clause [finally]¶
La structure try / catch peut avoir un troisième élément et devenir une structure try / catch / finally. Le code de la clausse [finally] est exécuté dans les deux cas suivants :
- la clause [try] ne lance pas d’exception. Elle est alors exécutée entièrement puis l’exécution du code passe à la clause [finally] qui est exécutée entièrement ;
- la clause [try] lance une exception. Elle est alors exécutée jusqu’à l’instruction qui lance l’exception. L’exécution du code passe alors à la clause [catch] qui est exécutée entièrement. Puis l’exécution du code passe à la clause [finally] qui est exécutée entièrement ;
Finalement, le code de la clause [finally] est toujours exécuté. Ce scénario est utile dans le cas suivant :
- dans le [try], le code a obtenu des ressources (fichiers, bases de données, connexions réseau, files d’attente). En général ces ressources sont coûteuses en mémoire. Il faut alors les rendre (on dit le plus souvent fermer) dès que c’est possible ;
- si l’acquisition des ressources a été faite dans le [try], on mettra leur restitution dans le [finally]. Cela nous assure que dans tous les cas (erreur ou pas), les ressources acquises sont restituées au système ;
Le script suivant [exemples/exceptions/exceptions-04.php] nous montre le fonctionnement de la clause [finally] dans diverses situations :
1 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 | <?php
// ou crée une instance d'exception
$e = new \Exception("Erreur…");
var_dump($e);
// premier test
try {
print "Premier test\n";
throw $e;
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
// second test
try {
print "Second test\n";
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
// trosième test
try {
print "Troisième test\n";
return;
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
|
Commentaires du code
- ligne 4 : $e est une instance de la classe prédéfinie [Exception]. On va la lancer à différents endroits ;
- lignes 8-15 : l’exception $e est lancée dans le [try] (ligne 10) ;
- ligne 11 : l’exception [Exception] est interceptée et son message d’erreur écrit sur la console ;
- lignes 13-15 : la clause [finally] écrit un message. D’après ce qui a été dit précédemment, ce message devrait être tout le temps écrit, erreur ou pas dans le [try] ;
- lignes 18-24 : il n’y a pas d’erreur dans le [try]. On devrait là également passer dans le [finally] ;
- lignes 27-34 : il y a une instruction [return] dans le try et pas d’erreur. On peut se demander alors si on va passer dans la clause [finally]. L’exécution montre que oui ;
Résultats
1 2 3 4 5 6 7 | Premier test
Erreur…
Terminé
Second test
Terminé
Troisième test
Terminé
|
Examinons un autre cas [exceptions-05.php] :
1 2 3 4 5 6 7 8 9 | <?php
// quatrième test
try {
print "Quatrième test\n";
exit;
} finally {
print "Terminé\n";
}
|
Commentaires
- ligne 6 : l’instruction [exit] arrête immédiatement l’exécution du script : la clause [finally] n’est pas exécutée ;
- lignes 4-9 : un exemple de try / catch / finally sans clause [catch]. C’est possible ;
Résultats
1 | Quatrième test
|
Créer ses propres classes d’exceptions¶
Dans un projet un peu important, il est utile de différentier les différentes erreurs en les encapsulant dans différentes classes d’exceptions. Dans le script précédent, nous avons vu que toute exception pouvait être interceptée par une clause [catch (Throwable]. C’est recommandé si on n’a aucune idée de l’erreur interceptée et que le traitement est le même pour toutes les erreurs. C’est parfois le cas mais on a souvent besoin d’adapter le traitement au type exact de l’erreur. Il faut alors différentier les erreurs entre-elles.
Examinons le script [exceptions-06.php] suivant :
1 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 | <?php
// on définit notre propre famille d'exceptions
class Exception1 extends \RuntimeException {
}
class Exception2 extends \RuntimeException {
}
// ou utilise nos exceptions
$e1 = new Exception1("Erreur1…");
var_dump($e1);
$e2 = new Exception2("Erreur2…");
var_dump($e2);
// premier test
print ("premier test\n");
try {
// on lance un type Exception1
throw $e1;
} catch (Exception1 $ex1) {
print "Exception 1" . "\n";
print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
print "Exception 2" . "\n";
print $ex2->getMessage() . "\n";
}
// second test
print ("second test\n");
try {
// on lance un type Exception2
throw $e2;
} catch (Exception1 $ex1) {
print "Exception 1" . "\n";
print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
print "Exception 2" . "\n";
print $ex2->getMessage() . "\n";
}
// troisième test
print ("troisième test\n");
try {
// on lance un type Exception1
throw $e1;
} catch (Exception1 | Exception2 $ex) {
print "Exception 1 ou 2" . "\n";
print $ex->getMessage() . "\n";
}
// quatrième test
print ("quatrième test\n");
try {
// on lance un type Exception2
throw $e2;
} catch (Exception1 | Exception2 $ex) {
print "Exception 1 ou 2" . "\n";
print $ex->getMessage() . "\n";
}
|
Commentaires
- lignes 4-10 : on définit deux classes [Exception1] et [Exception2] toutes deux dérivées de la classe prédéfinie [RuntimeException]. Le corps de ces classes est vide. Autrement dit, on ne les utilise que pour leurs types : c’est parce qu’elles ont des types différents qu’on va pouvoir différentier ces deux exceptions dans les clauses [catch] ;
- lignes 13-16 : on définit deux variables $e1 et $e2 ayant respectivement les types [Exception1] et [Exception2] ;
- lignes 20-29 : on a une structure try / catch / catch. Cela permet de gérer différents types d’exception avec différentes clauses [catch] ;
- ligne 23 : intercepte les exceptions de type [Exception1] ;
- ligne 26 : intercepte les exceptions de type [Exception2] ;
- ligne 49 : intercepte les exceptions de type [Exception1] ou (|) [Exception2] ;
Résultats
1 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 | object(Exception1)#1 (7) {
["message":protected]=>
string(10) "Erreur1…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(0)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-06.php"
["line":protected]=>
int(13)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
NULL
}
object(Exception2)#2 (7) {
["message":protected]=>
string(10) "Erreur2…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(0)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-06.php"
["line":protected]=>
int(15)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
NULL
}
premier test
Exception 1
Erreur1…
second test
Exception 2
Erreur2…
troisième test
Exception 1 ou 2
Erreur1…
quatrième test
Exception 1 ou 2
Erreur2…
|
Commentaires des résultats
- lignes 1-17 : le ‘contenu’ d’une exception :
- lignes 2-3 : le message d’erreur ;
- lignes 6-7 : le code d’erreur ;
- lignes 8-9 : le nom du fichier dans lequel s’est produite l’exception ;
- lignes 10-11 : la ligne à laquelle s’est produite l’exception ;
- lignes 15-16 : l’exception précédente. Une exception peut encapsuler une autre exception et définir ainsi une pile d’exceptions. L’attribut [previous] va permettre d’exploiter cette pile ;
Relancer une exception¶
Une exception peut être lancée plusieurs fois comme le montre le script [exceptions-07.php] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php
try {
try {
// on lance une exception
throw new \Exception("test");
} catch (\Exception $ex) {
// on relance l'exception interceptée
throw $ex;
} finally {
// on passera bien dans le finally
print "finally 1\n";
}
} catch (\Exception $ex2) {
// on récupère bien l'exception initiale
print $ex2->getMessage() . " dans try / catch / finally externe\n";
} finally {
// on passera bien dans le finally
print "finally 2\n";
}
|
Commentaires
- ligne 6 : on lance une exception ;
- ligne 7 : on l’intercepte ;
- ligne 9 : on la relance. Elle passe alors dans la structure try / catch / finally du niveau supérieur ;
- ligne 14 : on l’intercepte de nouveau ;
- lignes 10-12 : l’exécution montre que même après le [throw] de la ligne 9, on passe bien dans la clause [finally] du try / catch / finally ;
Résultats
1 2 3 | finally 1
test dans try / catch / finally externe
finally 2
|
Exploitation d’une pile d’exceptions¶
Une exception peut encapsuler une autre exception qui elle-même peut en encapsuler une autre formant finalement une pile d’exceptions. Voici un exemple [exceptions-08.php] :
1 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 | <?php
// on définit notre propre famille d'exceptions
class Exception1 extends \RuntimeException {
}
class Exception2 extends \RuntimeException {
}
class Exception3 extends \RuntimeException {
}
// ou utilise nos exceptions
$e1 = new Exception1("Erreur 1…", 1, new Exception2("Erreur 2…", 2, new Exception3("Erreur 3…")));
var_dump($e1);
// exploitation de l'exception courante
print $e1->getMessage() . "\n";
$e = $e1;
while ($e->getPrevious() !== NULL) {
// exception précédente
$e = $e->getPrevious();
// message d'erreur
print $e->getMessage() . "\n";
}
|
Commentaires
- lignes 4-14 : définissent trois classes d’exceptions dérivées de l’exception prédéfinie [RuntimeException] ;
- ligne 17 : une instance de la classe [Exception3] est encapsulée dans une instance de la classe [Exception2] qui elle-même est encapsulée dans une instance de la classe [Exception1]. Le constructeur utilisé ici est le constructeur de la classe [Exception] :
Le 3e paramètre du constructeur permet d’encapsuler une exception. Cela peut être utile dans le scénario suivant :
- on définit une méthode M qui peut générer une exception de type [Exception1] et seulement de ce type pour des raisons de compatibilité par exemple avec une interface ;
- or dans la méthode M peuvent se produire d’autres types d’exceptions. Pour remonter une erreur au code appelant la méthode M, on encapsulera alors ces exceptions dans le type [Exception1] que l’on lancera. Cela permet de ne pas perdre l’information contenue dans l’exception encapsulée et qui a été la cause originelle de l’erreur ;
- les lignes 20-27 montrent comment gérer la pile des exceptions internes à une exception ;
Résultats
1 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 | object(Exception1)#1 (7) {
["message":protected]=>
string(11) "Erreur 1…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(1)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
object(Exception2)#2 (7) {
["message":protected]=>
string(11) "Erreur 2…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(2)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
object(Exception3)#3 (7) {
["message":protected]=>
string(11) "Erreur 3…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(0)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
NULL
}
}
}
Erreur 1…
Erreur 2…
Erreur 3…
|
Exercice d’application – version 3¶
On reprend l’exercice déjà étudié précédemment (paragraphes lien et lien) pour le résoudre avec un code PHP utilisant une classe.
L’arborescence des scripts¶
L’exception [ExceptionImpots]¶
Dans la version 03, lorsqu’un constructeur ou une méthode de classe rencontrera une erreur, elle lancera une exception de type [ExceptionImpots] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | <?php
// espace de noms
namespace Application;
class ExceptionImpots extends \RuntimeException {
public function __construct(string $message, int $code=0) {
parent::__construct($message, $code);
}
}
|
Commentaires
- ligne 4 : la classe [ExceptionImpots] est dans l’espace de noms [Application] ;
- ligne 6 : la classe [ExceptionImpots] étend la classe prédéfinie dans PHP [RuntimeException] ;
- ligne 8 : le constructeur attend deux paramètres :
- $message : est le message d’erreur associée à l’exception ;
- $code : est le code d’erreur associé à l’exception. S’il n’est pas présent, alors le code 0 sera utilisé ;
La classe [TaxAdminData]¶
Dans la version 02, les données de l’administration fiscale ont été rassemblées :
- d’abord dans un fichier jSON ;
- puis de ce fichier jSON à un tableau associatif ;
Dans la version 03, les données de l’administration fiscale sont toujours dans le fichier [taxadmindata.json] mais avec des noms d’attribut différents :
1 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 | {
"limites": [
9964,
27519,
73779,
156244,
0
],
"coeffR": [
0,
0.14,
0.3,
0.41,
0.45
],
"coeffN": [
0,
1394.96,
5798,
13913.69,
20163.45
],
"plafondQfDemiPart": 1551,
"plafondRevenusCelibatairePourReduction": 21037,
"plafondRevenusCouplePourReduction": 42074,
"valeurReducDemiPart": 3797,
"plafondDecoteCelibataire": 1196,
"plafondDecoteCouple": 1970,
"plafondImpotCouplePourDecote": 2627,
"plafondImpotCelibatairePourDecote": 1595,
"abattementDixPourcentMax": 12502,
"abattementDixPourcentMin": 437
}
|
Dans la version 02, ce fichier servait à initialiser un tableau associatif. Dans la version 03 le fichier va initialiser la classe [TaxAdminData] suivante :
1 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 | <?php
namespace Application;
class TaxAdminData {
// tranches d'impôt
private $limites;
private $coeffR;
private $coeffN;
// constantes de calcul de l'impôt
private $plafondQfDemiPart;
private $plafondRevenusCelibatairePourReduction;
private $plafondRevenusCouplePourReduction;
private $valeurReducDemiPart;
private $plafondDecoteCelibataire;
private $plafondDecoteCouple;
private $plafondImpotCouplePourDecote;
private $plafondImpotCelibatairePourDecote;
private $abattementDixPourcentMax;
private $abattementDixPourcentMin;
// initialisation
public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
// on récupère le contenu du fichier des données fiscales
$fileContents = \file_get_contents($taxAdminDataFilename);
$erreur = FALSE;
// erreur ?
if (!$fileContents) {
// on note l'erreur
$erreur = TRUE;
$message = "Le fichier des données [$taxAdminDataFilename] n'existe pas";
}
if (!$erreur) {
// on récupère le code jSON du fichier de configuration dans un tableau associatif
$arrayTaxAdminData = \json_decode($fileContents, true);
// erreur ?
if ($arrayTaxAdminData === FALSE) {
// on note l'erreur
$erreur = TRUE;
$message = "Le fichier de données jSON [$taxAdminDataFilename] n'a pu être exploité correctement";
}
}
// erreur ?
if ($erreur) {
// on lance une exception
throw new ExceptionImpots($message);
}
// initialisation des attributs de la classe
foreach ($arrayTaxAdminData as $key => $value) {
$this->$key = $value;
}
// on vérifie que toutes les clés ont été initialisées
$arrayOfAttributes = \get_object_vars($this);
foreach ($arrayOfAttributes as $key => $value) {
if (!isset($this->$key)) {
throw new ExceptionImpots("L'attribut [$key] de [TaxAdminData] n'a pas été initialisé");
}
}
// on vérifie qu'on a que des valeurs réelles
foreach ($this as $key => $value) {
// $value doit être un nbre réel >=0 ou un tableau de réels >=0
$result = $this->check($value);
// erreur ?
if ($result->erreur) {
// on lance une exception
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// on note la valeur
$this->$key = $result->value;
}
}
// on rend l'objet
return $this;
}
private function check($value): \stdClass {
…
return $result;
}
// toString
public function __toString() {
// chaîne Json de l'objet
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
// getters et setters
public function getLimites() {
return $this->limites;
}
public function getCoeffR() {
return $this->coeffR;
}
…
}
public function setLimites($limites) {
$this->limites = $limites;
return $this;
}
public function setCoeffR($coeffR) {
$this->coeffR = $coeffR;
return $this;
}
…
}
|
Commentaires
- lignes 6-20 : les attributs qui vont accueillir les attributs de même nom du fichier jSON [taxadmindata.json]. C’est un point important : les attributs de la classe [TaxAdminData] sont identiques à ceux du fichier jSON [taxadmindata.json]. Cette particularité facilite beaucoup l’écriture du code ;
- la classe [TaxAdminData] n’a pas de constructeur. En PHP, il n’est pas possible d’avoir plusieurs constructeurs. En fixer un empêche alors d’initialiser l’objet d’une autre façon. Dans la suite, nos classes n’auront pas de constructeur mais plusieurs méthodes de type [setFromQqChose] qui permettront de l’initialiser de différentes façons. La construction d’un objet de type [TaxAdminData] se fait alors avec l’expression :
1 | (new TaxAdminData())→setFromQqChose(…)
|
- ligne 23 : la méthode [setFromJsonFile] initialise les attributs de la classe avec ceux de même nom dans le fichier [$jsonFilename] ;
- lignes 24-42 : le fichier jSON est exploité pour construire le tableau associatif [$arrayTaxAdminData]. Nous avons déjà rencontré ce code dans le script [main.php] de la version 02 ;
- lignes 44-47 : si on a rencontré une erreur dans l’exploitation du fichier jSON, on lance une exception. Celle-ci remontera jusqu’au script principal [main.php] ;
- lignes 48-51 : les attributs de la classe sont initialisés. On profite ici du fait que le tableau associatif [$arrayTaxAdminData] et la classe [TaxAdminData] ont des attributs de mêmes noms que les valeurs provenant du fichier jSON ;
- lignes 53-57 : on vérifie que tous les attributs de la classe [TaxAdminData] ont été initialisés ;
- ligne 53 : l’expression [get_object_vars($this)] rend un tableau associatif dont les attributs sont ceux de l’objet [$this], donc les attributs de la classe [TaxAdminData]. Ici il faut comprendre que l’opération d’initialisation des lignes 48-51 a pu ajouter des attributs à l’objet [$this]. Ainsi si on écrit :
1 | $this->x = "1000";
|
alors l’attribut [x] est ajouté à l’objet [$this] même si cet attribut n’a pas été déclaré dans la classe [TaxAdminData]. Ce qui est sûr, c’est que les attributs des lignes 6-20 font bien partie de l’objet [$this], mais ils ont pu être non initialisés. C’est une erreur facile à faire, il suffit de se tromper dans un nom d’attribut dans le fichier [taxadmindata.json] ;
- lignes 54-57 : on passe en revue tous les attributs de [$this] et si l’un d’eux n’a pas été initialisé, on lance une exception ;
- un attribut peut être initialisé avec une valeur incorrecte. En PHP, il n’est pas possible de donner un type aux attributs. Ainsi l’opération :
1 | $this→plafondQfDemiPart=’abcd’
|
est possible alors que l’attribut [$plafondQfDemiPart] devrait être réel ;
- lignes 59-71 : on vérifie que chacun des attributs de la classe a une valeur numérique réelle positive ou nulle. C’est la fonction [check] de la ligne 76 qui fait ce travail. Son paramètre [$value] est soit une unique valeur soit un tableau de valeurs ;
- ligne 62 : la fonction [check] rend un objet de type
[stdClass] avec deux attributs :
- [erreur] : à TRUE s’il y a eu erreur, à FALSE sinon ;
- [value] : la valeur numérique réelle correspondant au paramètre [$value] passé en paramètre, ligne 62 ;
- ligne 64 : on regarde si la vérification a réussi ou pas ;
- ligne 66 : si un attribut n’est pas un nombre réel positif ou nul, on lance une exception ;
- ligne 69 : sinon on note sa valeur numérique ;
- ligne 73 : on rend l’objet [$this] comme résultat ;
La fonction [check] est la suivante :
1 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 | private function check($value): \stdClass {
// $value est soit un tableau d'éléments soit un unique élément
// on crée un tableau
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// on transforme le tableau d'éléments de type non connu en tableau de réels
$newTableau = [];
$result = new \stdClass();
// les éléments du tableau doivent être des nombres décimaux positifs ou nuls
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// on met le float dans newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// on note l'erreur
$result->erreur = TRUE;
// on quitte
return $result;
}
}
// on rend le résultat
$result->erreur = FALSE;
if (!\is_array($value)) {
// une seule valeur
$result->value = $newTableau[0];
} else {
// une liste de valeurs
$result->value = $newTableau;
}
return $result;
}
|
Commentaires
- ligne 1 : le paramètre [$value] est soit un tableau soit un unique élément. Par ailleurs, on ne connaît pas son type. La valeur provient du fichier [taxadmindata.json]. Selon les valeurs inscrites dans ce fichier, les valeurs lues peuvent être des entiers, des réels, des chaînes, des booléens. Par exemple :
1 2 3 4 | "plafondQfDemiPart": 1551,
"plafondQfDemiPart": 1551.78,
"plafondQfDemiPart": "1551",
"plafondQfDemiPart": "xx",
|
Dans le cas 1, la valeur est de type [entier], dans le cas 2 de type [réel], dans le cas 3 de type [string] pouvant être converti en nombre, dans le cas 4 de type [string] ne pouvant pas être converti en nombre ;
- lignes 4-8 : on crée un tableau à partir du paramètre [$value] reçu en paramètre ligne 1 ;
- ligne 10 : le tableau qu’on va remplir avec des nombres réels ;
- ligne 11 : le résultat sera un objet de type [stdClass] ;
- ligne 13 : expression relationnelle d’un nombre réel positif ou nul ;
- lignes 14-24 : on vérifie que tous les éléments du tableau [$tableau] sont des nombres réels positifs ou nuls et on remplit le tableau [$newTableau] avec ces éléments transformés en type [float] (ligne 17) ;
- lignes 18-23 : dès qu’on détecte un élément qui n’est pas un nombre réel positif ou nul, on note l’erreur dans le résultat et on rend celui-ci ;
- lignes 25-34 : cas où tous les éléments du tableau [$tableau] ont été déclarés corrects ;
- ligne 32 : la valeur rendue [$result→value] est un tableau de réels [float] ou un réel unique ;
La fonction [__toString] des lignes 82-85 rend la chaîne jSON des attributs et valeurs de l’objet [$this].
Lignes 87-110 : les getters et setters de la classe ;
Note : il peut être parfois un peu pénible d’avoir à écrire tous les get / set d’une classe surtout lorsqu’il y a beaucoup d’attributs. Netbeans peut générer automatiquement ceux-ci ainsi que le constructeur. Pour ce faire, mettez simplement les attributs [1] :
- en [2], cliquez droit là ou voulez insérer du code puis choisissez l’option [Insert Code] ;
- en [4], indiquez que vous voulez générer le constructeur ;
- en [5], cochez tous les attributs : cela veut dire que vous voulez que le constructeur ait un paramètre pour chacun des attributs ;
- en [6], prenez le style des constructeurs Java ;
- en [7], indiquez que vous voulez explicitement le mot clé [public] devant le constructeur ;
- en [8], validez ;
- en [9], Netbeans a généré le constructeur. Cependant il n’a pas pu mettre le type des paramètres parce qu’il ne les connaît pas. Ajoutez-les vous-même [10] ;
Pour générer les getters et setters, recommencez les étapes 2-4, et à l’étape 4, choissez [Getter and Setter] :
- en [5], indiquez que vous voulez les getters et setters pour chacun des attributs ;
- en [6], indiquez que vous voulez les getters et setters dans le style utilisé par Java : setAttribut, getAttribut ;
- en [7], indiquez que vous que ces getters et setters soient publics ;
- en [8], validez ;
- en [9], les getters et setters générés par Netbeans ;
Effacez ces getters et setters et recommencez les étapes 2-7.
- en [8], cochez l’option [Fluent Setter] que nous n’avions pas cochée précédemment ;
Le résultat obtenu est le suivant :
Chaque setter se termine par une opération [return $this]. Ceci permet d’initialiser les attributs de la façon suivante :
1 | $data→setLimites($limites)→setCoeffR($coeffR)→setCoeffN($coeffN) ;
|
En effet, la valeur de [$data→setLimites($limites)] (ligne 32 du code) est [$this], donc ici [$data]. On peut donc appeler la méthode [setCoeffR($coeffR)] de cet objet et ainsi de suite, puisqu’à son tour, cette méthode rend elle aussi [$this] (ligne 37 du code). Cette écriture des méthodes d’une classe qui fait que les méthodes qui ne devraient rien rendre rendent l’objet [$this] s’appellent une écriture fluente. Elle facilite l’utilisation de ces méthodes.
L’interface [InterfaceImpots]¶
Nous définissons maintenant l’interface [InterfaceImpots] suivante [InterfaceImpots.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php
// espace de noms
namespace Application;
interface InterfaceImpots {
// récupérer les données des tranches d'impôt permettant le calcul de l'impôt
// peut lancer l'exception ExceptionImpots
public function getTaxAdminData(): TaxAdminData;
// l'interface sait calculer un impôt
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// l'interface sait exploiter des données dans des fichiers texte
// $usersFilename : fichier des données utilisateur sous la forme statut marital, nombre d'enfants, salaire annuel
// $resultsFilename : fichier des des résultats sous la forme statut marital, nombre d'enfants, salaire annuel, montat de l'impôt
// $errorsFilename : fichier des erreurs rencontrées
// peut lancer l'exception ExceptionImpots
public function executeBatchImpots(string $usersFileName, string $resultsFileName, string $errorsFileName): void;
}
|
Commentaires
- ligne 4 : l’interface est placée dans l’espace de noms [Application] ;
- ligne 6 : l’interface permettant le calcul des impôts ;
- ligne 10 : la méthode [getTaxAdminData] permettra d’acquérir les données de l’administration fiscale dans un objet de type [TaxAdminData] que nous venons de présenter. Comme ces données peuvent être dans un fichier ou une base de données voire sur le réseau, la méthode [getTaxAdminData] peut échouer à obtenir les données. Dans ce cas, elle lancera une exception de type [ExceptionImpots]. C’est la méthode standard en programmation objet pour signaler une erreur rencontrée dans une méthode ou un constructeur ;
- ligne 13 : la méthode [calculerImpot] permettra de calculer l’impôt d’un usager ;
- ligne 20 : la méthode [executeBatchImpots] permettra de calculer
l’impôt de plusieurs contribuables :
- [$usersFileName] est le nom du fichier texte contenant les données des contribuables ;
- [$resultsFileName] est le nom du fichier texte contenant le montant de l’impôt pour ces contribuables ;
- [$errorsFileName] est le nom du fichier texte contenant les erreurs rencontrées lors de l’exploitation de ces fichiers ;
Le contenu du fichier texte [$usersFileName] pourrait être le suivant :
1 2 3 4 5 6 7 8 9 10 11 | oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000
|
On notera que les lignes 5 et 7 contiennent des éléments erronés.
Le contenu du fichier texte [$resultsFileName] sera alors le suivant :
1 2 3 4 5 6 7 8 9 | {"marié":"oui","enfants":2,"salaire":55555,"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"oui","enfants":2,"salaire":50000,"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":3,"salaire":50000,"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
{"marié":"non","enfants":2,"salaire":100000,"impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":3,"salaire":100000,"impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"marié":"non","enfants":0,"salaire":100000,"impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":2,"salaire":30000,"impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}
{"marié":"non","enfants":0,"salaire":200000,"impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}
{"marié":"oui","enfants":3,"salaire":200000,"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}
|
et celui du fichier texte [$errorsFileName] le suivant :
1 2 | la ligne 5 du fichier taxpayersdata.txt est erronée
la ligne 7 du fichier taxpayersdata.txt est erronée
|
La classe [Utilitaires]¶
Nous définissons par ailleurs une classe [Utilitaires] dans un fichier [Utilitaires.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php
// espace de noms
namespace Application;
// une classe de fonctions utilitaires
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
// on supprime la marque de fin de ligne de $ligne si elle existe
$longueur = strlen($ligne); // longueur ligne
while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
$ligne = substr($ligne, 0, $longueur - 1);
$longueur--;
}
// fin - on rend la ligne
return($ligne);
}
}
|
Commentaires
- ligne 4 : la classe [Utilitaires] est également placée dans l’espace de noms [Exemples] ;
- ligne 9 : la méthode [cutNewLinechar] enlève l’éventuel caractère de fin de ligne du texte qu’on lui a passé en paramètre. Elle rend la nouvelle ligne ainsi formée. On notera que c’est une méthode statique, c’est à dire qu’elle sera appelée sous la former [Utilitaires::cutNewLineChar] ;
La classe abstraite [AbstractBaseImpots]¶
L’interface [InterfaceImpots] sera implémentée par la classe abstraite [AbstractBaseImpots] suivante [AbstractBaseImpots.php] :
1 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 | <?php
// espace de noms
namespace Application;
// définition d'une classe abstraite AbstractBaseImpots
abstract class AbstractBaseImpots implements InterfaceImpots {
// les données de l’administration fiscale
private $taxAdminData = NULL;
// données nécessaires au calcul de l'impôt
abstract function getTaxAdminData(): TaxAdminData;
// calcul de l'impôt
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié : oui, non
// $enfants : nombre d'enfants
// $salaire : salaire annuel
// $this->taxAdminData : données de l'administration fiscale
//
// on vérifie qu'on a bien les données de l'administration fiscale
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// calcul de l'impôt avec enfants
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// calcul de l'impôt sans les enfants
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// application du plafonnement du quotient familial
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// on prend l'impôt le plus fort
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// calcul d'une éventuelle décôte
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// calcul d'une éventuelle réduction d'impôts
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// 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);
}
public function executeBatchImpots(string $usersFileName, string $resultsFileName, string $errorsFileName): void {
…
}
}
|
Commentaires
- ligne 4 : la classe [AbstractBaseImpots] sera dans l’espace de noms [Application] comme les autres éléments de l’application en cours d’écriture ;
- ligne 7 : la classe [AbstractBaseImpots] implémente l’interface [InterfaceImpots] ;
- ligne 9 : les données de l’administration fiscale seront placées dans l’attribut [$taxAdminData] ;
- ligne 12 : implémentation de la méthode [getTaxAdminData] de l’interface. On ne sait pas encore définir cette méthode : nous avons vu un exemple où les données de l’administration fiscale ont été prises dans un fichier jSON au paragraphe. Nous verrons un autre cas où les données seront à chercher dans une base de données. Ce sera aux classes dérivées de définir le contenu de la méthode [getTaxAdminData]. Les deux cas précédents donneront naissance à deux classes dérivées. La méthode [getTaxAdminData] est donc déclarée abstraite ce qui automatiquement rend la classe elle-même abstraite (ligne 7) ;
- lignes 15-64 : la fonction de calcul de l’impôt déjà rencontrée aux paragraphes lien et lien ;
- la version 02 mettait les données de l’administration fiscale dans un
tableau associatif [$taxAdminData]. La version 03 les met dans
l’attribut [$this→taxAdminData]. La 1re différence entre
ces deux solutions est une différence de visibilité des données
fiscales :
- dans la version 02, le tableau associatif [$taxAdminData] n’avait pas une visibilité globale. Il était donc passé en paramètre à toutes les fonctions de calcul de l’impôt ;
- dans la version 03, l’attribut [$this→taxAdminData] a une visibilité globale pour toutes les méthodes de la classe. Il n’est donc pas passé en paramètre à toutes les fonctions de calcul de l’impôt ;
- une seconde différence vient du fait que la version 03 remplace des fonctions par des méthodes de classe. Chaque appel de méthode se fait désormais avec une expression [$this→getMéthode(…)] (lignes 27, 31, 57, 60) ;
- une troisième différence est que lorsque la méthode [calculerImpot] démarre son travail, elle ne sait pas si l’attribut [private $taxAdminData] dont elle a besoin a été initialisé. En effet, le constructeur de la classe ne l’initialise pas. C’est donc à la méthode [calculerImpot] de le faire à l’aide de la méthode [getTaxAdminData] de la ligne 12. C’est ce qui est fait aux lignes 23-25 ;
- en-dehors de ces différences, les méthodes de calcul de l’impôt restent ce qu’elles étaient dans les versions précédentes ;
La fonction [executeBatchImpots] est la suivante :
1 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 | public function executeBatchImpots(string $usersFileName, string $resultsFileName, string $errorsFileName): void {
// pas mal d'erreurs peuvent se produire dès qu'on gère des fichiers
try {
// ouverture fichier des erreurs
$errors = fopen($errorsFileName, "w");
if (!$errors) {
throw new ExceptionImpots("Impossible de créer le fichier des erreurs [$errorsFileName]", 10);
}
// ouverture fichier des résultats
$results = fopen($resultsFileName, "w");
if (!$results) {
throw new ExceptionImpots("Impossible de créer le fichier des résultats [$resultsFileName]", 11);
}
// lecture des données utilisateur
// chaque ligne a la forme statut marital, nombre d'enfants, salaire annuel
$data = fopen($usersFileName, "r");
if (!$data) {
throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$usersFileName]", 12);
}
// on exploite la ligne courante du fichier des données utilisateur
// qui a la forme statut marital, nombre d'enfants, salaire annuel
$num = 1; // n° ligne courante
$nbErreurs = 0; // nbre d'erreurs rencontrées
while ($ligne = fgets($data, 100)) {
// debug
// print "ligne n° " . ($i + 1) . " : " . $ligne;
// on enlève l'éventuelle marque de fin de ligne
$ligne = Utilitaires::cutNewLineChar($ligne);
// on récupère les 3 champs marié:enfants:salaire qui forment $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// on les vérifie
// le statut marital doit être oui ou non
$marié = trim(strtolower($marié));
$erreur = ($marié !== "oui" and $marié !== "non");
if (!$erreur) {
// le nombre d'enfants doit être un entier
$enfants = trim($enfants);
if (!preg_match("/^\s*\d+\s*$/", $enfants)) {
$erreur = TRUE;
} else {
$enfants = (int) $enfants;
}
}
if (!$erreur) {
// le salaire est un entier sans les centimes d'euros
$salaire = trim($salaire);
if (!preg_match("/^\s*\d+\s*$/", $salaire)) {
$erreur = TRUE;
} else {
$salaire = (int) $salaire;
}
}
// erreur ?
if ($erreur) {
fputs($errors, "la ligne [$num] du fichier [$usersFileName] est erronée\n");
$nbErreurs++;
} else {
// on calcule l'impôt
$result = $this->calculerImpot($marié, (int) $enfants, (int) $salaire);
// on inscrit le résultat dans le fichier des résultats
$result = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $result;
fputs($results, \json_encode($result, JSON_UNESCAPED_UNICODE) . "\n");
}
// ligne suivante
$num++;
}
// des erreurs ?
if ($nbErreurs > 0) {
throw new ExceptionImpots("Il y a eu des erreurs", 15);
}
} catch (ExceptionImpots $ex) {
// on relance l'exception
throw $ex;
} finally {
// on ferme tous les fichiers
fclose($data);
fclose($results);
fclose($errors);
}
}
|
Commentaires du code
- ligne 1 : la fonction reçoit trois paramètres :
- [$usersFileName] : le nom du fichier texte contenant les données des contribuables. Chaque ligne de texte contient les données d’un contribuable sous la forme : statut marital (oui / non), nombre d’enfants, salaire annuel :
1 2 | oui,2,55555
oui,2,50000
|
- [$resultsFileName] : le nom du fichier texte qui contiendra les résultats. Chaque ligne de texte aura la forme suivante :
1 2 | {"marié":"oui","enfants":2,"salaire":50000,"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":3,"salaire":50000,"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
|
- [$errorsFileName] : le nom du fichier texte des erreurs :
1 2 | la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
|
- ligne 3 : parce qu’un certain nombre d’opérations peuvent lancer une exception, un try / catch / finally entoure tout le code de la méthode ;
- lignes 3-19 : les trois fichiers sont ouverts. Une exception est lancée dès qu’une ouverture échoue ;
- ligne 24 : les lignes du fichier [$data] sont lues une par une à raison de 100 caractères au plus (les lignes font toutes moins de 100 caractères) ;
- ligne 28 : on utilise la méthode statique [Utilitaires::cutNewLineChar] pour enlever l’éventuelle marque de fin de ligne ;
- ligne 30 : on récupère les trois éléments de la ligne lue ;
- lignes 33-52 : la validité des trois éléments est vérifiée. Ici, on ne lance pas une exception s’il y a eu erreur mais on écrit le message de celle-ci dans le fichier texte [$errors] (ligne 55) ;
- ligne 59 : si la ligne lue est valide, le calcul de l’impôt est fait. On obtient un résultat sous forme de tableau associatif [« impôt » => floor($impot), « surcôte » => $surcôte, « décôte » => $décôte, « réduction » => $réduction, « taux » => $taux] ;
- ligne 61 : au résultat obtenu, on ajoute les clés [marié, enfants, salaire] ;
- ligne 61 : le résultat est inscrit dans le fichier texte [$results] sous la forme de la chaîne jSON du résultat obtenu ;
- lignes 68-70 : à la fin de l’exploitation du fichier [$data], on regarde le nombre de lignes erronées rencontrées. S’il y en a au moins une, on lance une exception ;
- lignes 71-74 : on intercepte l’exception qu’a pu lancer le code et on la relance immédiatement (ligne 73). Le but de cet artifice est de pouvoir avoir une clause [finally] aux lignes 74-79 : quelque soit la façon dont se termine l’exécution du code de la méthode, les trois fichiers qui ont pu être ouverts par ce code sont fermés. Fermer un fichier qui n’a pas été ouvert ne provoque pas d’erreur ;
La classe [ImpotsWithTaxAdminDataInJsonFile]¶
La classe abstraite [AbstractBaseImpots] n’implémente pas la méthode [getTaxAdminData] de l’interface [InterfaceImpots]. Il nous faut donc la définir dans une classe dérivée. Nous le faisons dans la classe dérivée [ImpotsWithTaxAdminDataInJsonFile] suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php
// espace de noms
namespace Application;
// définition d'une classe ImpotsWithDataInArrays
class ImpotsWithTaxAdminDataInJsonFile extends AbstractBaseImpots {
// un attribut de type Data
private $taxAdminData;
// le constructeur
public function __construct(string $jsonFileName) {
// on initialise $this->taxAdminData à partir du fichier jSON
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($jsonFileName);
}
// retourne les données permettant le calcul de l'impôt
public function getTaxAdminData(): TaxAdminData {
// on rend l'attribut [$this->taxAdminData]
return $this->taxAdminData;
}
}
|
Commentaires
- ligne 7 : la classe [ImpotsWithTaxAdminDataInJsonFile] étend la classe abstraite [AbstractBaseImpots]. Elle aura à définir la méthode [getTaxAdminData] que sa classe parent n’a pas définie ;
- ligne 9 : l’attribut [$taxAdminData] contiendra les données de l’administration fiscale ;
- lignes 12-15 : le constructeur reçoit comme unique paramètre le nom du fichiet jSON contenant les données fiscales ;
- ligne 14 : un objet de type [TaxAdminData] est créé puis initialisé. Cette opération peut lancer une exception de type [ExceptionImpots]. Celle-ci remontera jusqu’au script principal [main.php] ;
- lignes 18-20 : on donne un corps à la méthode [getTaxAdminData] que la classe parent n’avait pas définie. Ici, il suffit de rendre l’attribut [$this->taxAdminData] initialisé par le constructeur ;
Le script [main.php]¶
Ces classes et interface sont exploitées par le script [main.php] suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare(strict_types = 1);
// espace de noms
namespace Application;
// inclusion interface et classes
require_once __DIR__ . '/InterfaceImpots.php';
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . '/ExceptionImpots.php';
require_once __DIR__ . '/Utilitaires.php';
require_once __DIR__ . '/AbstractBaseImpots.php';
require_once __DIR__ . "/ImpotsWithTaxAdminDataInJsonFile.php";
// test -----------------------------------------------------
// définition des constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// on crée un objet ImpotsWithTaxAdminDataInJsonFile
$impots = new ImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// on exécute le batch des impôts
$impots->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit();
|
Commentaires
- ligne 4 : on impose le respect strict des types des paramètres des fonctions ;
- ligne 7 : le script [main.php] est lui aussi placé dans l’espace de noms [Application] ;
- lignes 10-15 : on indique à l’interpréteur PHP où se trouvent les classes et interfaces utilisées par le script. On notera qu’ici nous n’avons pas utilisé d’intruction use pour déclarer le nom complet des classes utilisées par le script. C’est en effet inutile parce que le script et les classes sont dans le même espace de noms [Application] ;
- lignes 18-22 : les noms des fichiers texte utilisés dans le script ;
- lignes 24-29 : un objet [ImpotsWithTaxAdminDataInJsonFile] est créé et l’éventuelle exception est gérée ;
- ligne 28 : on exécute la méthode [executeBatchImpots] qui va faire le calcul des impôts pour tous les contribuables du fichier [TAXPAYERSDATA_FILENAME]. Les résultats seront mis dans le fichier [RESULTS_FILENAME] et les erreurs éventuelles dans le fichier [ERRORS_FILENAME] ;
- lignes 29-32 : en cas d’erreur irrécupérable, on affiche le message de l’erreur ;
Résultats
Avec le fichier des contribuables [taxpayersdata.txt] suivants :
1 2 3 4 5 6 7 8 9 10 11 | oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000
|
on obtient le fichier des erreurs [errors.txt] suivant :
1 2 | la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
|
et le fichier des résultats [resultats.txt] suivant :
1 2 3 4 5 6 7 8 9 | {"marié":"oui","enfants":2,"salaire":55555,"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"oui","enfants":2,"salaire":50000,"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":3,"salaire":50000,"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
{"marié":"non","enfants":2,"salaire":100000,"impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":3,"salaire":100000,"impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"marié":"non","enfants":0,"salaire":100000,"impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":2,"salaire":30000,"impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}
{"marié":"non","enfants":0,"salaire":200000,"impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}
{"marié":"oui","enfants":3,"salaire":200000,"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}
|
Les Traits¶
Les Traits sont des structures analogues à des classes. Néanmoins on ne peut les instancier. Elles sont destinées à être incluses dans des classes. L’inclusion d’un Trait dans une classe a le même effet que si l’on avait copié le code du Trait dans la classe. On met dans un Trait du code susceptible d’être réutilisé dans plusieurs classes.
L’arborescence des scripts¶
Inclusion d’un trait dans une classe¶
Le script [traits-01.php] montre une utilisation basique d’un trait dans une classe :
1 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 | <?php
class Class04 {
// attribut
private $name;
// constructeur
public function __construct(string $name) {
$this->name = $name;
}
// getters et setters
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
}
trait Trait04 {
// attribut
private $name;
// getters et setters
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
}
class Class05 {
// inclusion du Trait
use Trait04;
// constructeur
public function __construct(string $name) {
$this->name = $name;
}
}
// test --------------
$class04 = new Class04("Tim");
$class05 = new Class05("Burton");
print $class04->getName() . "\n";
print $class05->getName() . "\n";
// affichage des deux classes
print_r($class04);
print_r($class05);
|
Commentaires du code
- lignes 3-21 : définition de la classe [Class04] avec un attribut, ses get / set et un constructeur ;
- lignes 23-36 : on reprend le code de [Class04] sans son constructeur et on le transfère dans le trait [Trait04] tel quel. On ne reprend pas le constructeur puisqu’un Trait n’est pas instanciable ;
- ligne 23 : c’est le mot clé [trait] qui fait de [Trait04] un trait plutôt qu’une classe ;
- lignes 38-45 : on définit une classe [Class05] qui reprend le code du trait [Trait04] (ligne 40) et lui ajoute un constructeur (lignes 43-45), identique à celui de la classe [Class04] pour rendre la classe instanciable ;
- ligne 40 : c’est le mot clé [use] qui permet l’inclusion d’un Trait dans une classe ;
- lignes 50-56 : des tests montrent que les classes [Class04] et [Class05] fonctionnent de la même façon ;
Résultats
1 2 3 4 5 6 7 8 9 10 | Tim
Burton
Class04 Object
(
[name:Class04:private] => Tim
)
Class05 Object
(
[name:Class05:private] => Burton
)
|
Les résultats des lignes 3-10 montrent que les classes [Class04] et [Class05] ont la même contenu ;
Conclusion
L’utilisation de l’instruction [use Trait] dans une classe est équivalente à inclure le code de [Trait] dans la classe.
Utiliser un même trait dans différentes classes¶
Un premier intérêt du trait semble être la réutilisation d’un même code (attributs +méthodes) entre différentes classes. Nous allons voir cependant qu’on peut arriver au même objectif en utilisant de simples classes.
Le partage d’un trait entre classes est illustré par le script [trait-02.php] suivant :
1 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 | <?php
trait Trait01 {
// attribut
private $id = 0;
// méthode
public function doSomething() {
print "Trait01::doSomething… ($this->id)\n";
}
}
class Class02 {
// inclusion Trait01
use Trait01 {
// la méthode [Trait01::doSomething] est accessible
// dans la classe sous le nom [doSomethingInTrait]
Trait01::doSomething as doSomethingInTrait;
}
// méthode propre à la classe
public function doSomething(): void {
// attribut id
$this->id += 10;
// utilisation méthode de Trait01
$this->doSomethingInTrait();
// afichage local
print "Class02->doSomething\n";
}
}
class Class03 {
// inclusion Trait01
use Trait01;
// méthode locale à la classe
public function doSomethingElse(): void {
// attribut id
$this->id += 10;
// utilisation méthode de Trait01
$this->doSomething();
// affichage local
print "Class03->doSomethingElse\n";
}
}
// test01 ----------------
function test01(): void {
$class02 = new Class02();
$class03 = new Class03();
$class02->doSomething();
$class03->doSomethingElse();
}
// test01
print "test01-----------------\n";
test01();
|
Commentaires
- lignes 3-10 : un trait définissant un attibut (ligne 5) et une méthode (lignes 8-10).
- le trait [Trait01] est injecté dans deux classes [Class02] (lignes 14-32) et [Class03] (lignes 34-46).
- lignes 16-20 : injection de [Trait01] dans [Class02] ;
- la ligne 19 vise à résoudre un conflit : [Trait01] et
[Class02] ont tous les deux une méthode appelée
[doSomething]. Il y a deux cas à prévoir :
- la méthode [Class02::doSomething] est appelée de l’extérieur de la classe. Dans ce cas, la méthode [Class02::doSomething] est prioritaire sur la méthode [Trait01::doSomething] et c’est elle qui est appelée ;
- la méthode [Class02::doSomething] est appelée de l’intérieur de la classe. Dans ce cas il y a conflit : l’interpréteur PHP ne sait pas quelle méthode appeler ;
La ligne 19 permet de renommer [doSomethingInTrait] la méthode [Trait01::doSomething]. Ainsi, à l’intérieur de [Class02] on utilisera les notations :
- [doSomethingInTrait] pour appeler la méthode [Trait01::doSomething] ;
- [doSomething] pour appeler la méthode [Class02::doSomething] ;
- lignes 25, 27 : la classe [Class02] utilisent l’attribut et la méthode de [Trait01] comme s’ils lui étaient propres ;
- lignes 34-48 : la classe [Class03] est identique à la classe [Class02]. L’inclusion de [Trait01] est ici plus simple parce qu’il n’y pas collision entre les méthodes de [Trait01] et [Class03] ;
Résultats
1 2 3 4 5 | test01-----------------
Trait01::doSomething… (10)
Class02->doSomething
Trait01::doSomething… (10)
Class03->doSomethingElse
|
On notera bien qu’il n’y a pas partage du trait [Trait01] entre les classes [Class02] et [Class03]. Ainsi l’attribut [Trait01::i] devient par inclusion de [Trait01] dans les classes [Class02] et [Class03] deux attributs différents [Class02::i] et [Class03::i]. C’est ce que montrent les lignes 2 et 4 des résultats. Si l’attribut [Trait01::i] avait été partagé entre les classes [Class02] et [Class03] on aurait eu 20 à la ligne 4 au lieu de 10.
Le script [trait-03.php] montre qu’on peut arriver au même résultat en utilisant une classe au lieu du trait :
1 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 | <?php
// classe qui remplace le trait
class Class01 {
// attribut
private $id = 0;
// setter
public function setId(int $id) {
$this->id = $id;
}
// getter
public function getId(): int {
return $this->id;
}
// méthode
public function doSomething(): void {
print "Class01::doSomething… ($this->id)\n";
}
}
class Class02 {
// inclusion Class01
private $class01;
// setter
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// méthode propre à la classe
public function doSomething(): void {
// chgt attribut de Class01
$id = $this->class01->getId();
$id += 10;
$this->class01->setId($id);
// utilisation méthode de Class01
$this->class01->doSomething();
// afichage local
print "Class02->doSomething\n";
}
}
class Class03 {
// inclusion Class01
private $class01;
// setter
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// méthode locale à la classe
public function doSomethingElse(): void {
// chgt attribut de Class01
$id = $this->class01->getId();
$id += 10;
$this->class01->setId($id);
// utilisation méthode de Class01
$this->class01->doSomething();
// affichage local
print "Class03->doSomethingElse\n";
}
}
// test01 ----------------
function test01(): void {
// deux objets
$class02 = new Class02();
$class03 = new Class03();
// vont accéder à deux instances différentes de [Class01]
$class02->setClass01(new Class01());
$class03->setClass01(new Class01());
// vérification
$class02->doSomething();
$class03->doSomethingElse();
}
// test02 ----------------
function test02(): void {
// instance partagée de [Class01]
$class01 = new Class01();
// deux objets
$class02 = new Class02();
$class03 = new Class03();
// vont accéder à la même instance de [Class01]
$class02->setClass01($class01);
$class03->setClass01($class01);
// vérification
$class02->doSomething();
$class03->doSomethingElse();
}
// test01
print "test01-----------------\n";
test01();
// test02
print "test02-----------------\n";
test02();
|
Commentaires
- lignes 4-23 : la classe [Class01] remplace le trait [Trait01]. Le code de [Trait01] était incorporé au code des classes [Class02] et [Class03]. Ici, ce ne sera pas le cas. Ce sera une référence au code de la classe [Class01] qui sera injectée dans les classes [Class02] et [Class03]. Ce qui fait que les attributs de la classe [Class01] seront extérieurs au code des [Class02] et [Class03]. Comme ici, l’attribut [id] est privé (ligne 6), il faut prévoir un getter (lignes 14-16) et un setter (lignes 9-11). C’est la 1re différence avec le trait : il faut créer le code d’accès aux attributs privés de la classe. On aurait pu passer la visibilité de l’attribut [id] à [public] mais il n’est jamais conseillé de faire cela. Passer par un setter pour fixer la valeur d’un attribut permet d’en vérifier la validité ;
- ligne 27 : inclusion dans le code de [Class02] d’une référence à la classe [Class01]. Parce que cet attribut est privé, il nous faut créer un setter (lignes 30-32) pour l’initialiser ;
- lignes 37-41 : pour tout usage du code de [Class01], il nous faut passer par l’attribut [$this→class01] ;
- lignes 48-69 : la classe [Class03] est un clône de la classe [Classe02] si ce n’est que sa méthode a un nom différent ;
- lignes 72-82 : le 1er test. Celui-ci consiste à injecter dans les classes [Class02] et [Class03] deux instances différentes de la classe [Class01] (lignes 77 et 78) ;
- lignes 85-97 : le 2e test injecte dans les classes [Class02] et [Class03] la même instance de la classe [Class01] (lignes 97, 92, 93) ;
- lignes 100-101 : exécution du test [test01] ;
- lignes 103-104 : exécution du test [test02] ;
Résultats
1 2 3 4 5 6 7 8 9 10 | test01-----------------
Class01::doSomething… (10)
Class02->doSomething
Class01::doSomething… (10)
Class03->doSomethingElse
test02-----------------
Class01::doSomething… (10)
Class02->doSomething
Class01::doSomething… (20)
Class03->doSomethingElse
|
Commentaires des résultats
- lignes 2-5 : on obtient les mêmes résultats qu’avec le trait [Trait01]. On en conclura que l’usage du trait n’est ici pas indispensable mais qu’il amène une réduction du code dû au fait qu’il n’y a pas besoin de méthodes pour accéder aux attributs du trait : ceux-ci font partie intégrante du code dans lequel le trait a été incorporé ;
- lignes 7-10 : du fait qu’on a injecté la même référence de [Class01] dans les classes [Class02] et [Class03], l’attribut [Class01::id] a été partagé entre les deux classes. C’est pour cela que la ligne 9 des résultats affiche 20 al lieu de 10 avec le trait. On en conclura que si des attributs du trait doivent être partagés entre des classes, alors le trait n’est pas utilisable et il faut alors utiliser une classe ;
Regrouper des méthodes dans un trait¶
Dans l’exemple précédent, le trait comportait attributs et méthodes. Nous considérons ici le cas où il ne contient que des méthodes. Dans ce cas, le trait ressemble à une factorisation de méthodes qu’on peut alors utiliser dans différentes classes. Comme le trait n’a ici pas d’attribut, on va considérer le cas où les méthodes qu’ils rassemblent travaillent uniquement sur des paramètres qu’on leur passe. En fait, ce n’est pas obligatoire : un trait peut travailler sur un attribut [$this→attribut1] sans avoir cet attribut. C’est alors aux classes qui utilisent ce trait de fournir l’attribut [$this→attribut1].
Dans le cas où le trait n’a que des méthodes qui travaillent uniquement sur des paramètres qu’on leur passe, nous montrerons que le trait peut alors être remplacé par une classe ayant les mêmes méthodes que le trait et déclarées statiques.
L’utilisation du trait est illustré par le script [trait-04.php] qui reprend le trait de l’exemple précédent en lui enlevant tout attribut :
1 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 | <?php
trait Trait01 {
// méthode à partager
public function doSomething() {
print "Trait01::doSomething ….\n";
}
}
class Class02 {
// inclusion Trait01
use Trait01 {
// la méthode [Trait01::doSomething] est accessible
// dans la classe sous le nom [doSomethingInTrait]
Trait01::doSomething as doSomethingInTrait;
}
public function doSomething(): void {
// appel méthode de Trait01
$this->doSomethingInTrait();
// affichage local
print "Class02->doSomething\n";
}
}
class Class03 {
// inclusion Trait01
use Trait01;
// méthode locale à la classe
public function doSomethingElse(): void {
// appel méthode de Trait01
$this->doSomething();
// affichage local
print "Class03->doSomethingElse\n";
}
}
// test ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();
|
Commentaires
- lignes 3-10 : le trait [Trait01] n’a plus d’attributs ;
- lignes 14-18 : inclusion de [Trait01] dans [Class02]. La méthode de [Trait01] est utilisée ligne 22 ;
- ligne 31 : inclusion de [Trait01] dans [Class03]. La méthode de [Trait01] est utilisée ligne 36 ;
Résultats
1 2 3 4 | Trait01::doSomething ….
Class02->doSomething
Trait01::doSomething ….
Class03->doSomethingElse
|
Dans ce cas d’usage, le trait [Trait01] peut aisément être remplacé par une classe. Ceci est montré par le script [trait-05.php] suivant :
1 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 | <?php
abstract class Class01 {
// méthode statique à partager
public static function doSomething() {
print "Class01::doSomething ….\n";
}
}
class Class02 {
public function doSomething(): void {
// appel méthode de Class01
Class01::doSomething();
// affichage local
print "Class02->doSomething\n";
}
}
class Class03 {
// méthode locale à la classe
public function doSomethingElse(): void {
// appel méthode de Class01
Class01::doSomething();
// affichage local
print "Class03->doSomethingElse\n";
}
}
// test ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();
|
Commentaires
- lignes 3-10 : le trait [Trait01] est remplacé par une classe abstraite [Class01] dont toutes les méthodes sont déclarées statiques. La classe est déclarée abstraite uniquement pour empêcher son instanciation. On aurait voulu écrire également [final] pour empêcher sa dérivation mais PHP 7 n’accepte pas le préfixe [final abstract] pour une classe. C’est l’un ou l’autre mais pas les deux ;
- ligne 16 : au lieu d’écrire [$this→doSomethingInTrait] on écrit maintenant [Class01::doSomething], ç-à-d qu’on appelle la méthode statique [doSomething] de la classe [Class01] ;
- ligne 28 : on répète la même démarche dans [Class03] ;
Résultats
1 2 3 4 | Class01::doSomething ….
Class02->doSomething
Class01::doSomething ….
Class03->doSomethingElse
|
On a bien le même résultat qu’avec le trait [Trait01] ce qui montre que l’usage de celui-ci peut être évité. On a écrit que les méthodes d’un trait peuvent travailler sur un attribut [$this→attribut1] sans que le trait ait cet attribut. C’est alors aux classes qui utilisent ce trait de fournir l’attribut [$this→attribut1]. C’est un cas exotique : autant ‘remonter’ l’attribut [$this→attribut1] que les classes utilisant le trait doivent avoir, dans le trait lui-même. Ainsi il fera forcément partie des attributs de la classe utilisant le trait.
Héritage multiple avec un trait¶
Il est fréquent de lire dans la littérature PHP que le trait permettrait l’héritage multiple : la possibilité pour une classe d’hériter de plusieurs classes. Le langage C++ possède cette possibilité mais pas les langages Java ou C# qui ne connaissent que l’héritage simple. Nous allons montrer que si effectivement l’usage d’un trait dans une classe dérivée permet d’implémenter quelque chose qui ressemble à l’héritage multiple, ce cas d’usage peut là encore s’implémenter avec de simples classes.
Le script [trait-06.php] met en œuvre une classe dérivée et un trait :
1 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 | <?php
trait Trait01 {
// attribut
private $i;
// méthode à partager
public function doSomethingInTrait01() {
// modification Trait01::$i
$this->i++;
// affichage
print "Trait01::doSomethingInTrait01… i=$this->i\n";
}
}
class Class02 {
// attribut
protected $j = 0;
// méthode
public function doSomethingInClass02(): void {
// modification Class02::j
$this->j += 10;
// affichage
print "Class02->doSomethingInClass02… j=$this->j\n";
}
}
// classe dérivée
class Class03 extends Class02 {
// hérite de Class02:j et Trait01::i
// inclusion Trait01
use Trait01;
// méthode
public function doSomethingInClass03(): void {
// utilisation méthode de Trait01
$this->doSomethingInTrait01();
// modification Trait01::i
$this->i += 100;
// modification Class03::j (==Class02::j)
$this->j += 1000;
// affichage
print "Class03->doSomethingInClass03… i=$this->i, j=$this->j\n";
}
}
// test ----------------
(new Class02())->doSomethingInClass02();
(new Class03())->doSomethingInClass03();
|
Commentaires
- lignes 3-15 : on revient à un trait [Trait01] avec un attribut et une méthode manipulant celui-ci ;
- lignes 17-29 : une classe [Class02] qui n’a rien à voir avec le trait [Trait01]. Elle ne l’utilise pas ;
- ligne 19 : on a déclaré l’unique attribut de [Class02] avec une visibilité [protected] pour que celui-ci soit accessible dans les classes dérivées ;
- ligne 32 : la classe [Class03] étend la classe [Class02]. De plus elle incorpore le trait [Trait01] (ligne 35). Finalement, elle hérite des attributs et méthodes de [Class02] et incorpore les attributs et méthodes de [Trait01]. On a donc bien quelque chose d’analogue à l’héritage multiple ;
Résultats
1 2 3 | Class02->doSomethingInClass02… j=10
Trait01::doSomethingInTrait01… i=1
Class03->doSomethingInClass03… i=101, j=1000
|
De la même façon qu’il a été fait dans un exemple précédent, nous allons montrer que :
- le trait peut être remplacé par une classe ;
- au lieu d’incorporer le trait dans la classe dérivée, on incorpore la référence d’une instance de la classe ;
Le script [trait-07.php] est le suivant :
1 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 | <?php
class Class01 {
// attribut
protected $i;
// getter et setter
public function getI(): int {
return $this->i;
}
public function setI(int $i): void {
$this->i = $i;
}
// méthode
public function doSomethingInClass01(): void {
// modification Class01::$i
$this->i++;
// affichage
print "Class01::doSomething in Class01… i=$this->i\n";
}
}
class Class02 {
// attribut
protected $j = 0;
// méthode propre à la classe
public function doSomethingInClass02(): void {
// modification Class02::j
$this->j += 10;
// affichage
print "Class02->doSomethingInClass02… j=$this->j\n";
}
}
class Class03 extends Class02 {
// inclusion Class01
private $class01;
// setter
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// méthode locale à la classe
public function doSomethingInClass03(): void {
// utilisation méthode de Class01
$this->class01->doSomethingInClass01();
// modification Class01::i
$i = $this->class01->getI();
$i += 100;
$this->class01->setI($i);
// modification Class03::j
$this->j += 1000;
// affichage
print "Class03->doSomethingInClass03… i=$i, j=$this->j\n";
}
}
// test ----------------
$class01 = new Class01();
$class02 = new Class02();
$class03 = new Class03();
$class03->setClass01($class01);
$class02->doSomethingInClass02();
$class03->doSomethingInClass03();
|
Commentaires
- lignes 3-24 : la classe [Class01] remplace le trait [Trait01]. Comme la classe [Class01] va être incorporée dans les classes via une référence, on a prévu des get / set pour l’attribut [$i] ;
- lignes 26-38 : la classe [Classe02] ne change pas ;
- ligne 40 : la classe [Class03] étend la classe [Classe02] ;
- ligne 42 : la classe [Classe01] est incorporée à [Classe03] via une référence ;
- lignes 45-47 : on prévoit un setter pour initialiser la référence à la classe [Classe01] ;
- lignes 50-61 : la méthode [doSomethingInClass03] fait la même chose que précédemment avec cependant un code plus complexe ;
Résultats
1 2 3 | Class02->doSomethingInClass02… j=10
Class01::doSomething in Class01… i=1
Class03->doSomethingInClass03… i=101, j=1000
|
De cet exemple, on peut conclure que là encore le trait n’est pas indispensable mais il faut reconnaître qu’il permet l’écriture d’un code plus court dans la classe dérivée.
Utiliser un trait à la place d’une classe abstraite¶
On rencontre souvent le cas d’utilisation suivant : on crée une interface I assez générale qui peut donner naissance à plusieurs implémentations. Celles-ci partagent un code commun mais diffèrent par d’autres méthodes. On peut implémenter ce cas d’utilisation de deux façons :
- on crée une classe abstraite C qui regroupe le code commun aux classes dérivées. La classe C implémente l’interface I mais certaines méthodes qui doivent être déclarées dans les classes dérivées sont dans la classe C déclarées abstraites et donc la classe C est elle-même abstraite. On crée ensuite des classes C1 et C2 dérivées de C qui implémentent chacune à leur manière les méthodes non définies (abstraites) de leur classe parent C ;
- on crée un trait T quasi identique à la classe abstraite C de la solution précédente. Ce trait n’implémente pas l’interface I car syntaxiquement elle ne le peut pas. On crée ensuite des classes C1 et C2 implémentant l’interface I et utilisant le trait T. Il ne reste plus à ces classes qu’à implémenter les méthodes de l’interface I non implémentées par le trait T qu’elles importent ;
Voici un exemple qui montre la grande proximité de ces deux solutions.
L’application 1 implémente la solution 1 décrite précédemment [trait-08.php] :
1 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 | <?php
interface Interface1 {
public function doSomething(): void;
public function doSomethingElse(): void;
}
abstract class AbstractClass implements Interface1 {
// attributs
protected $attr1 = 11;
protected $attr2 = 12;
// getters et setters
public function getAttr1() {
return $this->attr1;
}
public function getAttr2() {
return $this->attr2;
}
public function setAttr1($attr1) {
$this->attr1 = $attr1;
return $this;
}
public function setAttr2($attr2) {
$this->attr2 = $attr2;
return $this;
}
// méthode implémentée
public function doSomething(): void {
print "AbstractClass::doSomething [$this->attr1,$this->attr2]\n";
}
// méthode non implémentée
abstract public function doSomethingElse(): void;
}
// classe dérivée 1
class Class1 extends AbstractClass {
// attribut
private $attr3 = 13;
// getter et setter
public function getAttr3() {
return $this->attr3;
}
public function setAttr3($attr3) {
$this->attr3 = $attr3;
return $this;
}
// implémentation doSomethingElse
public function doSomethingElse(): void {
print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
}
}
// classe dérivée 2
class Class2 extends AbstractClass {
// attribut
private $attr4 = 14;
public function getAttr4() {
return $this->attr4;
}
public function setAttr4($attr4) {
$this->attr4 = $attr4;
return $this;
}
// implémentation doSomethingElse
public function doSomethingElse(): void {
print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
}
}
// fonction externe
function useInterfaceWith(Interface1 $interface):void{
$interface->doSomething();
$interface->doSomethingElse();
}
// tests
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
|
Commentaires
- lignes 3-8 : l’interface [Interface1] a deux méthodes ;
- lignes 10-41 : la classe abstraite [AbstractClass] implémente l’interface [Interface1] (ligne 10). Elle a deux attributs avec leurs getters et setters (lignes 12-32), implémente la méthode [doSomething] de l’interface [Interface1] (lignes 35-37) mais ne sait pas implémenter la méthode [doSomethingElse]. Celle-ci est donc déclarée abstraite (ligne 40). La classe abstraite [AbstractClass] ne peut être instanciée et pour servir à quelque chose elle doit obligatoirement être dérivée ;
- lignes 44-63 : la classe Class1 étend la classe abstraite [AbstractClass] et donc implémente l’interface [Interface1] (ligne 14). Elle donne un corps à la méthode [doSomethingElse] que sa classe parent n’avait pas définie (lignes 59-61). Elle ajoute également un attribut à ceux de sa classe parent (lignes 46-56) ;
- lignes 66-82 : la classe Class2 étend la classe abstraite [AbstractClass] et donc implémente l’interface [Interface1] (ligne 66). Elle donne un corps à la méthode [doSomethingElse] que sa classe parent n’avait pas définie (lignes 80-82). Elle ajoute également un attribut à ceux de sa classe parent (lignes 68-77) ;
- lignes 87-90 : la fonction [useInterfaceWith] reçoit en paramètre un type [Interface1] et appelle les deux méthodes de cette interface ;
- lignes 93-94 : on appelle la fonction [useInterfaceWith] la 1re fois avec un type [Class1] et la seconde fois avec un type [Class2]. C’est correct puisque ces deux types implémentent l’interface [Interface1] ;
Résultats
1 2 3 4 | AbstractClass::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
AbstractClass::doSomething [11,12]
Class2::doSomethingElse [11,12,14]
|
Maintenant nous implémentons la solution 2 avec le script [trait-09.php]. Cela consiste à remplacer la classe abstraite par un trait :
1 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 | <?php
interface Interface1 {
public function doSomething(): void;
public function doSomethingElse(): void;
}
trait Trait1 {
// attributs
private $attr1 = 11;
private $attr2 = 12;
// getters et setters
public function getAttr1() {
return $this->attr1;
}
public function getAttr2() {
return $this->attr2;
}
public function setAttr1($attr1) {
$this->attr1 = $attr1;
return $this;
}
public function setAttr2($attr2) {
$this->attr2 = $attr2;
return $this;
}
// méthode implémentée
public function doSomething(): void {
print "Trait::doSomething [$this->attr1,$this->attr2]\n";
}
}
// classe dérivée 1
class Class1 implements Interface1 {
// utilisation du trait
use Trait1;
// attribut
private $attr3 = 13;
// getter et setter
public function getAttr3() {
return $this->attr3;
}
public function setAttr3($attr3) {
$this->attr3 = $attr3;
return $this;
}
// implémentation doSomethingElse
public function doSomethingElse(): void {
print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
}
}
// classe dérivée 2
class Class2 implements Interface1 {
// utilisation du trait
use Trait1;
// attribut
private $attr4 = 14;
public function getAttr4() {
return $this->attr4;
}
public function setAttr4($attr4) {
$this->attr4 = $attr4;
return $this;
}
// implémentation doSomethingElse
public function doSomethingElse(): void {
print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
}
}
// fonction externe utilisant l'interface
function useInterfaceWith(Interface1 $interface): void {
$interface->doSomething();
$interface->doSomethingElse();
}
// tests
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
|
Commentaires
- lignes 3-8 : l’interface [Interface1] n’a pas changé ;
- lignes 10-41 : le trait [Trait1] remplace la classe abstraite
[AbstractClass] de la solution 1. Le code est le même aux détails
près suivants :
- ligne 10 : le trait [Trait1] n’implémente pas l’interface [Interface1]. C’est syntaxiquement impossible ;
- lignes 12-13 : l’attribut de visibilité [protected] des attributs de la classe abstraite [AbstractClass] devient ici [private]. Ces deux attributs visent à donner aux classes dérivées un accès direct aux attributs de la classe parent (protected) ou du trait (private) sans avoir à passer par les getters et setters ;
- le trait [Trait1] ne déclare pas la méthode abstraite [doSomethingElse] ;
- lignes 41-62 : la classe [Class1] de la solution 2 est identique
à la classe [Class1] de la solution 1 aux détails près suivants :
- ligne 41 : la classe [Class1] implémente l’interface [Interface1] alors que dans la solution 1, elle étendait la classe abstraite [AbstractClass] ;
- ligne 43 : elle utilise le trait [Trait1] pour implémenter une partie de l’interface ;
- lignes 65-85 : on peut faire les mêmes commentaires que pour [Class1] ;
- lignes 87-95 : le reste du code ne change pas ;
Résultats
1 2 3 4 | Trait::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
Trait::doSomething [11,12]
Class2::doSomethingElse [11,12,14]
|
On obtient bien les mêmes résultats.
Conclusion¶
Des exemples précédents, il ressort que les cas d’utilisation où l’usage du trait amènerait un avantage net ne sont pas clairs. Sur nos exemples on peut toujours s’en passer en le remplaçant par une classe. Il semble cependant que son utilisation soit pratique pour factoriser du code entre différentes classes dérivées, comme si ce code appartenait à une classe parent. C’est ce que nous ferons dans un exemple à suivre.
Applications en couches¶
Introduction¶
Nous allons découvrir maintenant comment structurer une application PHP en couches :
Dans ce schéma, c’est la couche de gauche qui prend l’initiative d’utiliser la couche de droite. Le rôle des couches est le suivant :
- [1] : la couche appelée [dao] (Data Access Objects) s’occupe des échanges avec des entrepôts de données externes [4] (fichiers, bases de données, services web…). Cette couche est parfois appelée [DAL] (Data Access Layer) un terme qui décrit mieux le rôle de la couche. Cette couche peut lire des données [3] ou en écrire [2]. Elle est sollicitée par la couche [métier] [6] et lui rend des résultats [7] ;
- [5] :une couche appelée [métier] qui rassemble des procédures
‘métier’ à qui on fournit toutes les données dont elles ont besoin.
C’est généralement la couche la plus stable d’un projet car elle ne
dépend pas de la façon dont les données sont acquises. Celles-ci ont
deux sources :
- [9] : les données fournies par le script PHP ;
- [6,7] : les données demandées à la couche [dao].
- [8] : le script principal est le chef d’orchestre. Dans une
application console, il va :
- créer les couches [métier] et [dao] ;
- dérouler l’algorithme de l’application. Cet algorithme est celui d’un chef d’orchestre : on n’y trouve aucun code ‘métier’ ou d’accès aux données. Le script principal se contente d’appeler les procédures de la couche [métier] [9]. Il ignore totalement la couche [dao] et les données externes. Il peut fournir à la couche [métier] des données [9]. Dans une application console, celles-ci peuvent provenir de fichiers de configuration ou de l’utilisateur du script. Il reçoit des résultats [10] de la couche [métier]. Il peut avoir besoin de stocker certains résultats : pour cela il utilise de nouveau les procédures de la couche [métier] qui elles vont s’adresser à la couche [dao] qui va faire le travail ;
- parmi les résultats, le script principal peut recevoir des exceptions. C’est son rôle de gérer les exceptions qui remontent de toutes les couches ;
Ce découpage en couches est destiné à faciliter l’évolution de l’application. Pour cela, chaque couche implémente une interface. Supposons que :
- la couche [dao] est implémentée par une classe [Dao] qui implémente une interface [IDao] ;
- la couche [métier] est implémentée par une classe [Métier] qui implémente une interface [IMétier] ;
Les procédures de la couche [métier] vont alors utiliser l’interface [IDao] plutôt que la classe [Dao]. Ceci permet de faire évoluer la couche [Dao] sans toucher à la couche [Métier]. Supposons que dans une version 1, la couche [Dao1] utilise des données d’une base de données. Suite à une évolution, ces données sont maintenant fournies par un service web dans une version 2, [Dao2]. On s’assurera que les deux classes [Dao1] et [Dao2] implémentent la même interface [IDao] et qu’elles lancent les mêmes exceptions. Si ceci est vérifié, la couche [Métier] qui travaille avec l’interface [IDao] restera inchangée ;
Le même raisonnement s’applique à la couche [Métier].
Voyons une implémentation avec des classes et interfaces :
L’application va implémenter la structure en couches suivante :
Objets échangés entre couches¶
En temps normal, les couches échangent divers objets. Ici elles échangeront la classe suivante [Personne.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php
// une personne
class Personne {
// identifiant
private $id;
// constructeur
public function __construct(int $id) {
$this->id = $id;
}
// toString
public function __toString(): string {
return "[Personne($this->id)]";
}
}
|
Commentaires
- ligne 6 : la personne n’a qu’un attribut, son identifiant [$id]. On va supposer que celui-ci désigne une personne unique dans un entrepôt de données ;
- lignes 9-11 : le constructeur qui permet de construire un type [Personne] avec son identifiant ;
- lignes 14-16 : la méthode [__toString] qui affiche l’identifiant de la personne ;
Couche [dao]¶
L’interface [IDao] implémentée par la couche [dao, 1] est la suivante [IDao.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php
// couche [dao] ------------------------
interface IDao {
// récupération d'une personne dans un entrepôt externe
// on passe l'identifiant de la personne
public function get(int $id): Personne;
// sauvegarde d'une personne dans un entrepôt externe
public function save(Personne $p): void;
}
|
Cette interface est implémentée par la classe [Dao1] suivante [Dao1.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php
class Dao1 implements IDao {
// sauvegarde d'une personne dans un entrepôt externe
public function save(Personne $p): void {
print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
}
// récupération d'une personne dans un entrepôt externe
public function get(int $id): Personne {
print "[Dao1] : Récupération de la personne d'identité ($id) en base de données [locale]\n";
return new Personne($id);
}
}
|
Commentaires
- la classe n’interagit pas avec un entrepôt de données. On se contente d’afficher des messages pour suivre le déroulement du code (lignes 7 et 12) ;
- ligne 13 : on rend bien une personne ayant l’identifiant passé en paramètre à la méthode (ligne 11) ;
Nous implémentons également l’interface [IDao] avec la classe [Dao2] suivante [Dao2.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php
class Dao2 implements IDao {
// sauvegarde d'une personne dans un entrepôt externe
public function save(Personne $p): void {
print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
}
// récupération d'une personne dans un entrepôt externe
public function get(int $id): Personne {
print "[Dao2] : Récupération de la personne d'identité ($id) en base de données [distante]\n";
return new Personne($id);
}
}
|
La classe [Dao2] est similaire à la classe [Dao1] si ce n’est que nous avons modifié les messages affichés.
Couche [métier]¶
La couche [métier, 2] offre l’interface [IMétier] suivante [IMetier.php] :
1 2 3 4 5 6 7 8 | <?php
// couche [métier] ------------------------
interface IMétier extends IDao {
// on exploite une personne identifiée par son id
public function doSomething(Personne $p): void;
}
|
Commentaires
- l’interface [IMétier] étend l’interface [IDao]. Ce n’est pas du tout obligatoire. On le fait ici parce que l’exemple est simple ;
- ligne 7 : la méthode [doSomething] est propre à la couche [Métier] ;
L’interface [IMétier] sera implémentée par la classe [Métier] suivante [Métier.php] :
1 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 | <?php
class Métier implements IMétier {
// couche [dao]
private $dao;
// getter / setter
public function setDao(IDao $dao): void {
$this->dao = $dao;
}
public function getDao(): IDao {
return $this->dao;
}
// on exploite une personne
public function doSomething(Personne $p): void {
// traitement
print "Métier : Traitement métier de la personne $p\n";
}
// sauvegarde de la personne
public function save(Personne $p): void {
print "[Métier] : Sauvegarde de la personne $p\n";
// on demande à la couche [dao] de faire la sauvegarde
$this->dao->save($p);
}
public function get(int $id): Personne {
print "[Métier] : récupération de la personne d'identifiant $id\n";
// on demande la personne à la couche [dao]
$personne = $this->dao->get($id);
return $personne;
}
}
|
Commentaires
- ligne 5 : la couche [métier] doit avoir une référence sur la couche [dao] pour pouvoir utiliser les méthodes de celle-ci ;
- lignes 8-10 : la méthode [setDao] permet de donner à la couche [métier] la référence de la couche [dao]. On notera que le type du paramètre est [IDao]. Ainsi la couche [métier] va être capable de travailler avec toute classe implémentant l’interface [IDao]. Si on passe d’une couche [Dao1] à une couche [Dao2] et que toutes deux implémentent l’interface [IDao], la couche [métier] n’a pas à être réécrite ;
- lignes 17-20 : implémentation de [IMetier::doSomething] ;
- lignes 23-27 : implémentation de [IMetier::save]. Cette méthode doit sauvegarder une personne dans un entrepôt de données. La couche [métier] ne sait pas faire ça. Elle s’adresse à la couche [dao] pour faire cette sauvegarde ;
- lignes 29-34 : implémentation de [IMetier::get]. Cette méthode doit récupérer dans un entrepôt de données la personne dont l’identifiant lui est passé en paramètre. La couche [métier] ne sait pas faire ça. Elle s’adresse à la couche [dao] pour faire le travail (ligne 32) ;
Conclusion
Dès que la couche [métier] a besoin d’accéder aux données stockées dans l’entrepôt de données, elle doit passer par la couche [dao] qui a été créée pour accéder à ces données.
Script principal¶
Nous allons écrire deux scripts, chefs d’orchestre pour cette application. Le 1er [main1.php] utilisera la couche [Dao1] alors que le second [main2.php] utilisera la couche [Dao2]. Nous voulons montrer que cela n’a aucune incidence sur le code de la couche [Métier].
Le script [main1.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php
// respect strict des paramètres des fonctions
declare (strict_types=1);
// inclusion classes et interfaces
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao1.php";
require_once __DIR__."/Métier.php";
// test ----------------
// création des couches
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// utilisation de la couche [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
|
Commentaires
- rappelons quelques points : le script [main1.php] est le chef d’orchestre de l’application. Il crée la structure en couches de l’application (lignes 15-17) puis ensuite commence à dialoguer avec la couche [métier] (lignes 19-21). La structure en couches est en effet la suivante :
Selon ce schéma, le script [main1.php] ne doit s’adresser qu’à la couche [métier]. Elle ne doit pas s’adresser à la couche [dao] même si c’est théoriquement possible.
Les résultats de l’exécution sont les suivants :
1 2 3 4 5 | [Métier] : récupération de la personne d'identifiant 4
[Dao1] : Récupération de la personne d'identité (4) en base de données [locale]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao1] : Sauvegarde de la personne [Personne(4)] en base de données [locale]
|
Commentaires
- la ligne 10 du code a provoqué l’écriture des lignes 1 et 2 des résultats ;
- la ligne 20 du code a provoqué l’écriture de la ligne 3 des résultats ;
- la ligne 21 du code a provoqué l’écriture des lignes 4 et 5 des résultats ;
Le script [main2.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php
// respect strict des paramètres des fonctions
declare (strict_types=1);
// inclusion classes et interfaces
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao2.php";
require_once __DIR__."/Métier.php";
// test ----------------
// création des couches
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// utilisation de la couche [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
|
Commentaires
- lignes 36-38 : la structure en couches utilise désormais la couche [Dao2] ;
Les résultats de l’exécution sont les suivants :
1 2 3 4 5 | [Métier] : récupération de la personne d'identifiant 4
[Dao2] : Récupération de la personne d'identité (4) en base de données [distante]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao2] : Sauvegarde de la personne [Personne(4)] en base de données [distante]
|
La couche [Dao1] simule un accès à une base de données locale alors que la couche [Dao2] simule un accès à une base de données distante. Tant que ces deux couches respectent l’interface [IDao], on voit que le code de la couche [Métier] n’a pas eu à être changé.
Nous allons appliquer ce que nous venons d’apprendre à l’exercice du calcul d’impôts qui nous sert de fil rouge.
Exercice d’application – version 4¶
L’application de calcul d’impôts va implémenter la structure en couches suivante :
Nous allons reprendre les éléments de la version 3 du paragraphe lien en les modifiant pour les adapter à la nouvelle architecture de l’application. On appelle parfois cela du ‘refactoring’. Nous supposons ici que les données nécessaires à l’application sont dans des fichiers texte. C’est la couche [Dao] qui va s’occuper des échanges avec ces fichiers.
Arborescence des scripts¶
Objets échangés entre couches¶
Nous allons garder certains objets de la version 3. Nous les redonnons ici pour rappel.
L’exception [ExceptionImpots] est l’exception que lancera la couche [Dao] lorsqu’elle rencontrera un problème soit avec l’accès aux données soit avec la nature des données (données incorrectes).
1 2 3 4 5 6 7 8 9 10 11 | <?php
// espace de noms
namespace Application;
class ExceptionImpots extends \RuntimeException {
public function __construct(string $message, int $code=0) {
parent::__construct($message, $code);
}
}
|
La classe [Utilitaires] rassemble des méthodes utiles à la gestion des fichiers texte (ici une seule méthode) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php
// espace de noms
namespace Application;
// une classe de fonctions utilitaires
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
// on supprime la marque de fin de ligne de $ligne si elle existe
$longueur = strlen($ligne); // longueur ligne
while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
$ligne = substr($ligne, 0, $longueur - 1);
$longueur--;
}
// fin - on rend la ligne
return($ligne);
}
}
|
La classe [TaxAdminData] est la classe qui encapsule les données de l’administration fiscale :
1 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
namespace Application;
class TaxAdminData {
// tranches d'impôt
private $limites;
private $coeffR;
private $coeffN;
// constantes de calcul de l'impôt
private $plafondQfDemiPart;
private $plafondRevenusCelibatairePourReduction;
private $plafondRevenusCouplePourReduction;
private $valeurReducDemiPart;
private $plafondDecoteCelibataire;
private $plafondDecoteCouple;
private $plafondImpotCouplePourDecote;
private $plafondImpotCelibatairePourDecote;
private $abattementDixPourcentMax;
private $abattementDixPourcentMin;
// initialisation
public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
// on récupère le contenu du fichier des données fiscales
$fileContents = \file_get_contents($taxAdminDataFilename);
…
// on rend l'objet
return $this;
}
private function check($value): \stdClass {
…
return $result;
}
// toString
public function __toString() {
// chaîne Json de l'objet
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
// getters et setters
public function getLimites() {
return $this->limites;
}
…
public function setLimites($limites) {
$this->limites = $limites;
return $this;
}
…
}
|
Nous ajoutons une nouvelle classe [TaxPayerData] qui encapsule les données écrites dans le fichier des résultats :
1 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 | <?php
// espace de noms
namespace Application;
// la classe des données
class TaxPayerData {
// données nécessaires au calcul de l'impôt du contribuable
private $marié;
private $enfants;
private $salaire;
// résultats du calcul de l'impôt
private $montant;
private $surcôte;
private $décôte;
private $réduction;
private $taux;
// setter
public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel) : TaxPayerData{
// données du contribuable nécessaires au calcul de l'impôt
$this->marié = $marié;
$this->enfants = $nbEnfants;
$this->salaire = $salaireAnnuel;
// on rend l'objet initialisé
return $this;
}
// getters et setters
public function getMarié() {
return $this->marié;
}
…
public function setMarié($marié) {
$this->marié = $marié;
return $this;
}
…
// toString
public function __toString() {
// chaîne Json de l'objet
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
}
|
Note : utilisez la génération automatique de code pour générer le constructeur, les getters et setters (cf paragraphe lien). Remarquez que les setters sont ‘fluents’.
La couche [dao]¶
Nous nous intéressons ici à la couche [1] de notre application :
L’interface [InterfaceDao]¶
L’interface de la couche [dao] sera la suivante [InterfaceDao.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php
// espace de noms
namespace Application;
interface InterfaceDao {
// lecture des données contribuables
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// lecture des données de l'administration fiscale (tranches d'impôts)
public function getTaxAdminData(): TaxAdminData;
// enregistrement des résultats
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
|
Commentaires
- le cahier des charges est ici le suivant :
- les données des contribuables sont dans un fichier texte ;
- on enregistre les résultats du calcul d’impôts dans un fichier texte ;
- on enregistre les éventuelles erreurs dans un fichier texte ;
- on ne sait pas sous quelle forme sont disponibles les données de l’administration fiscale. Pour chaque nouvelle forme, l’interface [InterfaceDao] devra être implémentée par une nouvelle classe ;
- les méthodes de l’interface qui rencontrent une erreur irrécupérable lors de l’accès aux données doivent lancer une exception de type [ExceptionImpots] ;
- ligne 9 : la méthode qui permet d’obtenir les données du contribuable
[statut marital, nombre d’enfants, salaire annuel] ;
- le 1er paramètre est le nom du fichier texte dans lequel se trouvent ces données ;
- le second paramètre est le nom du fichier texte dans lequel enregistrer les éventuelles erreurs rencontrées ;
- ligne 12 : la méthode qui permet d’obtenir les données de l’administration fiscale. On ne lui passe ici aucun paramètre car on ne sait pas comment elles sont stockées ;
- ligne 15 : la méthode qui permet d’enregistrer les résultats du calcul de l’impôt dans un fichier texte dont on passe le nom en paramètre ;
Lorsqu’on écrit l’interface [InterfaceDao], on sait qu’il y aura différentes façons d’écrire la méthode [getTaxAdminData] selon la façon dont seront stockées les données de l’administration fiscale. L’interface [InterfaceDao] sera donc implémentée par différentes classes, chacune s’occupant d’un stockage particulier de ces données (tableaux, fichiers texte, base de données, service web). Ces classes dérivées auront néanmoins un code commun, celui de l’implémentation des méthodes [getTaxPayersData, saveResults]. On sait que ce cas d’utilisation peut être implémenté de deux façons (cf paragraphe lien):
- on crée une classe abstraite C qui regroupe le code commun aux classes dérivées. La classe C implémente l’interface I mais certaines méthodes qui doivent être déclarées dans les classes dérivées sont dans la classe C déclarées abstraites et donc la classe C est elle-même abstraite. On crée ensuite des classes C1 et C2 dérivées de C qui implémentent chacune à leur manière les méthodes non définies (abstraites) de leur classe parent C ;
- on crée un trait T quasi identique à la classe abstraite C de la solution précédente. Ce trait n’implémente pas l’interface I car syntaxiquement elle ne le peut pas. On crée ensuite des classes C1 et C2 implémentant l’interface I et utilisant le trait T. Il ne reste à ces classes qu’à implémenter les méthodes de l’interface I non implémentées par le trait T qu’elles importent ;
Pour l’exemple, nous allons utiliser ici un trait [TraitDao].
Le trait [TraitDao]¶
Le code du trait [TraitDao] est le suivant [TraitDao.php] :
1 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 | <?php
// espace de noms
namespace Application;
trait TraitDao {
// lecture des données contribuables
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// tableau des données contribuables
$taxPayersData = [];
// tableau des erreurs
$errors = [];
// pas mal d'erreurs peuvent se produire dès qu'on gère des fichiers
try {
// lecture des données utilisateur
// chaque ligne a la forme statut marital, nombre d'enfants, salaire annuel
$taxPayersFile = fopen($taxPayersFilename, "r");
if (!$taxPayersFile) {
throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
}
// on exploite la ligne courante du fichier des données utilisateur
// qui a la forme statut marital, nombre d'enfants, salaire annuel
$num = 1; // n° ligne courante
$nbErreurs = 0; // nbre d'erreurs rencontrées
while ($ligne = fgets($taxPayersFile, 100)) {
// on néglige les lignes vides
$ligne = trim($ligne);
if (strlen($ligne) == 0) {
// ligne suivante
$num++;
// on reboucle
continue;
}
// on enlève l'éventuelle marque de fin de ligne
$ligne = Utilitaires::cutNewLineChar($ligne);
// on récupère les 3 champs marié:enfants:salaire qui forment $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// on les vérifie
// le statut marital doit être oui ou non
$marié = trim(strtolower($marié));
$erreur = ($marié !== "oui" and $marié !== "non");
if (!$erreur) {
// le nombre d'enfants doit être un entier
$enfants = trim($enfants);
if (!preg_match("/^\d+$/", $enfants)) {
$erreur = TRUE;
} else {
$enfants = (int) $enfants;
}
}
if (!$erreur) {
// le salaire est un entier sans les centimes d'euros
$salaire = trim($salaire);
if (!preg_match("/^\d+$/", $salaire)) {
$erreur = TRUE;
} else {
$salaire = (int) $salaire;
}
}
// erreur ?
if ($erreur) {
$errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
$nbErreurs++;
} else {
// on mémorise les informations
$taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
}
// ligne suivante
$num++;
}
// est-on à la fin du fichier ?
if (!feof($taxPayersFile)) {
// on est sorti de la boucle sur une erreur de lecture
throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
} else {
// on est sorti de la boucle sur la marque de fin de fichier
// on sauve les erreurs dans un fichier texte
$this->saveString($errorsFilename, implode("\n", $errors));
// résultat de la fonction
return $taxPayersData;
}
} finally {
// on ferme le fichier s'il est ouvert
if ($taxPayersFile) {
fclose($taxPayersFile);
}
}
}
// enregistrement des résultats
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// enregistrement du tableau [$taxPayersData] dans le fichier texte [$resultsFileName]
// si le fichier texte [$resultsFileName] n'existe pas, il est créé
$this->saveString($resultsFilename, implode("\n", $taxPayersData));
}
// enregistrement d'es résultats d'un tableau dans un fichier texte
private function saveString(string $fileName, string $data): void {
// enregistrement du tableau [$data] dans le fichier texte [$fileName]
// si le fichier texte [$fileName] n'existe pas, il est créé
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
}
}
}
|
Commentaires
- ligne 6 : nous définissons ici un trait et non une classe ;
- lignes 9-89 : la méthode [getTaxPayersData] implémente la méthode de même nom de l’interface [InterfaceDao]. Elle récupère dans un fichier texte nommé [$taxPayersFilename] les données des contribuables [statut marital, nombre d’enfants, salaire annuel]. Elle rend celles-ci sous la forme d’un tableau [$taxPayersData] d’éléments de type [TaxPayerData] (lignes 67, 81) ;
- la méthode [getTaxPayersData] est très semblable à la méthode
[AbstractBaseImpots::executeBatchImpots] décrite au paragraphe
lien avec les différences
suivantes :
- la méthode [getTaxPayersData] ne fait que récupérer les données des contribuables. Elle ne fait pas de calcul d’impôt. Ici c’est le rôle de la couche [métier] ;
- comme le faisait la méthode [executeBatchImpots] elle signale les erreurs. Ici les erreurs sont d’abord mémorisées dans un tableau [$errors] (ligne 13), tableau qui est mémorisé dans un fichier texte à la fin du traitement (ligne 79). Selon les cas, il est vide ou non ;
- dans le cas d’erreur irrécupérable, une exception de type [ExceptionImpots] est lancée (lignes 20, 75) ;
- ligne 73 : on notera le traitement fait à la sortie de la boucle des lignes 26-71. En effet la fonction [fgets] a l’inconvénient de rendre le booléen FALSE aussi bien lorsque la lecture des lignes a rencontré la marque de fin de fichier que si cette lecture n’a pu aboutir à cause d’une erreur. Pour différentier les deux cas, on teste si on est rendu à la fin du fichier avec la fonction [feof]. Si on n’est pas rendu à la fin du fichier, c’est qu’une erreur s’est produite et on lance alors une exception ;
- lignes 83-88 : le [finally] est exécuté qu’il y ait eu exception ou pas lors de l’exploitation du fichier ;
- ligne 85 : si le fichier a été ouvert, alors le ‘handle’ [$taxPayersFile] du fichier a la valeur booléenne TRUE, FALSE sinon ;
- lignes 99-105 : la méthode privée [saveString] utilisée ligne 79 pour enregistrer le tableau des erreurs dans un fichier texte ;
- ligne 99 : la méthode [saveString] reçoit deux paramètres :
- [string $filename] qui est le nom du fichier texte utilisé pour enregistrer les données ;
- [string $data] qui est la chaîne de caractères à enregistrer dans le fichier texte. Cette chaîne sera un ensemble de lignes terminée par le caractère de fin de ligne \n ;
- ligne 102 : la fonction PHP [file_puts_contents] enregistre une chaîne de caractères dans un fichier texte. Elle s’occupe d’ouvrir le fichier, d’écrire la chaîne dedans et de fermer le fichier. Elle rend le booléen FALSE si une erreur s’est produite ;
- ligne 103 : si une erreur se produit, on lance une exception ;
- lignes 92-96 : implémentation de la méthode [saveResults] de l’interface [InterfaceDao]. On utilise de nouveau la méthode privée [saveString]. Ici le second paramètre de [saveString] est une chaîne construite à partir du tableau [$taxPayersData] dont les éléments sont de type [TaxPayerData]. On peut se demander quel va être le résultat de l’opération :
1 | implode("\n", $taxPayersData)
|
Nous avons défini dans la classe [TaxPayerData] (paragraphe lien) la méthode [__toString] suivante :
- public function __toString() {
- // chaîne Json de l’objet
- return \json_encode(\get_object_vars($this),
- JSON_UNESCAPED_UNICODE);
- }
L’opération
1 | implode("\n", $taxPayersData)
|
va concaténer chaque élément du tableau [$taxPayersData] transformé en chaîne de caractères par sa méthode [__toString] avec la marque de fin de ligne \n. Cela va donner une chaîne de caractères de la forme :
json1njson2n…
Conclusion
Le trait [TraitDao] a implémenté deux des méthodes de l’interface [InterfaceDao], [getTaxPayersData] et [saveResults] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php
// espace de noms
namespace Application;
interface InterfaceDao {
// lecture des données contribuables
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// lecture des données de l'administration fiscale (tranches d'impôts)
public function getTaxAdminData(): TaxAdminData;
// enregistrement des résultats
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
|
Il nous reste à implémenter la méthode [getTaxAdminData] qui récupère les données de l’administration fiscale.
La classe [ImpotsWithTaxAdminDataInJsonFile]¶
La classe [ImpotsWithTaxAdminDataInJsonFile] implémente l’interface [InterfaceDao] de la façon suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php
// espace de noms
namespace Application;
// définition d'une classe ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
// usage d'un trait
use TraitDao;
// l'objet de type TaxAdminData qui contient les données des tranches d'impôts
private $taxAdminData;
// le constructeur
public function __construct(string $taxAdminDataFilename) {
// on veut initialiser l'attribut [$this->taxAdminData]
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// retourne les données permettant le calcul de l'impôt
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
|
Commentaires
- ligne 7 : la classe [ImpotsWithTaxAdminDataInJsonFile] implémente l’interface [InterfaceDao] ;
- ligne 9 : la classe [ImpotsWithTaxAdminDataInJsonFile] utilise le trait [traitDao] qui on le sait implémente les méthodes [getTaxPayersData] et [saveResults] de l’interface [InterfaceDao]. Il ne reste donc plus, à la classe [ImpotsWithTaxAdminDataInJsonFile] qu’à implémenter la méthode [getTaxAdminData] qui récupère les données de l’administration fiscale ;
- ligne 11 : l’attribut de type [TaxAdminData] que rend la méthode [getTaxAdminData] des lignes 20-22. Cet attribut est initialisé par le constructeur des lignes 14-17 ;
Nous en avons terminé avec la couche [dao] de notre application : nous avons une classe qui implémente totalement l’interface [InterfaceDao] que nous nous sommes imposés. Nous pouvons désormais passer à la couche[métier].
La couche [métier]¶
Nous allons maintenant implémenter la couche [2] de notre architecture :
L’interface [InterfaceMétier]¶
L’interface de la couche [métier] sera la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php
// espace de noms
namespace Application;
interface InterfaceMetier {
// 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;
}
|
Commentaires
ligne 9 : l’interface [InterfaceMétier] sait calculer le montant de l’impôt d’un contribuable particulier pourvu qu’on lui donne les informations suivantes : statut marital, nombre d’enfants, salaire annuel. La méthode [calculerImpot] n’utilise pas la couche [dao] aussi ne lance-t-elle pas d’exceptions ;
ligne 9 : l’interface [InterfaceMétier] peut aussi calculer le montant de l’impôt d’un ensemble de contribuables dont les données sont rassemblées dans le fichier texte nommé [$taxPayersFileName]. Elle met les résultats dans un fichier texte nommé [$resultsFileName]. La méthode [executeBatchImpots] doit s’adresser à la couche [dao] qui s’occupe des accès au système de fichiers. Des exceptions peuvent alors remonter de la couche [dao] que la méthode [executeBatchImpots] n’interceptera pas : elle les laissera remonter au script principal. Les erreurs non fatales sont enregistrées dans le fichier texte nommé [$errorsFileName] ;
ligne 9 : la méthode [calculerImpot] est une méthode purement [métier]. Elle ne se préoccupe pas d’où viennent les données qu’elle utilise ;
ligne 12 : la méthode [executeBatchImpots] va s’adresser à la couche [dao] pour lire et écrire des données dans des fichiers texte. Elle va appeler de façon répétée la méthode métier [calculerImpot] ;
La classe [Metier]
La classe [Metier] implémente l’interface [InterfaceMetier] de la façon suivante :
1 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 | <?php
// espace de noms
namespace Application;
class Metier implements InterfaceMetier {
// couche Dao
private $dao;
// données administration fiscale
private $taxAdminData;
//---------------------------------------------
// setter couche [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $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);
}
// calcul des impôts en mode batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
…
// enregistrement des résultats
$this->dao->saveResults($resultsFileName, $results);
}
}
|
Commentaires
- ligne 6 : la classe [Metier] implémente l’interface [InterfaceMetier], ç-à-d les méthodes [calculerImpot] (lignes 30-34) et [executeBatchImpots] (lignes 66-70) ;
- ligne 8 : une référence sur la couche [dao]. Il en faut obligatoirement une pour que la couche [métier] sache à qui s’adresser lorsqu’elle veut des données externes. Cet attribut sera initialisé via le setter des lignes 14-17 ou via le constructeur des lignes 19-26 ;
- ligne 10 : l’objet de type [TaxAdminData] qui encapsule les données de l’administration fiscale. Ces données sont nécessaires à la méthode métier [calculerImpot]. Cet attribut est initialisé via le constructeur des lignes 19-26 ;
- lignes 19-26 : le constructeur initialise les deux attributs de la
classe :
- l’attribut [$dao] est initialisé avec la référence passée en paramètre au constructeur. On notera que le type de ce paramètre est celui de l’interface [InterfaceDao] permettant ainsi à la classe [Metier] d’être initialisée par n’importe quelle classe implémentant cette interface ;
- l’attribut [$taxAdminData] est initialisé en faisant appel à la méthode [getTaxAdminData] de la couche [dao] ;
On en conclut que lorsque les méthodes [calculerImpots] et [executeBatchImpots] s’exécutent, les deux attributs [$dao] et [$taxAdminData] sont initialisés.
La méthode [calculerImpots] est la suivante :
1 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 | public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié : oui, non
// $enfants : nombre d'enfants
// $salaire : salaire annuel
// $this->taxAdminData : données de l'administration fiscale
//
// on vérifie qu'on a bien les données de l'administration fiscale
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// calcul de l'impôt avec enfants
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// calcul de l'impôt sans les enfants
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// application du plafonnement du quotient familial
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// on prend l'impôt le plus fort
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// calcul d'une éventuelle décôte
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// calcul d'une éventuelle réduction d'impôts
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// résultat
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
|
Commentaires
- ce code est celui de la méthode [AbstractBaseImpots::calculerImpot] de la version 3, expliquée au paragraphe lien. Il en est de même pour les méthodes privées [calculerImpot2, getDecôte, getRéduction, getRevenuImposable] ;
La méthode [Metier::executeBatchImpots] est la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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->dao->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->setMontant($result["impôt"]);
$taxPayerData->setDécôte($result["décôte"]);
$taxPayerData->setSurCôte($result["surcôte"]);
$taxPayerData->setTaux($result["taux"]);
$taxPayerData->setRéduction($result["réduction"]);
// on met le résultat dans le tableau des résultats
$results [] = $taxPayerData;
}
// enregistrement des résultats
$this->dao->saveResults($resultsFileName, $results);
}
|
Commentaires
- ligne 1 : la méthode doit appeler de façon répétée la méthode [calculerImpot] pour chacun des contribuables trouvés dans le fichier texte nommé [$taxPayersFileName]. Elle doit mettre les résultats dans le fichier texte nommé [$resultsFileName]. Les erreurs non fatales rencontrées sont enregistrées dans dans le fichier texte nommé [$errorsFileName]. La méthode ne lance pas d’exceptions elle-même mais laisse remonter celles que la couche [dao] lance ;
- ligne 4 : les données des contribuables sont demandées à la couche [dao]. Celle-ci renvoie un tableau d’élements de type [TaxPayerData] qui est une classe d’attributs [marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux] (cf paragraphe lien). S’il se produit une exception ici, comme elle n’est pas interceptée par un catch, elle remontera automatiquement au code appelant. Cela signifie qu’en cas d’exception, la ligne 6 n’est pas exécutée ;
- ligne 6 : le tableau des résultats de type [TaxPayerData] ;
- lignes 8-22 : on calcule l’impôt pour chacun des éléments du tableau des contribuables [$taxPayersData]. Pour cela, on fait appel à la méthode interne [calculerImpot] (ligne 10) ;
- lignes 15-19 : le résultat obtenu est utilisé pour initialiser les attributs de [TaxPayerData] qui ne l’étaient pas encore ;
- ligne 21 : le résultat obtenu est cumulé dans le tableau des résultats [$results] ;
- ligne 24 : une fois l’impôt calculé pour tous les contribuables, les résultats sont mémorisés dans un fichier texte. C’est la couche [dao] qui fait ce travail ;
Conclusion
En général la couche [métier] est assez simple à écrire car elle s’adresse à la couche [dao] qui, elle, gère l’accès aux données avec la gestion des erreurs qui va avec.
Le script principal¶
On écrit maintenant le script de la couche [3] de notre architecture :
Le script principal est le suivant [main.php] :
1 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 | <?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");
// inclusion interface et classes
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// définition des constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// création de la couche [métier]
$métier = new Metier($dao);
// calcul de l'impôts en mode batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;
|
Commentaires
- ligne 24 : le nom du fichier des données contribuables ;
- ligne 25 : le nom du fichier des résultats ;
- ligne 26 : le nom du fichier des erreurs ;
- ligne 27 : le nom du fichier jSON contenant les données de l’administration fiscale;
- ligne 31 : création de la couche [dao] ;
- ligne 33 : création de la couche [métier] s’appuyant sur cette couche [dao] ;
- ligne 35 : exécution de la méthode [executeBatchImpots] de la couche [métier] ;
- lignes 36-39 : on a vu que la couche [métier] pouvait remonter des exceptions. Elles sont interceptées ici ;
Tests visuels¶
Test n° 1¶
Avec le fichier des contribuables [taxpayersdata.txt] suivants :
1 2 3 4 5 6 7 8 9 10 11 | oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000
|
on obtient le fichier des erreurs [errors.txt] suivant :
1 2 | la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
|
et le fichier des résultats [resultats.txt] suivant :
1 2 3 4 5 6 7 8 9 | {"marié":"oui","enfants":2,"salaire":55555,"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"oui","enfants":2,"salaire":50000,"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":3,"salaire":50000,"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
{"marié":"non","enfants":2,"salaire":100000,"impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":3,"salaire":100000,"impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"marié":"non","enfants":0,"salaire":100000,"impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":2,"salaire":30000,"impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}
{"marié":"non","enfants":0,"salaire":200000,"impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}
{"marié":"oui","enfants":3,"salaire":200000,"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}
|
Test n° 2¶
Dans le script principal, on met pour le fichier des contribuables un nom de fichier qui n’existe pas :
1 | const TAXPAYERS_DATA_FILENAME = "taxpayersdata2.txt";
|
Les résultats obtenus à la console alors sont les suivants :
1 2 3 4 | Warning: fopen(taxpayersdata2.txt): failed to open stream: No such file or directory in C:\Data\st-2019\dev\php7\poly\scripts-console\impots\version-04\TraitDao.php on line 18
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
Done.
|
- ligne 1 : avertissements (warning) de l’interpréteur PHP ;
- ligne 2 : le message d’erreur de l’exception lancée par la couche [dao] ;
Il est possible de mettre en sourdine les messages d’erreur de l’interpréteur PHP :
La ligne 21 du code ci-dessus demande à ce que les erreurs PHP ne soient pas affichées. Pendant la phase de développement il est nécessaire qu’elles soient affichées. En mode production, il faut les cacher.
Les résultats de l’exécution sont alors les suivants :
1 2 | Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
|
Tests [Codeception]¶
Les tests visuels sont très insuffisants :
- on se limite en général à quelques tests ;
- on est plus ou moins attentif lors de cette vérification visuelle et des détails peuvent nous échapper ;
Dans la réalité du développement professionnel, les tests sont rédigés par des personnes dédiées dont c’est le rôle principal. Elles cherchent alors à faire les tests les plus complets possible(s). Pour cela elles utilisent des frameworks de test.
Nous allons ici utiliser le framework Codeception [https://codeception.com/] car il peut être intégré à Netbeans. C’est un framework avec un large éventail de possibilités. Nous n’allons en utiliser que quelques-unes. L’idée est d’avoir un moyen rapide, après chaque nouvelle version de l’exercice d’application, de vérifier que celle-ci fonctionne. L’existence de tests réussis donne au développeur confiance dans le code qu’il a écrit. C’est un facteur important.
Installation du framework [Codeception]¶
Comme beaucoup de bibliothèques PHP, le framework [Codeception] s’installe avec [Composer]. Nous ouvrons donc un terminal Laragon (cf paragraphe lien).
Il nous faut tout d’abord installer le framework de tests PHPUnit [https://phpunit.de/]. En effet Codeception utilise en sous-main le framework PHPUnit:
Ensuite, nous installons le framework Codeception :
C’est tout. Maintenant voyons l’intégration de [Codeception] dans Netbeans.
Intégration de [CodeCeption] dans Netbeans¶
- en [1-2], on accède aux propriétés du projet ;
- en [3-4], on fait de [Codeception] l’un des frameworks de test du projet ;
- en [5-8], on initialise le framework [Codeception] pour le projet ;
- en [9], un dossier [tests] a été créé, ainsi qu’un fichier de configuration [codeception.yml] en [10-11]. Le fichier [11] est le même que le fichier [10]. Codeception a simplement créé un dossier [Important Files] pour donner une signification particulière au fichier [10] ;
- en [12-13], on revient aux propriétés du projet ;
en [14-16], on désigne le dossier [tests] [16], comme le dossier de tests du projet ;
en [16], le dossier [tests] apparaît alors sous le nouveau nom [Test Files]. La présence de ce dossier dans un projet PHP montre que ce projet intègre un framework de tests programmés ;
nous créerons nos tests dans le dossier [unit] [17] ;
Tests de la couche [dao]
- nous allons créer tous nos tests dans le dossier [unit] [1] ;
- les noms des classes de test [Codeception] doivent se terminer par le mot clé [Test], sinon les classes ne seront pas reconnues comme classes de test ;
Nos classes de test [Codeception] auront la forme suivante [https://codeception.com/docs/05-UnitTests] :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// chargement de l’environnement de test
…
class DaoTest extends \Codeception\Test\Unit {
// attributs du test
private $attribut1;
public function __construct() {
parent::__construct();
// initialisation de l’environnement de test
…
}
// tests
public function testTaxAdminData() {
// tests
$this->assertEquals($expected, $actual);
$this->assertEqualsWithDelta($expected, $actual, $delta);
$this->assertTrue($actual);
$this->assertFalse($actual);
$this->assertNull($actual);
$this->assertEmpty($actual);
$this→assertSame($expected, $actual);
…
}
}
|
Commentaires
- ligne 7 : les classes de test seront dans le même espace de noms que l’application testée ;
- lignes 9-10 : ici on trouvera les opérations [require] pour charger les classes et interfaces testées ;
- ligne 12 : le nom de la classe de test doit obligatoirement se terminer par le mot clé [Test]. Cette classe doit étendre la classe [CodeceptionTestUnit] ;
- lignes 16-20 : le constructeur nous permettra d’initialiser l’environnement du test ;
- ligne 23 : les noms des méthodes de test doivent obligatoirement commencer par le mot clé [test] ;
- lignes 25-31 : diverses méthodes de test peuvent être utilisées ;
La classe de test [DaoTest] sera la suivante :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// définition des constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
|
Commentaires
Pour construire les tests d’une version de l’exercice d’application, nous utiliserons un environnement identique à celui utilisé par le script principal de la version. Celui de la version 04 est le script [main.php] suivant :
1 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 | <?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");
// inclusion interface et classes
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// définition des constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// création de la couche [métier]
$métier = new Metier($dao);
// calcul de l'impôts en mode batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;
|
Pour tester la couche [dao], dans la classe de test :
- nous reprenons l’environnement des lignes 13-27 de [main.php] ;
- dans le constructeur de la classe de test, nous construisons la couche [dao] comme en ligne 31 ;
- nous écrivons les méthodes de tests;
Nous procéderons de cette façon pour toutes les classes de test.
Revenons au code complet de la classe de test :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// définition des constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
// constantes de calcul
$this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
$this->assertEquals(21037, $this->taxAdminData->getPlafondRevenusCelibatairePourReduction());
$this->assertEquals(42074, $this->taxAdminData->getPlafondRevenusCouplePourReduction());
$this->assertEquals(3797, $this->taxAdminData->getValeurReducDemiPart());
$this->assertEquals(1196, $this->taxAdminData->getPlafondDecoteCelibataire());
$this->assertEquals(1970, $this->taxAdminData->getPlafondDecoteCouple());
$this->assertEquals(1595, $this->taxAdminData->getPlafondImpotCelibatairePourDecote());
$this->assertEquals(2627, $this->taxAdminData->getPlafondImpotCouplePourDecote());
$this->assertEquals(12502, $this->taxAdminData->getAbattementDixPourcentMax());
$this->assertEquals(437, $this->taxAdminData->getAbattementDixPourcentMin());
// tranches de l'impôt
$this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $this->taxAdminData->getLimites());
$this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $this->taxAdminData->getCoeffR());
$this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $this->taxAdminData->getCoeffN());
}
}
|
Commentaires
- lignes 10-25 : chargement de l’environnement nécessaire aux tests et définitions de constantes ;
- lignes 31-36 : construction de la couche [dao], ligne 34, puis initialisation de l’attribut [$taxAdminData] de la ligne 29. Cet attribut contient les données de l’administration fiscale ;
- lignes 39-55 : l’unique méthode de test. Celui-ci consiste à vérifier que le contenu de l’attribut [$taxAdminData] correspond à ce qui est attendu ;
- lignes 41-50 : vérifications des constantes du calcul de l’impôt ;
- lignes 52-55 : vérifications des tranches d’imposition. La méthode [assertSame] vérifie que deux entités PHP, ici des tableaux, sont identiques ;
Pour exécuter cette classe de test, on procède de la façon suivante :
- en [1-2], on exécute le test ;
- [3] : la fenêtre des résultats des tests ;
- [4] : la classe de test exécutée ;
- [5] : les résultats. Ici l’unique méthode de test a été réussie ;
- [6] : lorsque le test échoue ou plus fréquemment lorsqu’aucun test n’a été exécuté, il faut aller voir la fenêtre [6]. Le plus souvent, c’est le chargement de l’environnement du test qui a échoué et aucun test n’a alors pu être exécuté. Les erreurs affichées dans [6] sont celles qu’on aurait avec l’exécution d’un script PHP classique ;
Montrons un exemple de test erroné :
Dans la classe de test, nous introduisons une erreur dans la définition d’une constante :
// constantes
define(« ROOT », « C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x »);
puis nous exécutons le test. Le résultat obtenu est le suivant :
Dans la fenêtre [4] :
Tests de la couche [métier]¶
La classe de test [MetierTest] suit les mêmes règles de construction que la classe [DaoTest] mais il y a plus de méthodes de test :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// définition des constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class MetierTest extends \Codeception\Test\Unit {
// couche métier
private $métier;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
// création de la couche [métier]
$this->métier = new Metier($dao);
}
// tests
public function test1() {
$result = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test2() {
$result = $this->métier->calculerImpot("oui", 2, 50000);
$this->assertEqualsWithDelta(1385, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(384, $result["décôte"], 1);
$this->assertEqualsWithDelta(347, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test3() {
$result = $this->métier->calculerImpot("oui", 3, 50000);
$this->assertEqualsWithDelta(0, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(720, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test4() {
$result = $this->métier->calculerImpot("non", 2, 100000);
$this->assertEqualsWithDelta(19884, $result["impôt"], 1);
$this->assertEqualsWithDelta(4480, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test5() {
$result = $this->métier->calculerImpot("non", 3, 100000);
$this->assertEqualsWithDelta(16782, $result["impôt"], 1);
$this->assertEqualsWithDelta(7176, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test6() {
$result = $this->métier->calculerImpot("oui", 3, 100000);
$this->assertEqualsWithDelta(9200, $result["impôt"], 1);
$this->assertEqualsWithDelta(2180, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.3, $result["taux"]);
}
public function test7() {
$result = $this->métier->calculerImpot("oui", 5, 100000);
$this->assertEqualsWithDelta(4230, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test8() {
$result = $this->métier->calculerImpot("non", 0, 100000);
$this->assertEqualsWithDelta(22986, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test9() {
$result = $this->métier->calculerImpot("oui", 2, 30000);
$this->assertEqualsWithDelta(0, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0, $result["taux"]);
}
public function test10() {
$result = $this->métier->calculerImpot("non", 0, 200000);
$this->assertEqualsWithDelta(64210, $result["impôt"], 1);
$this->assertEqualsWithDelta(7498, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.45, $result["taux"]);
}
public function test11() {
$result = $this->métier->calculerImpot("oui", 3, 200000);
$this->assertEqualsWithDelta(42842, $result["impôt"], 1);
$this->assertEqualsWithDelta(17283, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
}
|
Commentaires
- lignes 10-25 : chargements des fichiers définissant l’environnement du test. Celui-ci est le même que pour la couche [dao] ;
- lignes 31-37 : instanciation des couches [dao] et [métier] ;
- lignes 40-47 : un test de calcul d’impôt ;
- ligne 41 : un certain calcul d’impôt est fait avec la couche [métier] ;
- lignes 42-46 : on vérifie que les résultats obtenus sont ceux du simulateur de l’administration fiscale [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm] ;
- lignes 23-26 : les tests d’égalité sont faits à 1 euro près. En effet, on a vu que des problèmes d’arrondi faisaient que l’algorithme du document donnait les résultats attendus à 1 euro près ;
- ligne 27 : le taux d’imposition est lui calculé sans marge d’erreur ;
- lignes 49-137 : on répète ce type de tests 10 fois avec à chaque fois une configuration du contribuable différente ;
Les tests donnent les résultats suivants :
Tests des prochaines versions¶
Dans la suite, les tests des couches [dao] et [métier] seront identiques à ceux de la version 04. Seul changera l’environnement du test. Nous ne présenterons donc que celui-ci et les résultats des tests.
Utilisation du SGBD MySQL¶
Nous allons maintenant écrire des scripts PHP utilisant une base de données MySQL :
Dans l’architecture ci-dessus, le script PHP (1) ne dialogue pas directement avec le SGBD (Système de Gestion de Bases de Données) (3). Il dialogue avec un intermédiaire appelé pilote de SGBD ou encore driver de SGBD. PHP fournit une interface standard pour ces pilotes, l’interface PDO (PHP Data Objects). Cette interface est implémentée par différentes classes adaptées à chaque SGBD : une classe pour le SGBD MySQL, une autre pour le SGBD PostgreSQL… Pour changer de SGBD, on change de pilote :
Le pilote PDO isole le script PHP (1) du SGBD (3, 6). Comme ces pilotes implémentent une interface standard, on peut s’attendre à ce que le script PHP (1) ne change pas si on passe du SGBD MySQL (3) au SGBD PostgreSQL (6). Dans la réalité cet idéal n’existe pas. En effet pour dialoguer avec le SGBD, le script PHP envoie des ordres SQL (Standard Query Language). C’est un langage implémenté par tous les SGBD mais qui est incomplet. Aussi les SGBD lui ont-ils ajouté des ordres propriétaires. C’est une première cause d’incompatibilité entre SGBD. Par ailleurs les types de données utilisables dans les bases de données peuvent être différents d’un SGBD à l’autre. Ainsi PostgreSQL accepte un nombre de types de données bien plus grand que le SGBD MySQL. C’est une seconde cause d’incompatibilité. Une autre cause est la gestion des clés primaires automatiques (générées par le SGBD) : quasiment chaque SGBD a sa propre politique. Etc… Les causes d’incompatibilité sont nombreuses.
Si on veut éviter de réécrire le script PHP (1) en passant de MySQL (3) à PostgreSQL (6), on est généralement amenés à insérer une nouvelle couche entre le script PHP (1) et le pilote PDO (2, 5) dont le rôle sera de gommer les incompatibilités entre les deux SGBD. Cependant dans les cas simples que nous allons rencontrer, cette couche supplémentaire ne sera pas nécessaire.
Nous allons maintenant utiliser le SGBD MySQL. Celui-ci est inclus dans le paquetage Laragon (cf paragraphe lien).
Si le lecteur est novice avec la notion de base de données et de langage SQL, il pourra lire le document [http://sergetahe.com/cours-tutoriels-de-programmation/cours-tutoriel-sql-avec-le-sgbd-firebird/]. Ce document utilise le SGBD Firebird et non pas MySQL mais donne les fondamentaux des bases de données et du langage SQL. Comme MySQL, Firebird dispose d’une version librement utilisable et de faible empreinte mémoire.
Création d’une base de données¶
Nous montrons maintenant comment créer une base de données ainsi qu’un utilisateur MySQL avec l’outil Laragon.
- une fois lancé, Laragon [1] peut être administré à partir d’un menu [2] ;
- en [3-5], on installe l’outil [phpMyAdmin] d’administration de MySQL s’il n’a pas déjà été installé ;
- en [6], on démarre le serveur web Apache ainsi que le SGBD MySQL ;
- en [7], le serveur Apache est lancé ;
- en [8], le SBD MySQL est lancé ;
- en [8-10], on crée une base de données qu’on nomme [dbpersonnes] [11]. On va construire une base de données de personnes ;
- en [11], on va gérer la base de données qu’on vient de créer ;
- l’opération [Bases de données] émet une requête web vers l’URL [http://localhost/phpmyadmin]. C’est le serveur web Apache de Laragon qui répond. L’URL [http://localhost/phpmyadmin] est l’URL de l’utilitaire [phpMyAdmin] que nous avons installé précédemment [5]. Cet utilitaire permet de gérer les bases de données MySQL ;
- par défaut, les identifiants de connexion de l’administrateur de la base sont : root [13] sans mot de passe [14] ;
- en [16], la base de données que nous avons créée précédemment ;
- on a pour l’instant une base [dbpersonnes] [17] qui est vide [18] ;
On crée un utilisateur [admpersonnes] avec le mot de passe [nobody] qui va avoir tous les droits sur la base de données [dbpersonnes] :
- en [19], on est positionnés sur la base [dbpersonnes] ;
- en [20], on sélectionne l’onglet [Privileges] ;
- en [21-22], on voit que l’utilisateur [root] a tous les droits sur la base [dbpersonnes] ;
- en [23], on crée un nouvel utilisateur ;
- en [25-26], l’utilisateur aura l’identifiant [admdbpersonnes] ;
- en [27-29], son mot de passe sera [nobody] ;
- en [30], phpMyAdmin signale que le mot de passe est très faible (facile à craquer). En production, il est préférable de générer un mot de passe fort avec [31] ;
- en [32], on indique que l’utilisateur [admdbpersonnes] doit avoir tous les droits sur la base [dbpersonnes] ;
- en [33], on valide les renseignements donnés ;
- en [35], phpMyAdmin indique que l’utilisateur a été créé ;
- en [36], l’ordre SQL qui a été émis sur la base ;
- en [37], l’utilisateur [admpersonnes] a tous les droits sur la base de données [dbpersonnes] ;
Désormais nous avons :
- une base de données MySQL [dbpersonnes] ;
- un utilisateur [admpersonnes/nobody] qui a tous les droits sur cette base de données ;
Nous allons écrire des scripts PHP pour exploiter la base de données. PHP dispose de diverses bibliothèques pour gérer les bases de données. Nous utiliserons la bibliothèque PDO (PHP Data Objects) qui s’intercale entre le code PHP et le SGBD :
La bibliothèque PDO permet au script PHP de s’abstraire de la nature exacte du SGBD utilisé. Ainsi ci-dessus, le SGBD MySQL peut être remplacé par le SGBD PostgreSQL avec un impact minimum sur le code du script PHP. Cette bibliothèque n’est pas disponible par défaut. On peut vérifier sa disponibilité de la façon suivante :
- en [1-4], on vérifie les extensions PDO actives ;
- en [5], on voit que l’extension PDO pour le SGBD MySQL est active. Les autres ne le sont pas. Il suffirait de les cliquer pour les activer ;
Une autre façon d’activer une extension est de modifier directement le fichier [php.ini] (paragraphe lien) qui configure PHP :
- en [1], l’extension PDO de MySQL est activée ;
- en [2], l’extension PDO de Firebird est désactivée ;
Après avoir modifié le fichier [php.ini], il faut relancer le PHP de Laragon pour que les modifications soient prises en compte.
Connexion à une base de données MySQL¶
La connexion à un SGBD se fait par la construction d’un objet PDO. Le constructeur admet différents paramètres :
1 | $dbh=new PDO(string $dsn,string $user,string $passwd,array $driver_options)
|
La signification des paramètres est la suivante :
$dsn (Data Source Name) est une chaîne précisant la nature du SGBD et sa localisation sur internet. La chaîne « mysql:host=localhost » indique qu’on a affaire à un SGBD MySQL opérant sur le serveur local. Cette chaîne peut comprendre d’autres paramètres, notamment le port d’écoute du SGBD et le nom de la base à laquelle on veut se connecter : « mysql:host=localhost:port=3306:dbname=dbpersonnes » ;
$user identifiant de l’utilisateur qui se connecte ;
$passwd son mot de passe ;
$driver_options un tableau d’options pour le pilote du SGBD ;
Seul le premier paramètre est obligatoire. L’objet ainsi construit sera ensuite le support de toutes les opérations faites sur la base de données à laquelle on s’est connecté. Si l’objet PDO n’a pu être construit, une exception de type PDOException est lancée.
Voici un exemple de connexion [mysql-01.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php
// connexion à une base MySql locale
// l'identité de l'utilisateur est (admpersonnes,nobody)
const ID = "admpersonnes";
const PWD = "nobody";
const HOTE = "localhost";
try {
// connexion
$dbh = new PDO("mysql:host=".HOTE, ID, PWD);
print "Connexion réussie\n";
// fermeture de la connexion
$dbh = NULL;
} catch (PDOException $e) {
print "Erreur : " . $e->getMessage() . "\n";
exit();
}
|
Résultats :
1 | Connexion réussie
|
Commentaires
- ligne 11 : la connexion à un SGBD se fait par la construction d’un objet PDO. Le constructeur est ici utilisé avec les paramètres suivants :
- une chaîne précisant la nature du SGBD et sa localisation sur
- internet. La chaîne « mysql:host=localhost » indique qu’on a affaire à un SGBD MySQL opérant sur le serveur local. Le port n’a pas été précisé. Le port 3306 est alors utilisé par défaut. Le nom de la base de données n’est pas indiqué non plus. Il y aura alors connexion au SGBD MySQL, la sélection d’une base précise se faisant plus tard ;
- un identifiant d’utilisateur ;
- son mot de passe ;
- ligne 14 : la fermeture de la connexion se fait par suppression de l’objet PDO créé initialement ;
- ligne 15 : la connexion à un SGBD peut échouer. Dans ce cas, une exception de type PDOException est lancée. Celle-ci dérive de l’exception PHP [RuntimeException] ;
- ligne 16 : on affiche le message d’erreur de l’exception ;
Réexécutons le script en mettant ligne 6 un mot de passe erroné. Le résultat est alors le suivant :
1 | Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes'@'localhost' (using password: YES)
|
Création d’une table¶
Le script [mysql-02.php] montre la création d’une table dans une base de données :
1 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 | <?php
// identité de la base de données
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// identifiants de l'utilisateur
const ID = "admpersonnes";
const PWD = "nobody";
try {
// connexion à la base MySql
$connexion = new PDO(DSN, ID, PWD);
// suppression de la table personnes si elle existe
$sql = "drop table personnes";
$connexion->exec($sql);
// création de la table personnes
$sql = "create table personnes (prenom varchar(30) NOT NULL, nom varchar(30) NOT NULL, age integer NOT NULL, primary key(nom,prenom))";
$connexion->exec($sql);
} catch (PDOException $ex) {
// affichage erreur
print "Erreur : " . $ex->getMessage() . "\n";
} finally {
// on se déconnecte si besoin est
$connexion = NULL;
}
// fin
print "Terminé\n";
exit;
|
Commentaires
- ligne 11 : connexion à la base de données. C’est toujours la 1re chose à faire. Le résultat de la connexion est un objet [PDO] au travers duquel vont prendre les opérations sur la base de données ;
- ligne 13 : l’ordre SQL [drop table personnes] va supprimer la table [personnes] de la base de données [dbpersonnes]. Si la table [personnes] n’existe pas, cela ne provoque pas d’erreur ;
- ligne 14 : exécution de l’ordre SQL précédent sur la base de données [dbpersonnes]. Cet exécution peut lancer une [PDOException] qui sera interceptée ligne 18 ;
- ligne 16 : cet ordre SQL crée une table [personnes]. Une table
contient des lignes et des colonnes. Les colonnes forment ce qu’on
appelle la structure de la table. Les lignes forment le
contenu de la table. Une base de données peut contenir une ou
plusieurs tables. La table [personnes] aura ici trois colonnes :
- prenom : le prénom d’une personne sous la forme d’une chaîne d’au plus 30 caractères ;
- nom : le nom de cette même personne sous la forme d’une chaîne d’au plus 30 caractères ;
- age : l’âge de la personne sous la forme d’un entier ;
- l’attribut NOT NULL sur une colonne impose que la colonne ait une valeur. Ne pas lui en donner provoque une [PDOException] ;
- [primary key(nom,prenom)] fixe une clé primaire à la table [personnes]. Une clé primaire a une valeur unique pour chaque ligne de la table. Ici la clé primaire sera obtenue par concaténation des colonnes [nom] et [prenom] de la ligne. Cette contrainte fait que dans la table on ne pourra pas avoir deux personnes ayant les mêmes nom et prénom, donc deux homonymes. Créer un homonyme d’une personne dans la table provoque une [PDOException] ;
- ligne 17 : exécution de l’ordre SQL sur la base de données [dbpersonnes] ;
- ligne 20 : s’il se produit une [PDOException], on affiche le message d’erreur associé ;
- lignes 21-24 : on passe dans la clause [finally] dans tous les cas, exception ou pas, pour fermer la connexion à la base de données (ligne 23) ;
Résultats :
Si l’exécution du script se passe sans erreurs, on peut voir la présence de la table dans phpMyAdmin :
- en [3] la base de données ;
- en [4], la table présentée ;
- en [5], la structure des tables est présentée dans l’onglet [Structure] ;
- en [6-8], les trois colonnes de la table ;
- en [9], aucune des trois colonnes ne peut être vide ;
- en [10], la liste des index de la table. Un index permet de retrouver dans la table les lignes ayant tel index, plus vite que si on parcourait séquentiellement les lignes de la table. La clé primaire fait toujours partie des index mais un index peut ne pas être une clé primaire ;
- en [11], l’index est ici la clé primaire ;
- en [12], l’index est constitué des colonnes [nom, prenom] de chaque ligne ;
Maintenant, voyons ce qui se passe si on crée des erreurs, respectivement sur le nom de la base, le nom de l’utilisateur, son mot de passe :
Si on met un nom de base inexistante :
1 | Erreur : SQLSTATE[HY000] [1044] Access denied for user 'admpersonnes'@'%' to database 'dbpersonnes2'
|
Si on met un nom d’utilisateur inexistant :
1 | Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes2'@'localhost' (using password: YES)
|
Si on met un mot de passe erroné :
1 | Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes'@'localhost' (using password: YES)
|
Remplissage d’une table¶
Nous allons écrire un script PHP qui exécute des ordres SQL trouvés dans le fichier texte [creation.txt] suivant :
1 2 3 4 5 6 7 8 9 10 | drop table if exists personnes
SET NAMES 'utf8'
create table personnes (prenom varchar(30) not null, nom varchar(30) not null, age integer not null, primary key (nom,prenom))
insert into personnes (prenom, nom, age) values('Paul','Langevin',48)
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70)
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70)
insert into personnes (prenom, nom, age) values ('Pierre','Nicazou',35)
insert into personnes (prenom, nom, age) values ('Géraldine','Colou',26)
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56)
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56)
|
Commentaires
- le langage SQL (Structured Query Language) n’est pas sensible à la casse (majuscules, minuscules) des ordre SQL ;
- ligne 1 : on supprime la table [personnes] si elle existe ;
- ligne 2 : on indique au serveur MySQL qu’on va lui envoyer des caractères codés en UTF-8. Cet ordre SQL propre à MySQL est nécessaire ici par exemple pour avoir ligne 7, le é de Géraldine dans la base. Si on ne met pas la ligne 2, le é va être traduit en une suite de deux caractères étranges. Le client est le script PHP écrit sous Netbeans. Or celui-ci code les fichiers en UTF-8 [1-4] ci-dessous :
- ligne 3 : création de la table [personnes] avec les trois colonnes (prenom, nom, age) et la clé primaire (nom, prenom) ;
- lignes 4-10 : insertion de 7 lignes dans la table [personnes] ;
- ligne 6 : cet ordre d’insertion devrait échouer car il tente la même insertion que ligne 5. La contrainte de clé primaire devrait empêcher cette insertion : on ne peut avoir deux personnes ayant mêmes nom et prénom ;
- ligne 10 : cet ordre d’insertion devrait échouer car il tente la même insertion que ligne 9 ;
Le script PHP chargé d’exécuter les ordres SQL de ce fichier texte est le suivant [mysql-03.php] :
1 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 | <?php
// identité de la base de données
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// identifiants de l'utilisateur
const ID = "admpersonnes";
const PWD = "nobody";
// identité du fichier texte des commandes SQL à exécuter
const SQL_COMMANDS_FILENAME = "creation.txt";
// ouverture connexion à la base MySql
try {
$connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
// affichage erreur
print "Erreur : " . $ex->getMessage() . "\n";
exit;
}
// on veut qu'à chaque erreur de SGBD, une exception soit lancée
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// exécution du fichier d'ordres SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE, FALSE);
// fermeture connexion
$connexion = NULL;
//affichage nombre d'erreurs
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
print "$erreurs[$i]\n";
}
// c'est fini
print "Terminé\n";
exit;
// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE, bool $arrêt = TRUE): array {
// utilise la connexion $connexion
// exécute les commandes SQL contenues dans le fichier texte SQLFileName
// ce fichier est un fichier de commandes SQL à exécuter à raison d'une par ligne
// si $suivi=1 alors chaque exécution d'un ordre SQL fait l'objet d'un affichage indiquant sa réussite ou son échec
// si $arrêt=1, la fonction s'arrête sur la 1re erreur rencontrée sinon elle exécute ttes les commandes sql
// la fonction rend un tableau (nb d'erreurs, erreur1, erreur2…)
// on vérifie la présence du fichier SQLFileName
if (!file_exists($SQLFileName)) {
return ["Le fichier [$SQLFileName] n'existe pas"];
}
// exécution des requêtes SQL contenues dans SQLFileName
// on les met dans un tableau
$requêtes = file($SQLFileName);
// erreur ?
if ($requêtes === FALSE) {
return ["Erreur lors de l'exploitation du fichier SQL [$SQLFileName]"];
}
// on exécute les requêtes une par une - au départ pas d'erreurs
$erreurs = [];
$i = 0;
$fini = FALSE;
while ($i < count($requêtes) && !$fini) {
// on récupère le texte de la requête
// trim va enlever la marque de fin de ligne
$requête = trim($requêtes[$i]);
// requête vide ?
if (strlen($requête) == 0) {
// on ignore la requête et on passe à la requête suivante
$i++;
continue;
}
try {
// exécution de la requête - une exception peut être lancée
$connexion->exec($requête);
// suivi écran ou non ?
if ($suivi) {
print "$requête : Exécution réussie\n";
}
} catch (PDOException $ex) {
// il s'est produit une erreur
addError($erreurs, $requête, $ex->getMessage(), $suivi);
// est-ce qu'on s'arrête ?
$fini = $arrêt;
}
// requête suivante
$i++;
}
// résultat
return $erreurs;
}
function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
// on ajoute un msg d'erreur
$msg = "$requête : Erreur (" . $msg . ")";
$erreurs[] = $msg;
// suivi écran ou non ?
if ($suivi) {
print "$msg\n";
}
}
|
Commentaires
- la fonction [exécuterCommandes] (lignes 36-89) est chargée d’exécuter les commandes SQL qu’elle trouve dans le fichier texte [$SQLFileName] (paramètre 2). Pour les exécuter elle utilise la connexion ouverte [$connexion] (paramètre 1) avec le serveur MySQL. Le troisième paramètre [$suivi] est un booléen qui contrôle les affichages écran : à TRUE, l’ordre SQL exécuté est affiché à l’écran avec sa réussite ou son échec, sinon l’exécution de l’ordre SQL est silencieux. Le quatrième paramètre [$arrêt] contrôle ce qu’il faut faire lorsq’une commande SQL échoue : à TRUE, il indique que l’exécution des commandes SQL doit s’arrêter, sinon celle-ci continue. La fonction [exécuterCommandes] rend un tableau de messages d’erreurs, vide s’il n’y a pas eu d’erreurs ;
- lignes 11-18 : on ouvre la connexion vers la base MySQL [dbpersonnes]. Si l’ouverture échoue, un message d’erreur est affiché et on s’arrête (lignes 14-18) ;
- ligne 22 : on passe donc une connexion ouverte à la fonction [exécuterCommandes]. Elle sera fermée au retour de la fonction (ligne 24) ;
- ligne 20 : avant de la passer à la fonction [exécuterCommandes], on configure la connexion. En cas d’erreur, les opérations SQL avec un objet [PDO] peuvent soit rendre le booléen FALSE (valeur par défaut), soit lancer une exception. La ligne 20 choisit ce second cas. En effet, il est facile ‘d’oublier’ de vérifier le résultat booléen de l’exécution d’un ordre SQL. Cela produira une erreur ultérieurement mais ailleurs dans le code rendant ainsi plus difficile le lieu originel de celle-ci. Dans le cas d’une exception non gérée (absence de catch), l’exception va remonter dans le code jusqu’à rencontrer un catch ou jusqu’à remonter à l’interpréteur PHP qui lui interceptera l’exception. Dans ce cas, la nature de l’exception et son lieu d’origine dans le code sont affichés ;
- ligne 22 : la fonction [exécuterCommandes] est appelée pour exécuter le fichier d’ordres SQL [$SQLFileName] ;
- lignes 45-47 : on vérifie que le fichier des ordres SQL existe bien. Si ce n’est pas le cas, on note l’erreur et on retourne ce résultat ;
- ligne 51 : on met les ordres SQL dans un tableau [$requêtes]. Lignes 53-55, si l’opération échoue, on rend un tableau d’erreurs avec un unique message ;
- ligne 57 : on va cumuler les erreurs dans le tableau [$erreurs] ;
- ligne 58 : n° de la requête ;
- ligne 59 : le booléen [$fini] contrôle l’exécution des ordres SQL du tableau [$requêtes]. Lorsqu’il passe à TRUE, l’exécution s’arrête ;
- ligne 60 : on boucle sur toutes les requêtes ;
- ligne 63 : on extrait le texte de l’ordre SQL n° i. La fonction [trim] va enlever les espaces qui précèdent et suivent le texte de l’ordre SQL. Par ‘espaces’, il faut entendre le blanc \b, le retour chariot \r, la marque de fin de ligne \n, le saut de page \f, la tabulation \t… Ce qui nous importe ici c’est que la marque de fin de ligne du texte SQL va disparaître ;
- lignes 65-69 : si le texte SQL est vide, alors on ignore la requête et on passe à la suivante ;
- ligne 72 : on envoie l’ordre SQL au serveur MySQL. La méthode [PDO::exec] va lancer une exception si l’exécution échoue. On rappelle que ce comportement est dû à la configuration faite à la ligne 20 ;
- ligne 79 : le message d’erreur est ajouté au tableau des erreurs ;
- ligne 81 : on positionne le booléen [$fini] qui contrôle la boucle. Si le paramètre [$arrêt] (ligne 36) vaut TRUE, on doit arrêter la boucle ;
- lignes 74-76 : si l’exécution de l’ordre SQL a réussi, on l’affiche à l’écran si le paramètre [$suivi] (ligne 36) vaut TRUE ;
- ligne 87 : une fois toutes les ordres SQL exécutés, on rend le tableau des erreurs [$erreurs] ;
La fonction [adError] des lignes 90-97 permet d’ajouter une erreur au tableau des erreurs [$erreurs] :
- ligne 90 : la fonction reçoit 4 paramètres :
- le paramètre [$erreurs] est passé par référence. En effet on veut agir sur le tableau qui est passé en paramètre et non sur une copie de celui-ci ;
- le paramètre [$requête] est le texte SQL de l’ordre qui a échoué ;
- le paramètre [$msg] est le message d’erreur lié à l’ordre qui a échoué ;
- le booléen [$suivi] indique si le message d’erreur doit être affiché ($suivi=TRUE) ou non ($suivi=FALSE) sur la console ;
La fonction [exécuterCommandes] est appelé par le script des lignes 3-33 :
- lignes 11-18 : une connexion est faite avec la base de données MySQL [dbpersonnes] ;
- ligne 20 : la connexion est configurée ;
- ligne 22 : le fichier des ordres SQL est ensuite exécuté ;
- ligne 24 : on ferme la connexion ;
- lignes 26-29 : on affiche les erreurs rendues par la fonction [exécuterCommandes] ;
Les résultats écran :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | drop table if exists personnes : Exécution réussie
SET NAMES 'utf8' : Exécution réussie
create table personnes (prenom varchar(30) not null, nom varchar(30) not null, age integer not null, primary key (nom,prenom)) : Exécution réussie
insert into personnes (prenom, nom, age) values('Paul','Langevin',48) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Lefur-Sylvie' for key 'PRIMARY')
insert into personnes (prenom, nom, age) values ('Pierre','Nicazou',35) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Géraldine','Colou',26) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Girond-Paulette' for key 'PRIMARY')
-----------------------
Il y a eu 2 erreur(s)
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Lefur-Sylvie' for key 'PRIMARY')
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Girond-Paulette' for key 'PRIMARY')
Terminé
|
Les insertions faites sont visibles avec phpMyAdmin :
Exécution d’ordres SQL quelconques¶
Le script suivant montre l’exécution des ordres SQL du fichier texte [sql.txt] suivant :
1 2 3 4 5 6 7 8 9 | select * from personnes
select nom,prenom from personnes order by nom asc, prenom desc
select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
insert into personnes values('Josette','Bruneau',46)
update personnes set age=47 where nom='Bruneau'
select * from personnes where nom='Bruneau'
delete from personnes where nom='Bruneau'
select * from personnes where nom='Bruneau'
xselect * from personnes where nom='Bruneau'
|
Parmi ces ordres SQL, il y a l’ordre select qui ramène des résultats de la base de données, les ordres insert, update, delete qui modifient la base sans ramener de résultats et enfin des ordres erronés tels que le dernier (xselect). Le script [mysql-04.php] est le suivant :
1 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 | <?php
// identité de la base de données
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// identifiants de l'utilisateur
const ID = "admpersonnes";
const PWD = "nobody";
// identité du fichier texte des commandes SQL à exécuter
const SQL_COMMANDS_FILENAME = "sql.txt";
try {
// connexion à la base MySql
$connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
// affichage erreur
print "Erreur : " . $ex->getMessage() . "\n";
exit;
}
// on veut qu'à chaque erreur de SGBD, une exception soit lancée
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// exécution du fichier d'ordres SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE, FALSE);
// fermeture connexion
$connexion = NULL;
//affichage nombre d'erreurs
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
print "$erreurs[$i]\n";
}
// c'est fini
print "Terminé\n";
exit;
// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE, bool $arrêt = TRUE): array {
………………………………………………………….
// on exécute les requêtes une par une - au départ pas d'erreurs
$erreurs = [];
$i = 0;
$fini = FALSE;
while ($i < count($requêtes) && !$fini) {
// on récupère le texte de la requête
// trim va enlever la marque de fin de ligne
$requête = trim($requêtes[$i]);
// requête vide ?
if (strlen($requête) == 0) {
// on ignore la requête et on passe à la requête suivante
$i++;
continue;
}
// exécution de la requête
// on récupère son nom
$commande = "";
if (preg_match("/^\s*(\S+)/", $requête, $champs)) {
$commande = strtolower($champs[0]);
}
try {
// est-ce un ordre SELECT ?
if ($commande === "select") {
$résultat = $connexion->query($requête);
} else {
$résultat = $connexion->exec($requête);
}
// suivi écran ou non ?
if ($suivi) {
print "[$requête] : Exécution réussie\n";
}
// on affiche le résultat de l'exécution
afficherInfos($commande, $résultat);
} catch (PDOException $ex) {
// il s'est produit une erreur
addError($erreurs, $requête, $ex->getMessage(), $suivi);
// est-ce qu'on s'arrête ?
$fini = $arrêt;
}
// requête suivante
$i++;
}
// résultat
return $erreurs;
}
function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
…
}
// ---------------------------------------------------------------------------------
function afficherInfos(string $commande, $résultat): void {
// affiche le résultat $résultat d'une requête sql
// s'agissait-il d'un select ?
switch ($commande) {
case "select" :
// on affiche les noms des champs
$titre = "";
$nbColonnes = $résultat->columnCount();
for ($i = 0; $i < $nbColonnes; $i++) {
$infos = $résultat->getColumnMeta($i);
$titre .= $infos['name'] . ",";
}
// on enlève le dernier caractère ,
$titre = substr($titre, 0, strlen($titre) - 1);
// on affiche la liste des champs
print "$titre\n";
// ligne séparatrice
$séparateurs = "";
for ($i = 0; $i < strlen($titre); $i++) {
$séparateurs .= "-";
}
print "$séparateurs\n";
// données
foreach ($résultat as $ligne) {
$data = "";
for ($i = 0; $i < $nbColonnes; $i++) {
$data .= $ligne[$i] . ",";
}
// on enlève le dernier caractère ,
$data = substr($data, 0, strlen($data) - 1);
// on affiche
print "$data\n";
}
break;
case "update":
case "insert":
case "delete";
print " $résultat lignes(s) a (ont) été modifiée(s)\n";
break;
}
}
|
Commentaires
- lignes 36-83 : la fonction [exécuterCommandes] est légèrement modifiée : l’ordre SQL [select] ne s’exécute pas de la même façon que les autres ordres SQL. Cet ordre est le seul à ramener comme résultat une table, ç-à-d un ensemble de lignes et de colonnes de la base de données ;
- lignes 55-57 : on isole le 1er mot de l’ordre SQL à l’aide d’une expression régulière ;
- lignes 60-64 : si la commande SQL est [select], on utilise la méthode [PDO::query] sinon la méthode [PDO::exec] pour exécuter l’ordre SQL. Dans les deux cas, si l’exécution échoue, une exception sera lancée et interceptée lignes 71-77. Si l’exécution réussit, la ligne 70 affiche son résultat ;
- lignes 90-130 : la fonction afficherInfos affiche des informations sur le résultat de l’exécution d’un ordre SQL ;
- ligne 94 : on traite le cas du [select]. Son résultat est un objet de type [PDOStatement] ;
- ligne 96 : la méthode [PDOStatement::getColumnCount()] rend le nombre de colonnes de la table résultat du select ;
- lignes 98-99 : la méthode [PDOStatement::getMeta(i)] rend un dictionnaire d’informations sur la colonne n° i de la table résultat du select. Dans ce dictionnaire, la valeur associée à la clé “name” est le nom de la colonne ;
- lignes 97-102 : les noms des colonnes de la table résultat du select sont concaténées dans une chaîne de caractères ;
- lignes 105-110 : on construit une ligne de séparation ayant la même longueur que la chaîne de caractères construite précédemment ;
- lignes 112-121 : un objet de type PDOStatement peut être parcouru par une boucle foreach. A chaque itération, l’élément obtenu est une ligne de la table résultat du select sous la forme d’un tableau de valeurs représentant les valeurs des différentes colonnes de la ligne. On affiche toutes ces valeurs avec une boucle for (lignes 114-116)* *;
- lignes 123-127 : le résultat de l’exécution d’un ordre insert, update, delete est le nombre de lignes modifiées par l’ordre ;
Les résultats écran :
1 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 | [set names 'utf8'] : Exécution réussie
[select * from personnes] : Exécution réussie
prenom,nom,age
--------------
Géraldine,Colou,26
Paulette,Girond,56
Paul,Langevin,48
Sylvie,Lefur,70
Pierre,Nicazou,35
[select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
nom,prenom
----------
Colou,Géraldine
Girond,Paulette
Langevin,Paul
Lefur,Sylvie
Nicazou,Pierre
[select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] : Exécution réussie
prenom,nom,age
--------------
Pierre,Nicazou,35
Géraldine,Colou,26
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[update personnes set age=47 where nom='Bruneau'] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,47
[delete from personnes where nom='Bruneau'] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
-----------------------
Il y a eu 1 erreur(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
Terminé
|
Utilisation d’ordres SQL préparés¶
Exemple 1¶
Examinons le script suivant [mysql-05.php] :
1 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 | <?php
// identité de la base de données
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// identifiants de l'utilisateur
const ID = "admpersonnes";
const PWD = "nobody";
try {
// connexion à la base MySql
$connexion = new PDO(DSN, ID, PWD);
// on veut qu'à chaque erreur de SGBD, une exception soit lancée
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// on vide la table des personnes
$connexion->exec("delete from personnes");
// une liste de personnes
$personnes = [];
$personnes[] = ["nom" => "Langevin", "prenom" => "Paul", "age" => 47];
$personnes[] = ["nom" => "Lefur", "prenom" => "Sylvie", "age" => 28];
// on va mettre ces personnes dans la base de données
$statement = $connexion->prepare("insert into personnes (nom, prenom, age) values (:nom, :prenom, :age)");
for ($i = 0; $i < count($personnes); $i++) {
$statement->execute($personnes[$i]);
}
} catch (PDOException $ex) {
// affichage erreur
print "Erreur : " . $ex->getMessage() . "\n";
} finally {
// fermeture connexion
$connexion = NULL;
}
// c'est fini
print "Terminé\n";
exit;
|
Commentaires
Nous nous intéressons ici aux lignes 16-24 qui insèrent deux personnes dans la table des personnes de la base [dbpersonnes].
- ligne 21 : on ‘prépare’ un ordre SQL paramétré. Les paramètres sont précédés du caractère : :nom, :prenom, :age. Pour ‘préparer’ un ordre SQL, on utilise la méthode [PDO::prepare]. Le résultat est un type [PDOStatement]. La ‘préparation’ n’est pas une exécution : rien n’est exécuté ;
- ligne 23 : exécution de l’ordre ‘préparé’ avec la méthode [PDOStatement::execute]. Pour ce faire, il faut donner des valeurs aux paramètres :nom, :prenom et :age. Il y a plusieurs façons de faire cela. On utilise ici un dictionnaire ayant pour clés les paramètres de l’ordre préparé que l’on passe à la méthode [PDOStatement::execute]. Une autre façon de faire est de donner aux paramètres une valeur avec la méthode [PDOStatement::bindValue($paramètre,$valeur)]. Par exemple ici :
1 2 3 4 | $statement→bindValue(“nom”,”Langevin”);
$statement→bindValue(“prenom”,”Paul”);
$statement→bindValue(“age”,47);
$statement→execute();
|
L’inconvénient est qu’il faut réitérer cette instruction pour chaque paramètre. La méthode du dictionnaire peut alors être plus pratique. La méthode [PDOStatement::execute] rend FALSE si l’exécution échoue ;
- la méthode utilisée ici pour faire les insertions :
- une méthode préparée ;
- n exécutions de l’ordre préparé ;
est plus économique en temps d’exécution que d’exécuter n ordres SQL différents. Cette méthode est donc à privilégier. Elle est utilisable pour les ordres SQL select, update, delete, insert. Dans le cas de l’ordre SQL select, après son exécution avec [PDOStatement::execute], on récupère les lignes du résultat avec la méthode [PDOStatement::fetchAll] ;
Exemple 2¶
Le script suivant [mysql-06.php] montre l’utilisation d’un ordre préparé pour l’opération SQL select, ainsi que diverses façons de récupérer les lignes ramenées par cette opération :
1 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 | <?php
// identité de la base de données
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// identifiants de l'utilisateur
const ID = "admpersonnes";
const PWD = "nobody";
try {
// connexion à la base MySql
$connexion = new PDO(DSN, ID, PWD);
// on veut qu'à chaque erreur de SGBD, une exception soit lancée
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// on vide la table des personnes
$connexion->exec("delete from personnes");
// on va mettre ces personnes dans la base de données
$statement = $connexion->prepare("insert into personnes (nom, prenom, age) values (:nom, :prenom, :age)");
for ($i = 0; $i < 10; $i++) {
$statement->execute(["nom" => "nom" . $i, "prenom" => "prenom" . $i, "age" => $i * 10]);
}
// on interroge la base
$statement = $connexion->prepare("select nom, prenom, age from personnes");
$statement->execute();
// 1re ligne
$ligne = $statement->fetch();
var_dump($ligne);
// 2e ligne
$ligne = $statement->fetch(PDO::FETCH_ASSOC);
var_dump($ligne);
// 3e ligne
$ligne = $statement->fetch(PDO::FETCH_OBJ);
var_dump($ligne);
// 4e ligne
$statement->setFetchMode(PDO::FETCH_CLASS, "Person");
$ligne = $statement->fetch();
var_dump($ligne);
// lecture séquentielle de toutes les lignes
$statement = $connexion->prepare("select nom, prenom, age from personnes");
$statement->execute();
$statement->setFetchMode(PDO::FETCH_CLASS, "Person");
while ($personne = $statement->fetch()) {
print "$personne\n";
}
} catch (PDOException $ex) {
// affichage erreur
print "Erreur : " . $ex->getMessage() . "\n";
} finally {
// fermeture connexion
$connexion = NULL;
}
// c'est fini
print "Terminé\n";
exit;
class Person {
private $nom;
private $prenom;
private $age;
public function __toString() {
return "Personne[$this->nom,$this->prenom,$this->age]";
}
}
|
Commentaires
- lignes 17-20 : on insère 10 lignes dans la table [personnes] de la base [admpersonnes] :
- ligne 22 : on « prépare » un ordre SQL [select] qu’on exécute ligne 23 ;
- ligne 25 : on récupère avec la méthode [PDOStatement::fetch] une ligne du résultat de l’opération SQL [select] exécutée. La méthode [PDOStatement::fetch] peut récupérer de diverses façons les lignes résultats d’une opération SQL [select] préparée. Le script en présente quelques unes. La méthode [PDOStatement::fetch] sans paramètres ramène la ligne courante du [select] sous la forme d’un dictionnaire indexé à la fois sur les n°s de colonnes et leurs noms ;
- ligne 26 : affiche le résultat suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | array(6) {
["nom"]=>
string(4) "nom0"
[0]=>
string(4) "nom0"
["prenom"]=>
string(7) "prenom0"
[1]=>
string(7) "prenom0"
["age"]=>
string(1) "0"
[2]=>
string(1) "0"
}
|
- lignes 28-29 : le paramètre [PDO::FETCH_ASSOC] fait que la ligne ramenée est un dictionnaire indexé par les noms des colonnes de la table :
1 2 3 4 5 6 7 8 | array(3) {
["nom"]=>
string(4) "nom1"
["prenom"]=>
string(7) "prenom1"
["age"]=>
string(2) "10"
}
|
- lignes 31-32 : le paramètre [PDO::FETCH_OBJ] fait que la ligne ramenée est un objet de type [stdclass] dont les attributs sont les noms des colonnes de la table :
1 2 3 4 5 6 7 8 | object(stdClass)#2 (3) {
["nom"]=>
string(4) "nom2"
["prenom"]=>
string(7) "prenom2"
["age"]=>
string(2) "20"
}
|
- ligne 34 : on fixe le mode de recherche de la méthode [fetch] avec la méthode [PDOStatement::setFetchMode]. Ce mode devient alors le mode par défaut tant qu’il n’est pas changé soit par une autre opération [PDOStatement::setFetchMode] soit en passant un mode en paramètre à la méthode [PDOStatement::fetch] comme il a été fait précédemment. L’opération [setFetchMode(PDO::FETCH_CLASS, « Person »)] indique que la ligne lue doit être placée dans un objet de type [Person]. Cette classe doit avoir parmi ces attributs, des attributs portant le nom des colonnes de la ligne lue. C’est le cas de la classe [Person] définie aux lignes 56-63 ;
- la ligne 36 affiche le résultat suivant :
1 2 3 4 5 6 7 8 | object(Person)#4 (3) {
["nom":"Person":private]=>
string(4) "nom3"
["prenom":"Person":private]=>
string(7) "prenom3"
["age":"Person":private]=>
string(2) "30"
}
|
- lignes 38-43 : montrent comment exploiter séquentiellement les résultats du [select] ;
- ligne 42 : l’affichage de [$personne] va utiliser la méthode [__toString] de la classe [Person] ;
Utilisation de transactions¶
Une transaction permet de regrouper une séquence d’ordres SQL en une unité d’exécution : soit tous les ordres réussissent soit l’un d’eux échoue et alors tous les ordres SQL qui ont précédé celui-ci sont annulés. Dit autrement, lorsqu’on utilise une transaction pour exécuter des ordres SQL, après l’exécution de celle-ci la base de données est dans un état stable :
- soit dans un état nouveau créé par l’exécution réussie de tous les ordres SQL de la transaction ;
- soit dans l’état dans laquelle elle était avant que la transaction ne commence à être exécutée ;
Nous allons reprendre l’exemple de l’exécution des ordres SQL contenus dans un fichier texte étudié au paragraphe lien. Nous allons inclure cette exécution dans une transaction. Les ordres SQL seront contenus dans le fichier [sql2.txt] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | set names 'utf8'
select * from personnes
select nom,prenom from personnes order by nom asc, prenom desc
select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
insert into personnes values('Josette','Bruneau',46)
update personnes set age=47 where nom='Bruneau'
select * from personnes where nom='Bruneau'
delete from personnes where nom='Bruneau'
select * from personnes where nom='Bruneau'
insert into personnes values('Josette','Bruneau',46)
select * from personnes where nom='Bruneau'
xselect * from personnes where nom='Bruneau'
|
L’ordre erroné de la ligne 12 va faire échouer toute la transaction. On devrait donc retrouver la base comme elle était avant la transaction. Dans l’exemple ci-dessus, on ne devrait donc pas voir dans la table la ligne insérée par la ligne 10 ci-dessus. Le script évolue très peu. On redonne cependant la totalité du code [mysql-07.php] :
1 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 | <?php
// identité de la base de données
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// identifiants de l'utilisateur
const ID = "admpersonnes";
const PWD = "nobody";
// identité du fichier texte des commandes SQL à exécuter
const SQL_COMMANDS_FILENAME = "sql2.txt";
try {
// connexion à la base MySql
$connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
// affichage erreur
print "Erreur : " . $ex->getMessage() . "\n";
exit;
}
// on veut qu'à chaque erreur de SGBD, une exception soit lancée
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// exécution du fichier d'ordres SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE);
// fermeture connexion
$connexion = NULL;
//affichage nombre d'erreurs
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
print "$erreurs[$i]\n";
}
// c'est fini
print "Terminé\n";
exit;
// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE): array {
// utilise la connexion $connexion
// exécute les commandes SQL contenues dans le fichier texte SQLFileName
// ce fichier est un fichier de commandes SQL à exécuter à raison d'une par ligne
// les commandes SQL sont exécutées dans une transaction
// si l'un des ordres échoue, la transaction est annulée et on retrouve la base de données dans l'état où elle était avant la transaction
// si $suivi=1 alors chaque exécution d'un ordre SQL fait l'objet d'un affichage indiquant sa réussite ou son échec
// la fonction rend un tableau (nb d'erreurs, erreur1, erreur2…)
//
// on vérifie la présence du fichier SQLFileName
if (!file_exists($SQLFileName)) {
return ["Le fichier [$SQLFileName] n'existe pas"];
}
// exécution des requêtes SQL contenues dans SQLFileName
// on les met dans un tableau
$requêtes = file($SQLFileName);
// erreur ?
if ($requêtes === FALSE) {
return ["Erreur lors de l'exploitation du fichier SQL [$SQLFileName]"];
}
// les requêtes vont être placées dans une transaction
$connexion->beginTransaction();
// on exécute les requêtes une par une - au départ pas d'erreurs
$erreurs = [];
$i = 0;
$fini = FALSE;
while ($i < count($requêtes) && !$fini) {
// on récupère le texte de la requête
// trim va enlever la marque de fin de ligne
$requête = trim($requêtes[$i]);
// requête vide ?
if (strlen($requête) == 0) {
// on ignore la requête et on passe à la requête suivante
$i++;
continue;
}
// exécution de la requête
// on récupère son nom
$commande = "";
if (preg_match("/^\s*(\S+)/", $requête, $champs)) {
$commande = strtolower($champs[0]);
}
try {
// est-ce un ordre SELECT ?
if ($commande === "select") {
$résultat = $connexion->query($requête);
} else {
$résultat = $connexion->exec($requête);
}
// suivi écran ou non ?
if ($suivi) {
print "[$requête] : Exécution réussie\n";
}
// on affiche le résultat de l'exécution
afficherInfos($commande, $résultat);
} catch (PDOException $ex) {
// il s'est produit une erreur
addError($erreurs, $requête, $ex->getMessage(), $suivi);
// on s'arrête au tour suivant
$fini = TRUE;
}
// requête suivante
$i++;
}
// fin de la transaction
if (!$fini) {
// il n'y a pas eu d'erreurs : on valide la transaction
$connexion->commit();
} else {
// il y a eu des erreurs : on annule la transaction
$connexion->rollBack();
// ajout erreur
addError($erreurs, "", "Transaction annulée", $suivi);
}
// résultat
return $erreurs;
}
function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
…
}
// ---------------------------------------------------------------------------------
function afficherInfos(string $commande, $résultat): void {
…
}
|
Commentaires
Nous avons surligné les modifications du script original [mysql-04.php].
- lignes 22, 36 : la fonction [exécuterCommandes] a perdu son quatrième paramètre [$arrêt=TRUE]. En effet, comme les ordres SQL s’exécutent au sein d’une transaction, toute erreur provoquera l’arrêt de la transaction ;
- lignes 40-41 : rappel de la fonction d’une transaction ;
- ligne 57 : on démarre une transaction. A partir de maintenant, tout ordre SQL exécuté dans la boucle des lignes 62-99 l’est au sein de cette transaction ;
- lignes 101-109 : le booléen [$fini] est à TRUE s’il y a eu erreur (ligne 95). Lorsqu’il est à FALSE, il n’y a pas eu d’erreurs et on valide alors la transaction (ligne 103). Lorsqu’il est à TRUE, il y a eu des erreurs et on annule alors la transaction (ligne 106) et on rajoute l’erreur de transaction dans la liste des erreurs (ligne 108) ;
Résultats
Avant exécution du script, la base [admpersonnes] est dans l’état suivant :
On exécute le script [mysql-07.php]. Les affichages écran sont alors les suivants :
1 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 | [set names 'utf8'] : Exécution réussie
[select * from personnes] : Exécution réussie
prenom,nom,age
--------------
prenom0,nom0,0
prenom1,nom1,10
prenom2,nom2,20
prenom3,nom3,30
prenom4,nom4,40
prenom5,nom5,50
prenom6,nom6,60
prenom7,nom7,70
prenom8,nom8,80
prenom9,nom9,90
[select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
nom,prenom
----------
nom0,prenom0
nom1,prenom1
nom2,prenom2
nom3,prenom3
nom4,prenom4
nom5,prenom5
nom6,prenom6
nom7,prenom7
nom8,prenom8
nom9,prenom9
[select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] : Exécution réussie
prenom,nom,age
--------------
prenom4,nom4,40
prenom3,nom3,30
prenom2,nom2,20
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[update personnes set age=47 where nom='Bruneau'] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,47
[delete from personnes where nom='Bruneau'] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,46
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
[] : Erreur (Transaction annulée)
-----------------------
Il y a eu 2 erreur(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
[] : Erreur (Transaction annulée)
Terminé
|
- ligne 53 : une erreur se produit sur la commande [xselect] ;
- ligne 54 : la transaction est alors annulée ;
Si on vérifie l’état de la base, on la trouve dans le même état qu’avant l’exécution du script. On n’y voit pas notamment la ligne [Josette, Bruneau, 46] de la ligne 52 des résultats ci-dessus.
Résumé
- une transaction commence avec la méthode [PDO::beginTransaction] ;
- on la termine sur un succès avec la méthode [PDO::commit] ;
- on la termine sur un échec avec la méthode [PDO::rollback] ;
Lorsqu’on exploite une base de données, c’est une bonne habitude de mettre toute opération SQL dans une transaction pour s’isoler des autres utilisateurs de la base (elle a également ce rôle). Une transaction doit être la plus courte possible. Il ne faut donc pas oublier de la terminer par un [commit] ou un [rollback] selon les cas.
Exercice d’application – version 5¶
Nous avons déjà écrit plusieurs versions de cet exercice. La dernière version utilisait une architecture en couches :
La couche [dao] implémente une interface [InterfaceDao]. Nous avons construit une classe implémentant de cette interface :
- [DaoImpotsWithTaxAdminDataInJsonFile] qui allait chercher les données fiscales dans un fichier jSON ;
Nous allons implémenter l’interface [InterfaceDao] par une nouvelle classe [DaoImpotsWithTaxAdminDataInDatabase] qui ira chercher les données de l’administration fiscale dans une base de données MySQL.
Création de la base de données [dbimpots-2019]¶
En suivant l’exemple du paragraphe lien, nous construisons une base de données MySQL nommée [dbimpots-2019] dont le propriétaire sera [admimpots] avec le mot de passe [mdpimpots] :
- en [1-4] ci-dessus, nous voyons la base [dbimpots-2019] qui pour l’instant n’a pas de tables ;
- en [1-5] ci-dessus, nous voyons que l’utilisateur [admimpots] a tous les droits sur la base [dbimpots-2019]. Ce que nous ne voyons pas ici c’est que cet utilisateur a le mot de passe [admimpots] ;
Nous créons maintenant la table [tbtranches]qui contiendra les tranches d’imposition :
- en [1-7], nous créons une table nommée [tbtranches] ayant 4 colonnes ;
- en [3-6] nous définissons une colonne nommée [id] (3), de type entier [int] (4), qui sera clé primaire [6] de la table et sera autoincrémentée [5] par le SGBD. Cela signifie que MySQL va gérer lui-même les valeurs de la clé primaire au moment des insertions. Il affectera la valeur 1 à la clé primaire de la 1ière insertion, puis 2 à la suivante, etc … ;
- en [7], l’assistant nous propose d’autres options de configuration de la clé primaire. Ici on se contente de valider [7] les valeurs par défaut ;
- en [8-16], on définit les trois autres colonnes de la table :
- [limites] (8) de type nombre décimal (9) à 10 chiffres dont 2 décimales (10) contiendra les éléments de la colonne 17 des tranches d’impôts ;
- [coeffR] (11) de type nombre décimal (12) à 6 chiffres dont 2 décimales (13) contiendra les éléments de la colonne 18 des tranches d’impôts ;
- [coeffN] (14) de type nombre décimal (15) à 10 chiffres dont 2 décimales (16) contiendra les éléments de la colonne 19 des tranches d’impôts ;
Après avoir validé cette structure, nous obtenons le résultat suivant :
- en [5], l’icône de la clé indique que la colonne [id] est clé primaire. On voit également que cette clé primaire a des valeurs entières (6) et qu’elle est gérée (autoincrémentée) par MySQL ;
De la même façon que nous avons créé la table [tbtranches] nous construisons la table [tbconstantes] qui contiendra les constantes du calcul de l’impôt :
Il est possible d’exporter la structure de la base de données dans un fichier texte sous forme d’une suite d’ordres SQL :
L’option [5] n’exporte ici que la structure de la base de données et pas son contenu. Dans notre cas, la base n’a pas encore de contenu.
L’option [11] produit le fichier SQL [dbimpots-2019.sql] suivant :
1 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 | -- phpMyAdmin SQL Dump
-- version 4.8.5
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Jun 30, 2019 at 01:10 PM
-- Server version: 5.7.24
-- PHP Version: 7.2.11
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `dbimpots-2019`
--
CREATE DATABASE IF NOT EXISTS `dbimpots-2019` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `dbimpots-2019`;
-- --------------------------------------------------------
--
-- Table structure for table `tbconstantes`
--
DROP TABLE IF EXISTS `tbconstantes`;
CREATE TABLE `tbconstantes` (
`id` int(11) NOT NULL,
`plafondQfDemiPart` decimal(10,2) NOT NULL,
`plafondRevenusCelibatairePourReduction` decimal(10,2) NOT NULL,
`plafondRevenusCouplePourReduction` decimal(10,2) NOT NULL,
`valeurReducDemiPart` decimal(10,2) NOT NULL,
`plafondDecoteCelibataire` decimal(10,2) NOT NULL,
`plafondDecoteCouple` decimal(10,2) NOT NULL,
`plafondImpotCelibatairePourDecote` decimal(10,2) NOT NULL,
`plafondImpotCouplePourDecote` decimal(10,2) NOT NULL,
`abattementDixPourcentMax` decimal(10,2) NOT NULL,
`abattementDixPourcentMin` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Table structure for table `tbtranches`
--
DROP TABLE IF EXISTS `tbtranches`;
CREATE TABLE `tbtranches` (
`id` int(11) NOT NULL,
`limites` decimal(10,2) NOT NULL,
`coeffR` decimal(10,2) NOT NULL,
`coeffN` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `tbconstantes`
--
ALTER TABLE `tbconstantes`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `tbtranches`
--
ALTER TABLE `tbtranches`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `tbconstantes`
--
ALTER TABLE `tbconstantes`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `tbtranches`
--
ALTER TABLE `tbtranches`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
Vous pouvez utiliser ce fichier SQL pour régénérer la base [dbimpots-2019] si elle a été détruite ou altérée. Il n’y a pas lieu ici de supprimer la base avant de la régénérer puisque le script SQL prend soin de le faire lui-même :
Organisation du code¶
Pour mieux montrer le rôle des différents scripts PHP que nous écrivons, nous allons organiser notre code en dossiers :
- en [1], vue d’ensemble de la version 05 ;
- en [2], les entités de l’application, entités échangées entre couches ;
- en [3], les utilitaires de l’application ;
- en [4], les données utilisées ou produites par l’application.
Nous prenons ici la décision de n’utiliser que des fichiers jSON pour
les fichiers texte. Ceux-ci présentent plusieurs avantages :
- ils sont reconnus par beaucoup d’outils ;
- ces outils ont une coloration syntaxique. Par ailleurs, la notation jSON a des règles. Lorsque celles-ci ne sont pas respectées les outils les signalent. Par exemple, une erreur difficile à détecter dans un fichier texte basique est l’utilisation de O majuscule / minuscule à la place de zéros. Si cette erreur se produit elle sera signalée. En effet dans le code jSON :
« plafondRevenusCouplePourReduction »: 42O74
où on a mis par inadvertance un O majuscule à la place du zéro dans [42074], Netbeans signale la faute :
En effet, Netbeans reconnaît le O majuscule qui fait de [49O74] une chaîne de caractères. Il en conclut que la syntaxe devrait être [4-5] : la chaîne [47O74] devrait être entourée de guillemets. L’attention du développeur est donc attirée par la faute et peut la corriger : soit mettre les guillemets, soit remplacer le O par un zéro ;
Les autres éléments de la version 05 sont les suivants :
- en [6], les interfaces et classes de la couche [Dao] ;
- en [7], les interfaces et classes de la couche [métier] ;
- en [8], les scripts principaux de la version 05 ;
La version 05 a deux objectifs distincts :
- remplir la base MySQL [dbimpots-2019] avec le contenu du fichier jSON [Data/txadmindata.json] ;
- implémenter le calcul de l’impôt avec des données fiscales venant désormais de la base MySQL [dbimpots-2019] ;
Nous allons traiter ces deux objectifs séparément.
Remplissage de base de données [dbimpots-2019]¶
Objectif¶
Le fichier texte taxadmindata.json contient les données de l’administration fiscale :
1 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 | {
"limites": [
9964,
27519,
73779,
156244,
0
],
"coeffR": [
0,
0.14,
0.3,
0.41,
0.45
],
"coeffN": [
0,
1394.96,
5798,
13913.69,
20163.45
],
"plafondQfDemiPart": 1551,
"plafondRevenusCelibatairePourReduction": 21037,
"plafondRevenusCouplePourReduction": 42074,
"valeurReducDemiPart": 3797,
"plafondDecoteCelibataire": 1196,
"plafondDecoteCouple": 1970,
"plafondImpotCouplePourDecote": 2627,
"plafondImpotCelibatairePourDecote": 1595,
"abattementDixPourcentMax": 12502,
"abattementDixPourcentMin": 437
}
|
Notre objectif est de transférer ces données dans la base MySQL [dbimpots-2019] créée précédemment.
Les entités¶
L’entité [Database] servira à encapsuler les données du fichier jSON [database.json] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {
"dsn": "mysql:host=localhost;dbname=dbimpots-2019",
"id": "admimpots",
"pwd": "mdpimpots",
"tableTranches": "tbtranches",
"colLimites": "limites",
"colCoeffR": "coeffr",
"colCoeffN": "coeffn",
"tableConstantes": "tbconstantes",
"colPlafondQfDemiPart": "plafondQfDemiPart",
"colPlafondRevenusCelibatairePourReduction": "plafondRevenusCelibatairePourReduction",
"colPlafondRevenusCouplePourReduction": "plafondRevenusCouplePourReduction",
"colValeurReducDemiPart": "valeurReducDemiPart",
"colPlafondDecoteCelibataire": "plafondDecoteCelibataire",
"colPlafondDecoteCouple": "plafondDecoteCouple",
"colPlafondImpotCelibatairePourDecote": "plafondImpotCelibatairePourDecote",
"colPlafondImpotCouplePourDecote": "plafondImpotCouplePourDecote",
"colAbattementDixPourcentMax": "abattementDixPourcentMax",
"colAbattementDixPourcentMin": "abattementDixPourcentMin"
}
|
L’entité [TaxAdminData] servira à encapsuler les données du fichier jSON [taxadmindata.json] suivant :
1 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 | {
"limites": [
9964,
27519,
73779,
156244,
0
],
"coeffR": [
0,
0.14,
0.3,
0.41,
0.45
],
"coeffN": [
0,
1394.96,
5798,
13913.69,
20163.45
],
"plafondQfDemiPart": 1551,
"plafondRevenusCelibatairePourReduction": 21037,
"plafondRevenusCouplePourReduction": 42074,
"valeurReducDemiPart": 3797,
"plafondDecoteCelibataire": 1196,
"plafondDecoteCouple": 1970,
"plafondImpotCouplePourDecote": 2627,
"plafondImpotCelibatairePourDecote": 1595,
"abattementDixPourcentMax": 12502,
"abattementDixPourcentMin": 437
}
|
L’entité [TaxPayerData] servira à encapsuler les données du fichier jSON [taxpayerdata.json] suivant :
1 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 | [
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
|
La classe de base [BaseEntity]¶
Pour simplifier le code des entités, nous adopterons la règle suivante : les attributs d’une entité ont les mêmes noms que les attributs du fichier jSON que l’entité doit encapsuler. Moyennant cette règle, les entités [Database, TaxAdminData, TaxPayerData] ont des points communs qui peuvent être factorisés dans une classe parent. Ce sera la classe [BaseEntity] suivante :
1 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 | <?php
namespace Application;
class BaseEntity {
// attribut
protected $arrayOfAttributes;
// initialisation à partir d'un fichier jSON
public function setFromJsonFile(string $jsonFilename) {
// on récupère le contenu du fichier des données fiscales
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// erreur ?
if (!$fileContents) {
// on note l'erreur
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// on récupère le code jSON du fichier de configuration dans un tableau associatif
$this->arrayOfAttributes = \json_decode($fileContents, true);
// erreur ?
if ($this->arrayOfAttributes === FALSE) {
// on note l'erreur
$erreur = TRUE;
$message = "Le fichier de données jSON [$jsonFilename] n'a pu être exploité correctement";
}
}
// erreur ?
if ($erreur) {
// on lance une exception
throw new ExceptionImpots($message);
}
// initialisation des attributs de la classe
foreach ($this->arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// on rend l'objet
return $this;
}
public function checkForAllAttributes() {
// on vérifie que toutes les clés ont été initialisées
foreach (\array_keys($this->arrayOfAttributes) as $key) {
if ($key !== "arrayOfAttributes" && !isset($this->$key)) {
throw new ExceptionImpots("L'attribut [$key] de la classe "
. get_class($this) . " n'a pas été initialisé");
}
}
}
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// on initialise certains attributs de la classe
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// on retourne l'objet
return $this;
}
// toString
public function __toString() {
// attributs de l'objet
$arrayOfAttributes = \get_object_vars($this);
// on enlève l'attribut de la classe parent
unset($arrayOfAttributes["arrayOfAttributes"]);
// chaîne Json de l'objet
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
// getter
public function getArrayOfAttributes() {
return $this->arrayOfAttributes;
}
}
|
Commentaires
ligne 5 : la classe [BaseEntity] est destinée à être étendue par les classes [Database, TaxAdminData, TaxPayerData] ;
ligne 7 : l’attribut [$arrayOfAttributes] est un tableau contenant tous les attributs de la classe fille ayant étendu [BaseEntity] ainsi que leurs valeurs ;
lignes 9-41 : l’attribut [$arrayOfAttributes] est initialisé à partir du fichier jSON [$jsonFilename] passé en paramètre. Une exception de type [ExceptionImpot] est lancée si le fichier jSON n’a pu être lu ou si ce n’est pas un fichier jSON valide ;
lignes 36-38 : on a là un code spécial s’il est exécuté par une classe fille. Dans ce cas, [$this] représente une instance de la classe fille [Database, TaxAdminData, TaxPayerData] et dans ce cas là, les lignes 36-38 initialisent les attributs de cette classe fille, à condition que ces attributs aient la visibilité protected (ou public) (cf paragraphe lien). On a dit en effet que les attributs des entités [Database, TaxAdminData, TaxPayerData] étaient les mêmes que les attributs du fichier jSON qu’ils encapsulaient. Finalement, la méthode [setFromJsonFile] permet à une classe fille de s’initialiser à partir d’un fichier jSON ;
ligne 40 : on rend l’objet [$this] donc une instance de classe fille si la méthode [setFromJsonFile] a été appelée par une classe fille ;
lignes 43-51 : la méthode [checkForAllAttributes] permet à une classe fille de vérifier que tous ses attributs ont été initialisés. Si ce n’est pas le cas, une exception [ExceptionImpots] est lancée. Cette méthode permet à la classe fille de vérifier que son fichier jSON n’a pas oublié certains attributs ;
lignes 53-60 : la méthode [setFromArrayOfAttributes] permet à une classe fille d’initialiser tout ou partie de ses attributs à partir d’un tableau associatif dont les clés ont les mêmes noms que les attributs de la classe fille à initialiser ;
lignes 63-70 : la méthode [__toString] permet d’avoir la représentation jSON d’une classe fille ;
L’entité [Database]
L’entité [Database] est la suivante :
1 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 | <?php
namespace Application;
class Database extends BaseEntity {
// attributs
protected $dsn;
protected $id;
protected $pwd;
protected $tableTranches;
protected $colLimites;
protected $colCoeffR;
protected $colCoeffN;
protected $tableConstantes;
protected $colPlafondQfDemiPart;
protected $colPlafondRevenusCelibatairePourReduction;
protected $colPlafondRevenusCouplePourReduction;
protected $colValeurReducDemiPart;
protected $colPlafondDecoteCelibataire;
protected $colPlafondDecoteCouple;
protected $colPlafondImpotCelibatairePourDecote;
protected $colPlafondImpotCouplePourDecote;
protected $colAbattementDixPourcentMax;
protected $colAbattementDixPourcentMin;
…
}
|
La classe [Database] est utilisée pour encapsuler les données du fichier jSON [database.json] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {
"dsn": "mysql:host=localhost;dbname=dbimpots-2019",
"id": "admimpots",
"pwd": "mdpimpots",
"tableTranches": "tbtranches",
"colLimites": "limites",
"colCoeffR": "coeffr",
"colCoeffN": "coeffn",
"tableConstantes": "tbconstantes",
"colPlafondQfDemiPart": "plafondQfDemiPart",
"colPlafondRevenusCelibatairePourReduction": "plafondRevenusCelibatairePourReduction",
"colPlafondRevenusCouplePourReduction": "plafondRevenusCouplePourReduction",
"colValeurReducDemiPart": "valeurReducDemiPart",
"colPlafondDecoteCelibataire": "plafondDecoteCelibataire",
"colPlafondDecoteCouple": "plafondDecoteCouple",
"colPlafondImpotCelibatairePourDecote": "plafondImpotCelibatairePourDecote",
"colPlafondImpotCouplePourDecote": "plafondImpotCouplePourDecote",
"colAbattementDixPourcentMax": "abattementDixPourcentMax",
"colAbattementDixPourcentMin": "abattementDixPourcentMin"
}
|
La classe et le fichier jSON ont les mêmes attributs. Ceux-ci décrivent les caractéristiques de la base de données MySQL [dbimpots-2019] :
dsn | Nom DSN de la base |
---|---|
id | Propriétaire de la base |
pwd | Son mot de passe |
tableTranches | Nom de la table contenant les tranches d’imposition |
colLimites colCoeffR colCoeffN |
Noms des colonnes de la table [tableTranches] |
tableConstantes | Nom de la table contenant les constantes de calcul de l’impôt |
colPlafondQfDemiPart colPlafon dRevenusCelibatairePourReduction colP lafondRevenusCouplePourReduction colValeurReducDemiPart colPlafondDecoteCelibataire colPlafondDecoteCouple colP lafondImpotCelibatairePourDecote colPlafondImpotCouplePourDecote colAbattementDixPourcentMax colAbattementDixPourcentMin |
Noms des colonnes de la table [tableConstantes] contenant les constantes de calcul de l’impôt |
Pourquoi nommer les tables et les colonnes alors qu’on connaît déjà leurs noms et que ce n’est pas quelque chose amené à changer ? Après le SGBD MySQL, on va utiliser le SGBD PostgreSQL pour stocker les données de l’administration fiscale. Or les noms des colonnes et tables Postgres ne suivent pas les mêmes règles que MySQL. On va être obligés d’utiliser d’autres noms. C’est également vrai pour d’autres SGBD. Si on veut avoir du code portable entre SGBD, il est alors préférable d’utiliser des paramètres plutôt que les noms en dur des tables et colonnes.
Revenons au code de la classe [Database] :
1 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 | <?php
namespace Application;
class Database extends BaseEntity {
// attributs
protected $dsn;
protected $id;
protected $pwd;
protected $tableTranches;
protected $colLimites;
protected $colCoeffR;
protected $colCoeffN;
protected $tableConstantes;
protected $colPlafondQfDemiPart;
protected $colPlafondRevenusCelibatairePourReduction;
protected $colPlafondRevenusCouplePourReduction;
protected $colValeurReducDemiPart;
protected $colPlafondDecoteCelibataire;
protected $colPlafondDecoteCouple;
protected $colPlafondImpotCelibatairePourDecote;
protected $colPlafondImpotCouplePourDecote;
protected $colAbattementDixPourcentMax;
protected $colAbattementDixPourcentMin;
// setter
// initialisation
public function setFromJsonFile(string $jsonFilename) {
// parent
parent::setFromJsonFile($jsonFilename);
// on vérifie que tous les attributs ont été initialisés
parent::checkForAllAttributes();
// on retourne l'objet
return $this;
}
// getters et setters
public function getDsn() {
return $this->dsn;
}
…
public function setDsn($dsn) {
$this->dsn = $dsn;
return $this;
}
…
}
|
Commentaires
lignes 7-24 : tous les attributs de la classe ont la visibilité [protected]. C’est la condition pour qu’ils puissent être modifiés depuis la classe parent [BaseEntity] (cf paragraphe lien) ;
lignes 28-35 : la méthode [setFromJsonFile] permet d’initialiser les attributs de la classe [Database] à partir du contenu d’un fichier jSON passé en paramètre. Il faut que les attributs du fichier jSON et ceux de la classe [Database] soient identiques. Si le fichier jSON n’est pas exploitable, une exception est lancée ;
ligne 30 : c’est la classe parent qui fait l’initialisation ;
ligne 32 : on demande à la classe parent de vérifier que tous les attributs de la classe [Database] ont été initialisés. Si ce n’est pas le cas, une exception est lancée ;
ligne 34 : on rend l’instance [Database] qui vient d’être initialisée ;
lignes 37 et au-delà : les getters et setters des attributs de la classe ;
L’entité [TaxAdminData]
L’entité [TaxAdminData] est la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php
namespace Application;
class TaxAdminData extends BaseEntity {
// tranches d'impôt
protected $limites;
protected $coeffR;
protected $coeffN;
// constantes de calcul de l'impôt
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
…
}
|
La classe [TaxAdminData] est utilisée pour encapsuler les données du fichier jSON [taxadmindata.json] suivant :
1 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 | {
"limites": [
9964,
27519,
73779,
156244,
0
],
"coeffR": [
0,
0.14,
0.3,
0.41,
0.45
],
"coeffN": [
0,
1394.96,
5798,
13913.69,
20163.45
],
"plafondQfDemiPart": 1551,
"plafondRevenusCelibatairePourReduction": 21037,
"plafondRevenusCouplePourReduction": 42074,
"valeurReducDemiPart": 3797,
"plafondDecoteCelibataire": 1196,
"plafondDecoteCouple": 1970,
"plafondImpotCouplePourDecote": 2627,
"plafondImpotCelibatairePourDecote": 1595,
"abattementDixPourcentMax": 12502,
"abattementDixPourcentMin": 437
}
|
La classe et le fichier jSON ont les mêmes attributs. Ceux-ci représentent les données de l’administration fiscale. Le reste du code de la classe [TaxAdminData] est le suivant :
1 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 | <?php
namespace Application;
class TaxAdminData extends BaseEntity {
// tranches d'impôt
protected $limites;
protected $coeffR;
protected $coeffN;
// constantes de calcul de l'impôt
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// initialisation
public function setFromJsonFile(string $taxAdminDataFilename) {
// parent
parent::setFromJsonFile($taxAdminDataFilename);
// on vérifie que tous les attributs ont été initialisés
parent::checkForAllAttributes();
// on vérifie que les valeurs des attributs sont des réels >=0
foreach ($this as $key => $value) {
if ($key !== "arrayOfAttributes") {
// $value doit être un nbre réel >=0 ou un tableau de réels >=0
$result = $this->check($value);
// erreur ?
if ($result->erreur) {
// on lance une exception
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// on note la valeur
$this->$key = $result->value;
}
}
}
// on rend l'objet
return $this;
}
protected function check($value): \stdClass {
// $value est un tableau d'éléments de type string ou un unique élément
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// on transforme le tableau de strings en tableau de réels
$newTableau = [];
$result = new \stdClass();
// les éléments du tableau doivent être des nombres décimaux positifs ou nuls
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// on met le float dans newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// on note l'erreur
$result->erreur = TRUE;
// on quitte
return $result;
}
}
// on rend le résultat
$result->erreur = FALSE;
if (!\is_array($value)) {
// une seule valeur
$result->value = $newTableau[0];
} else {
// une liste de valeurs
$result->value = $newTableau;
}
return $result;
}
// getters et setters
…
}
|
Commentaires
ligne 23 : la méthode [setFromJsonFile] sert à initialiser les attributs de la classe [TaxAdminData] à partir d’un fichier jSON passé en paramètre. Il faut que les attributs du fichier jSON existent sous le même nom dans la classe ;
ligne 25 : c’est la classe parent qui fait ce travail ;
ligne 27 : on demande à la classe parent de vérifier que tous les attributs de la classe fille ont été initialisés ;
lignes 29-42 : on vérifie localement que tous les attributs ont eu une valeur réelle positive ou nulle. Cette vérification a déjà été discutée au paragraphe lien de la version 03 ;
La couche [dao]
Maintenant nous pouvons écrire le code qui va transférer les données du fichier texte [taxadmindata.json] dans les tables [tbtranches, tbconstantes] de la base MySQL [dbimpots-2019]. Nous adopterons l’architecture suivante :
La couche [dao] implémentera l’interface [InterfaceDao4TransferAdminDataFromFile2Database] suivante :
1 2 3 4 5 6 7 8 9 | <?php
// espace de noms
namespace Application;
interface InterfaceDao4TransferAdminData2Database {
public function transferAdminData2Database(): void;
}
|
Commentaires
- ligne 8 : la méthode [transferAdminData2Database] a pour rôle de stocker les données de l’administration fiscale dans une base de données ;
L’interface [InterfaceDao4TransferAdminData2Database] sera implémentée par la classe [DaoTransferAdminDataFromJsonFile2Database] suivante :
1 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;
// définition d'une classe TransferAdminDataFromFile2DatabaseDao
class DaoTransferAdminDataFromJsonFile2Database implements InterfaceDao4TransferAdminData2Database {
// attributs de la base de données cible
private $database;
// données de l'administration fiscale
private $taxAdminData;
// constructeur
public function __construct(string $databaseFilename, string $taxAdminDataFilename) {
// on mémorise la configuration de la bd
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// on mémorise les données fiscales
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// transfère les données des tranches d'impôts d'un fichier texte
// vers la base de données
public function transferAdminData2Database(): void {
// on travaille sur la base
$database = $this->database;
try {
// on ouvre la connexion à la base de données
$connexion = new \PDO($database->getDsn(), $database->getId(), $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->fillTableTranches($connexion);
// on remplit la table des constantes
$this->fillTableConstantes($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;
}
}
// remplissage de la table des tranches d'impôt
private function fillTableTranches($connexion): void {
…
}
// remplissage de la table des constantes
private function fillTableConstantes($connexion): void {
…
}
}
|
Commentaires
Nous utilisons ici ce que nous avons appris dans le chapitre sur MySQL.
- ligne 7 : la classe [DaoTransferAdminDataFromJsonFile2Database] implémente l’interface [InterfaceDao4TransferAdminData2Database] ;
- ligne 9 : l’attribut [$database] est l’objet de type [Database] encapsulant les données du fichier [database.json] ;
- ligne 11 : l’attribut [$taxAdminData] est l’objet de type [TaxAdminData] encapsulant les données du fichier [taxadmindata.json] ;
- lignes 14-19 : le constructeur reçoit en paramètres les noms des fichiers [database.json, taxadmindata.json] ;
- ligne 16 : initialisation de l’attribut [$database] ;
- ligne 18 : initialisation de l’attribut [$taxAdminData] ;
- ligne 23 : on implémente l’unique méthode de l’interface [InterfaceDao4TransferAdminData2Database] ;
- lignes 26-38 : on remplit les tables [tbtranches, tbconstantes]
en deux temps :
- ligne 34 : on remplit d’abord la table [tbtranches]. Cela se fait au sein d’une transaction (lignes 32, 38). La méthode [fillTableTranches] (ligne 55) lance une exception dès que quelque chose se passe mal. Dans ce cas, l’exécution se poursuit avec le catch / finally des lignes 39-50 ;
- ligne 36 : on remplit la table [tbconstantes] de la même façon à l’aide de la méthode [fillTableConstantes] (ligne 60) ;
- lignes 39-47 : cas où une exception a été lancée par le code ;
- lignes 41-44 : si une transaction existe, elle est annulée ;
- ligne 46 : on lance une exception de type [ExceptionImpots] avec le message de l’exception originelle qui est, elle, d’un type quelconque ;
- lignes 47-50 : dans la clause [finally], la connexion est fermée ;
Le code de la méthode [fillTableTranches] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private function fillTableTranches($connexion): void {
// raccourci pour la bd
$database = $this->database;
// les données à insérer dans la base de données
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
// on vide la table au cas où il y aurait qq chose dedans
$statement = $connexion->prepare("delete from " . $database->getTableTranches());
$statement->execute();
// on prépare les insertions
$sqlInsert = "insert into {$database->getTableTranches()} "
. "({$database->getColLimites()}, {$database->getColCoeffR()},"
. " {$database->getColCoeffN()}) values (:limites, :coeffR, :coeffN)";
$statement = $connexion->prepare($sqlInsert);
// on exécute l'ordre préparé avec les valeurs des tranches d'impôts
for ($i = 0; $i < count($limites); $i++) {
$statement->execute([
"limites" => $limites[$i],
"coeffR" => $coeffR[$i],
"coeffN" => $coeffN[$i]]);
}
}
|
Commentaires
- ligne 1 : la méthode [fillTableTranches] reçoit en paramètre une connexion ouverte. On sait de plus qu’une transaction a démarré au sein de cette connexion ;
- lignes 5-7 : les valeurs à insérer dans la table sont fournies par l’attribut [$taxAdminData] ;
- lignes 9-10 : on supprime le contenu actuel de la table [tbtranches] ;
- lignes 12-15 : on prépare l’insertion de lignes dans la table. On utilise ici les noms des colonnes fournis par l’attribut [$database] ;
- lignes 17-22 : on exécute autant de fois que nécessaire, l’instruction d’insertion préparée aux lignes 12-15 ;
Le code de la méthode [fillTableConstantes] est le suivant :
1 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 | private function fillTableConstantes($connexion): void {
// raccourci
$database = $this->database;
// on vide la table au cas où il y aurait qq chose dedans
$statement = $connexion->prepare("delete from {$database->getTableConstantes()}");
$statement->execute();
// on prépare l'insertion
$taxAdminData = $this->taxAdminData;
$sqlInsert = "insert into {$database->getTableConstantes()}"
. " ({$database->getColPlafondQfDemiPart()},"
. " {$database->getColPlafondRevenusCelibatairePourReduction()},"
. " {$database->getColPlafondRevenusCouplePourReduction()},"
. " {$database->getColValeurReducDemiPart()},"
. " {$database->getColPlafondDecoteCelibataire()},"
. " {$database->getColPlafondDecoteCouple()},"
. " {$database->getColPlafondImpotCelibatairePourDecote()},"
. " {$database->getColPlafondImpotCouplePourDecote()},"
. " {$database->getColAbattementDixPourcentMax()},"
. " {$database->getColAbattementDixPourcentMin()})"
. " values ("
. ":plafondQfDemiPart,"
. ":plafondRevenusCelibatairePourReduction,"
. ":plafondRevenusCouplePourReduction,"
. ":valeurReducDemiPart,"
. ":plafondDecoteCelibataire,"
. ":plafondDecoteCouple,"
. ":plafondImpotCelibatairePourDecote,"
. ":plafondImpotCouplePourDecote,"
. ":abattementDixPourcentMax,"
. ":abattementDixPourcentMin)";
$statement = $connexion->prepare($sqlInsert);
// on exécute l'ordre préparé
$statement->execute([
"plafondQfDemiPart" => $taxAdminData->getPlafondQfDemiPart(),
"plafondRevenusCelibatairePourReduction" => $taxAdminData->getPlafondRevenusCelibatairePourReduction(),
"plafondRevenusCouplePourReduction" => $taxAdminData->getPlafondRevenusCouplePourReduction(),
"valeurReducDemiPart" => $taxAdminData->getValeurReducDemiPart(),
"plafondDecoteCelibataire" => $taxAdminData->getPlafondDecoteCelibataire(),
"plafondDecoteCouple" => $taxAdminData->getPlafondDecoteCouple(),
"plafondImpotCelibatairePourDecote" => $taxAdminData->getPlafondImpotCelibatairePourDecote(),
"plafondImpotCouplePourDecote" => $taxAdminData->getPlafondImpotCouplePourDecote(),
"abattementDixPourcentMax" => $taxAdminData->getAbattementDixPourcentMax(),
"abattementDixPourcentMin" => $taxAdminData->getAbattementDixPourcentMin()
]);
}
|
Commentaires
ligne 1 : la méthode [fillTableConstantes] reçoit en paramètre une connexion ouverte. On sait de plus qu’une transaction a démarré au sein de cette connexion ;
lignes 5-6 : la table [tbconstantes] est vidée ;
lignes 9-31 : préparation de l’ordre SQL d’insertion. Il est complexe du fait qu’il y a 10 colonnes à initialiser dans cette opération d’insertion et qu’il faut aller chercher les noms des colonnes dans l’attribut [$database] ;
ligne 33-44 : exécution de l’ordre d’insertion. Il n’y a qu’une ligne à insérer. Là encore, le code est rendu complexe du fait qu’il faille chercher les valeurs à insérer dans l’attribut [$taxAdminData] ;
Le script principal
Le script principal s’appuie sur la couche [dao] pour opérer le transfert de données :
1 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 | <?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");
// inclusion interface et classes
require_once __DIR__ . "/../Entities/BaseEntity.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao.php";
require_once __DIR__ . "/../Dao/TraitDao.php";
require_once __DIR__ . "/../Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../Dao/DaoTransferAdminDataFromJsonFile2Database.php";
//
// définition des constantes
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
//
try {
// création de la couche [dao]
$dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
// transfert des données dans la base
$dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fin
print "Terminé\n";
exit;
|
Commentaires
- lignes 12-21 : chargement des classes et interfaces de l’application ;
- lignes 24-24 : les deux fichiers jSON ;
- ligne 30 : on instancie la couche [dao] en passant au constructeur les deux fichiers jSON ;
- ligne 32 : on opère le transfert de données ;
Lorsque nous exécutons ce code, nous obtenons le résultat suivant dans la base de données :
Colonne [3], on voit les valeurs attribuées par MySQL à la clé primaire [id]. La numérotation démarre à 1. La copie d’écran ci-dessus a été obtenue après plusieurs exécutions du script.
Calcul de l’impôt¶
Architecture¶
La version 04 de l’application de calcul d’impôt utilisait une architecture en couches :
La couche [dao] implémente une interface [InterfaceDao]. Nous avons construit une classe implémentant cette interface :
- [DaoImpotsWithTaxAdminDataInJsonFile] qui allait chercher les données fiscales dans un fichier jSON. C’était la version 04 ;
Nous allons implémenter l’interface [InterfaceDao] par une nouvelle classe [DaoImpotsWithTaxAdminDataInDatabase] qui ira chercher les données de l’administration fiscale dans une base de données MySQL. La couche [dao], comme précédemment, écrira les résultats et les erreurs dans des fichiers texte et trouvera les données des contribuables également dans un fichier texte. Seulement cette fois-ci, ces fichiers texte seront des fichiers jSON. Par ailleurs, nous savons que si nous continuons à respecter l’interface [InterfaceDao], la couche [métier] n’aura pas à être modifiée.
L’entité [TaxPayerData]¶
La classe [TaxPayerData] sert à encapsuler dans une classe les données du fichier jSON [taxpayersdata.json] suivant :
1 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 | [
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
|
La classe [TaxPayerData] est la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php
// espace de noms
namespace Application;
// la classe des données
class TaxPayerData extends BaseEntity {
// données nécessaires au calcul de l'impôt du contribuable
protected $marié;
protected $enfants;
protected $salaire;
// résultats du calcul de l'impôt
protected $impôt;
protected $surcôte;
protected $décôte;
protected $réduction;
protected $taux;
// getters et setters
…
}
|
Commentaires
ligne 7 : la classe [TaxPayerData] étend la classe [BaseEntity]. Les méthodes de sa classe parent étant suffisantes, la classe [TaxPayerData] n’en définit pas elle-même. On rappelle que les attributs de la classe [TaxPayerData] sont identiques à ceux du fichier jSON [taxpayersdata.json] ;
La couche [dao]
Le trait [TraitDao]
Le trait [TraitDao] implémente une partie de l’interface [InterfaceDao]. Rappelons celle-ci :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php
// espace de noms
namespace Application;
interface InterfaceDao {
// lecture des données contribuables
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// lecture des données de l'administration fiscale (tranches d'impôts)
public function getTaxAdminData(): TaxAdminData;
// enregistrement des résultats
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
|
Le trait [TraitDao] implémente les méthodes [getTaxPayersData, saveResults] de l’interface [InterfaceDao]. Du fait qu’entre les versions 04 et 05, on a changé la définition de l’entité [TaxPayerData], il nous faut revoir le code de [TraitDao] :
1 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 | <?php
// espace de noms
namespace Application;
trait TraitDao {
// lecture des données contribuables
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// on récupère les données des contribuables dans un tableau
$baseEntity = new BaseEntity();
$baseEntity->setFromJsonFile($taxPayersFilename);
$arrayOfAttributes = $baseEntity->getArrayOfAttributes();
// tableau des données contribuables
$taxPayersData = [];
// tableau des erreurs
$errors = [];
// on boucle sur le tableau des attributs d'élements de type [TaxPayerData]
$i = 0;
foreach ($arrayOfAttributes as $attributesOfTaxPayerData) {
// vérification
$error = $this->check($attributesOfTaxPayerData);
if (!$error) {
// un contribuable de +
$taxPayersData[] = (new TaxPayerData())->setFrOmArrayOfAttributes($attributesOfTaxPayerData);
} else {
// une erreur de + - on note le numéro de la donnée invalide
$error = ["numéro" => $i] + $error;
$errors[] = $error;
}
// suivant
$i++;
}
// on sauve les erreurs dans un fichier json
$string = "";
foreach ($errors as $error) {
$string .= \json_encode($error, JSON_UNESCAPED_UNICODE) . "\n";
}
$this->saveString($errorsFilename, $string);
// résultat de la fonction
return $taxPayersData;
}
private function check(array $attributesOfTaxPayerData): array {
// on vérifie les données de [$taxPayerData]
// la liste des atributs erronés
$attributes = [];
// le statut marital doit être oui ou non
$marié = trim(strtolower($attributesOfTaxPayerData["marié"]));
$erreur = ($marié !== "oui" and $marié !== "non");
if ($erreur) {
// on note l'erreur
$attributes[] = ["marié" => $marié];
}
// le nombre d'enfants doit être un entier positif ou nul
$enfants = trim($attributesOfTaxPayerData["enfants"]);
if (!preg_match("/^\d+$/", $enfants)) {
// on note l'erreur
$erreur = TRUE;
$attributes[] = ["enfants" => $enfants];
} else {
$enfants = (int) $enfants;
}
// le salaire doit être un entier positif ou nul (sans les centimes d'euros)
$salaire = trim($attributesOfTaxPayerData["salaire"]);
if (!preg_match("/^\d+$/", $salaire)) {
// on note l'erreur
$erreur = TRUE;
$attributes[] = ["salaire" => $salaire];
} else {
$salaire = (int) $salaire;
}
// erreur ?
if ($erreur) {
// retour avec erreur
return ["erreurs" => $attributes];
} else {
// retour sans erreur
return [];
}
}
// enregistrement des résultats
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// enregistrement du tableau [$taxPayersData] dans le fichier texte [$resultsFileName]
// si le fichier texte [$resultsFileName] n'existe pas, il est créé
// construction de la chaîne jSON des résultats
$string = "[" . implode(",
", $taxPayersData) . "]";
// enregistrement de cette chaîne
$this->saveString($resultsFilename, $string);
}
// enregistrement d'es résultats d'un tableau dans un fichier texte
private function saveString(string $fileName, string $data): void {
// enregistrement de la chaîne [$data] dans le fichier texte [$fileName]
// si le fichier texte [$fileName] n'existe pas, il est créé
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
}
}
}
|
Commentaires
[TraitDao] implémente les méthodes [getTaxPayersData] (ligne 9) et [saveResults] (ligne 86) de l’interface [InterfaceDao] ;
ligne 9 : la méthode [getTaxPayersData] reçoit en paramètres :
- [$taxPayersFilename] : le nom du fichier jSON des données des contribuables [taxpayersdata.json] ;
- [$errorsFilename] : le nom du fichier jSON des erreurs [errors.json] ;
lignes 11-13 : le contenu du fichier jSON des données des contribuables est transféré dans un tableau associatif [$arrayOfAttributes]. Si le fichier jSON s’avère inexploitable, une exception [ExceptionImpots] a été lancée ;
ligne 15 : le tableau [$taxPayersData] va contenir les données des contribuables encapsulées dans des objets de type [TaxPayerData] ;
ligne 17 : on va cumuler les erreurs dans le tableau [$errors] ;
lignes 99-33 : construction du tableau [$taxPayersData] ;
ligne 22 : avant d’être encapsulées dans un type [TaxPayerData], les données sont vérifiées. La méthode [check] rend :
- un tableau [‘erreurs’=>[…]] avec les attributs erronés si les données sont incorrectes ;
- un tableau vide si les données sont correctes ;
ligne 25 : cas où les données sont valides. Un nouvel objet [TaxPayerData] est construit et ajouté au tableau [$taxPayersData] ;
lignes 26-30 : cas où les données sont invalides. On note dans l’erreur, le n° de l’objet [TaxPayerData] erroné dans le fichier jSON pour que l’utilisateur puisse le retrouver, puis l’erreur est ajoutée au tableau [$errors] ;
lignes 35-39 : on enregistre les erreurs rencontrées dans le fichier jSON [$errorsFilename] passé en paramètre, ligne 9 ;
ligne 41 : on rend le tableau des objets [TaxPayerData] construits : c’était l’objectif de la méthode ;
lignes 44-83 : la méthode privée [check] vérifie la validité des paramètres [marié, enfants, salaire] du tableau [$attributesOfTaxPayerData] passé en paramètre ligne 44. S’il y a des attributs erronés, elle les cumule dans le tableau [$attributes] (lignes 47, 53, 60, 70) sous la forme d’un tableau [‘attribut erroné’=> valeur de l’attribut erroné] ;
ligne 78 : s’il y a des erreurs, on rend un tableau [‘erreurs’=>$attributes] ;
ligne 81 : s’il n’y a pas d’erreurs, on rend un tableau d’erreurs vide ;
lignes 86-93 : implémentation de la méthode [saveResults] de l’interface [InterfaceDao] ;
ligne 90 : on construit la chaîne jSON à enregistrer dans le fichier jSON [$resultsFilename] passé en paramètre ligne 86. on doit construire la chaîne jSON d’un tableau :
- chaque élément du tableau est séparé du suivant par une virgule et un saut de ligne ;
- l’ensemble du tableau est entouré de crochets [] ;
ligne 92 : la chaîne jSON est enregistrée dans le fichier jSON [$resultsFilename] ;
La classe [DaoImpotsWithTaxAdminDataInDatabase]
La classe [DaoImpotsWithTaxAdminDataInDatabase] implémente l’interface [InterfaceDao] de la façon suivante :
1 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 | <?php
// espace de noms
namespace Application;
// définition d'une classe ImpotsWithDataInDatabase
class DaoImpotsWithTaxAdminDataInDatabase implements InterfaceDao {
// usage d'un trait
use TraitDao;
// 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;
}
}
|
Commentaires
- ligne 4 : on garde l’espace de noms déjà utilisé pour les autres implémentations de la couche [dao] ;
- ligne 7 : la classe [DaoImpotsWithTaxAdminDataInDatabase] implémente l’interface [InterfaceDao] ;
- ligne 9 : on importe le trait [TraitDao]. On sait que ce trait implémente une partie de l’interface. La seule méthode qui reste à implémenter est la méthode [getTaxAdminData] des lignes 62-64. Cette méthode se contente de rendre l’attribut privé [taxAdminData] de la ligne 11. On en déduit que le constructeur devra initialiser cet attribut. C’est son unique rôle ;
- ligne 16 : le constructeur reçoit comme unique paramètre [$databaseFilename] qui est le nom du fichier jSON [database.json] qui définit la base de données MySQL [dbimpots-2019] ;
- ligne 18 : le fichier jSON [$databaseFilename] est utilisé pour créer un objet de type [Database] construit et mémorisé dans l’attribut [$database] de la ligne 13. Si le fichier jSON n’a pu être exploité correctement, une exception [ExceptionImpots] a été lancée ;
- ligne 20 : on crée l’objet [$this→taxAdminData] que le constructeur doit initialiser ;
- lignes 22-26 : on ouvre la connexion à la base de données. Notez la notation [PDO] pour désigner la classe [PDO] de PHP. En effet, comme on est dans l’espace de noms [Application], si on écrivait simplement [PDO], ce nom relatif serait préfixé par l’espace de noms courant et donnerait donc la classe [ApplicationPDO] qui n’existe pas ;
- ligne 28 : lors d’une erreur, le SGBD lancera une \PDOException (ligne 37) ;
- ligne 30 : on démarre une transaction. Celle-ci n’est pas vraiment utile car seuls deux ordres SQL vont être exécutés, ordres qui ne modifient pas la base. On le fait néanmoins pour s’isoler des autres utilisateurs de la base ;
- ligne 32 : la lecture de la table des tranches d’imposition [tbtranches] est faite par la méthode privée [getTranches] de la ligne 52 ;
- ligne 34 : la lecture de la table des constantes de calcul [tbconstantes] est faite par la méthode privée [getConstantes] de la ligne 57 ;
- ligne 36 : si on arrive à cette ligne c’est que tout s’est bien passé. On valide donc la transaction;
- lignes 37-42 : si on arrive là c’est qu’une exception s’est produite. On invalide donc la transaction s’il y en avait une en cours (lignes 39-42). Ligne 44, pour avoir des exceptions homogènes, on relance le message de l’exception reçue sous la forme cette fois d’une exception de type [ExceptionImpots] ;
- lignes 45-48 : dans tous les cas (exception ou pas) on ferme la connexion ;
La méthode [getTranches] est la suivante :
1 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 | private function getTranches($connexion): void {
// raccourcis
$database = $this->database;
$taxAdminData = $this->taxAdminData;
// on prépare la requête SELECT
$statement = $connexion->prepare(
"select {$database->getColLimites()}," .
" {$database->getColCoeffR()}," .
" {$database->getColCoeffN()}" .
" from {$database->getTableTranches()}");
// on exécute l'ordre préparé avec les valeurs des tranches d'impôts
$statement->execute();
// on exploite le résultat
$limites = [];
$coeffR = [];
$coeffN = [];
// remplissage des trois tableaux
while ($tranche = $statement->fetch(\PDO::FETCH_OBJ)) {
$limites[] = (float) $tranche->{$database->getColLimites()};
$coeffR[] = (float) $tranche->{$database->getColCoeffR()};
$coeffN[] = (float) $tranche->{$database->getColCoeffN()};
}
// on mémorise les données dans l'attribut [$taxAdminData] de la classe
$taxAdminData->setFromArrayOfAttributes([
"limites" => $limites,
"coeffR" => $coeffR,
"coeffN" => $coeffN
]);
}
|
Commentaires
- ligne 1 : la méthode reçoit en paramètre [$connexion] qui est une connexion ouverte et dans laquelle une transaction est en cours ;
- lignes 2-4 : on crée deux raccourcis pour éviter d’avoir à écrire [$this->database] et [$taxAdminData = $this->taxAdminData] dans tout le code. On a là des copies de références d’objets et non pas une copie des objets eux-mêmes ;
- ligne 6-10 : l’ordre SELECT est préparé, puis exécuté en ligne 12 ;
- lignes 13-22 : le résultat du SELECT est exploité. Les informations reçues sont cumulées dans trois tableaux [limites, coeffR, coeffN] ;
- lignes 24-28 : les trois tableaux sont utilisés pour initialiser l’attribut [$this->taxAdminData] de la classe ;
La méthode privée [getConstantes] est la suivante :
1 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 | private function getConstantes($connexion): void {
// raccourcis
$database = $this->database;
$taxAdminData = $this->taxAdminData;
// on prépare la requête SELECT
$select = "select {$database->getColPlafondQfDemiPart()}," .
"{$database->getColPlafondRevenusCelibatairePourReduction()}," .
"{$database->getColPlafondRevenusCouplePourReduction()}," . "{$database->getColValeurReducDemiPart()}," .
"{$database->getColPlafondDecoteCelibataire()}," . "{$database->getColPlafondDecoteCouple()}," .
"{$database->getColPlafondImpotCelibatairePourDecote()}," . "{$database->getColPlafondImpotCouplePourDecote()}," .
"{$database->getColAbattementDixPourcentMax()}," . "{$database->getColAbattementDixPourcentMin()}" .
" from {$database->getTableConstantes()}";
$statement = $connexion->prepare($select);
// on exécute l'ordre préparé
$statement->execute();
// on exploite le résultat - 1 seule ligne ici
$row = $statement->fetch(\PDO::FETCH_OBJ);
// on initialise l'attribut [$taxAdminData]
$taxAdminData->setPlafondQfDemiPart($row->{$database->getColPlafondQfDemiPart()});
$taxAdminData->setPlafondRevenusCelibatairePourReduction(
$row->{$database->getColPlafondRevenusCelibatairePourReduction()});
$taxAdminData->setPlafondRevenusCouplePourReduction($row->{$database->getColPlafondRevenusCouplePourReduction()});
$taxAdminData->setValeurReducDemiPart($row->{$database->getColValeurReducDemiPart()});
$taxAdminData->setPlafondDecoteCelibataire($row->{$database->getColPlafondDecoteCelibataire()});
$taxAdminData->setPlafondDecoteCouple($row->{$database->getColPlafondDecoteCouple()});
$taxAdminData->setPlafondImpotCelibatairePourDecote($row->{$database->getColPlafondImpotCelibatairePourDecote()});
$taxAdminData->setPlafondImpotCouplePourDecote($row->{$database->getColPlafondImpotCouplePourDecote()});
$taxAdminData->setAbattementDixPourcentMax($row->{$database->getColAbattementDixPourcentMax()});
$taxAdminData->setAbattementDixPourcentMin($row->{$database->getColAbattementDixPourcentMin()});
}
|
Commentaires
- ligne 1 : la méthode reçoit en paramètre [$connexion] qui est une connexion ouverte et dans laquelle une transaction est en cours ;
- lignes 2-4 : on crée deux raccourcis pour éviter d’avoir à écrire [$this->database] et [$taxAdminData = $this->taxAdminData] dans tout le code. On a là des copies de références d’objets et non pas une copie des objets eux-mêmes ;
- ligne 6-15 : l’ordre SELECT est préparé, puis exécuté en ligne 15 ;
- lignes 17-29 : le résultat du SELECT est exploité. Les informations récupérées sont utilisées pour initialiser l’attribut [$this->taxAdminData] de la classe ;
Note : on remarquera que la classe ne dépend pas du SGBD MySQL. C’est le code appelant qui fixe le SGBD utilisé via le DSN de la base de données.
La couche [métier]¶
- nous venons d’implémenter la couche [dao] (3) ;
- parce que nous avons respecté l’interface [InterfaceDao], la couche [métier] (2) peut en théorie rester inchangée. Cependant, nous n’avons pas seulement modifié la couche [dao]. Nous avons également modifié les entités qui elles sont partagées par toutes les couches ;
La couche [métier] implémente l’interface [InterfaceMetier] suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php
// espace de noms
namespace Application;
interface InterfaceMetier {
// 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 12 : la méthode [executeBatchImpots] utilise désormais le fichier jSON [$taxPayersFileName] alors que dans la version 04, c’était un fichier texte basique. ;
Dans la version 04, la méthode [executeBatchImpots] était la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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->dao->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->setMontant($result["impôt"]);
$taxPayerData->setDécôte($result["décôte"]);
$taxPayerData->setSurCôte($result["surcôte"]);
$taxPayerData->setTaux($result["taux"]);
$taxPayerData->setRéduction($result["réduction"]);
// on met le résultat dans le tableau des résultats
$results [] = $taxPayerData;
}
// enregistrement des résultats
$this->dao->saveResults($resultsFileName, $results);
}
|
- la ligne 15 est désormais erronée. Dans la nouvelle définition de la classe [TaxPayerData], la méthode [setMontant] n’existe plus ;
Dans la version 05, la méthode [executeBatchImpots] sera la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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->dao->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->dao->saveResults($resultsFileName, $results);
}
|
Commentaires
ligne 15 : au lieu d’utiliser les setters individuels de la la classe [TaxPayerData], on utilise son setter global [setFromArrayOfAttributes] ;
le reste du code n’a pas à être modifié ;
Le script principal
- nous venons d’implémenter les couches [dao] (3) et [métier] (2) ;
- il nous reste à écrire le script principal (1) ;
Le script principal est analogue à celui de la version 04 :
1 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
// 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");
// inclusion interface et classes
require_once __DIR__ . "/../Entities/BaseEntity.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao.php";
require_once __DIR__ . "/../Dao/TraitDao.php";
require_once __DIR__ . "/../Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once __DIR__ . "/../Métier/InterfaceMetier.php";
require_once __DIR__ . "/../Métier/Metier.php";
//
// définition des constantes
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
const RESULTS_FILENAME = "../Data/resultats.json";
const ERRORS_FILENAME = "../Data/errors.json";
const TAXPAYERSDATA_FILENAME = "../Data/taxpayersdata.json";
try {
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// création de la couche [métier]
$métier = new Metier($dao);
// calcul de l'impôts en mode batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fin
print "Terminé\n";
exit;
|
Commentaires
- lignes 12-22 : chargement de tous les fichiers de la version 05;
- lignes 25-29 : les noms des différents fichiers jSON de l’application ;
- ligne 33 : construction de la couche [dao] ;
- ligne 35 : construction de la couche [métier] ;
- ligne 37 : appel de la méthode [executeBatchImpots] de la couche [métier] ;
Résultats
L’application produit deux fichiers jSON :
- [resultats.json] : les résultats des différents calcul d’impôts ;
- [errors.json] : qui signale les erreurs trouvées dans le fichier jSON [taxpayersdata.json] ;
Le fichier [errors.json] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
|
Cela signifie que dans [taxpayersdata.json], l’élément n° 1 du tableau des contribuables est erroné. Le fichier [taxpayersdata.json] était le suivant :
1 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 | [
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
|
Le fichier des résultats [resultats.json] est lui le suivant :
1 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 | [
{
"marié": "oui",
"enfants": 2,
"salaire": 55555,
"impôt": 2814,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000,
"impôt": 1384,
"surcôte": 0,
"décôte": 384,
"réduction": 347,
"taux": 0.14
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000,
"impôt": 0,
"surcôte": 0,
"décôte": 720,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000,
"impôt": 19884,
"surcôte": 4480,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000,
"impôt": 16782,
"surcôte": 7176,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000,
"impôt": 9200,
"surcôte": 2180,
"décôte": 0,
"réduction": 0,
"taux": 0.3
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000,
"impôt": 4230,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000,
"impôt": 22986,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000,
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000,
"impôt": 64210,
"surcôte": 7498,
"décôte": 0,
"réduction": 0,
"taux": 0.45
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000,
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
}
]
|
Ces résultats sont conformes à ceux de la version 04.
Tests [Codeception]¶
Comme il a été fait au paragraphe lien pour la version 04, nous allons écrire des tests [Codeception] pour la version 05.
Test de la couche [dao]¶
Le test [DaoTest.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// répertoires racines
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/Entities/BaseEntity.php";
require_once ROOT . "/Entities/TaxAdminData.php";
require_once ROOT . "/Entities/TaxPayerData.php";
require_once ROOT . "/Entities/Database.php";
require_once ROOT . "/Entities/ExceptionImpots.php";
require_once ROOT . "/Utilities/Utilitaires.php";
require_once ROOT . "/Dao/InterfaceDao.php";
require_once ROOT . "/Dao/TraitDao.php";
require_once ROOT . "/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/Métier/InterfaceMetier.php";
require_once ROOT . "/Métier/Metier.php";
// bibliothèques tierces
require_once VENDOR . "/autoload.php";
// définition des constantes
const DATABASE_CONFIG_FILENAME = ROOT ."/Data/database.json";
const TAXADMINDATA_FILENAME = ROOT ."/Data/taxadmindata.json";
const RESULTS_FILENAME = ROOT ."/Data/resultats.json";
const ERRORS_FILENAME = ROOT ."/Data/errors.json";
const TAXPAYERSDATA_FILENAME = ROOT ."/Data/taxpayersdata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
// constantes de calcul
$this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
…
}
}
|
Commentaires
- lignes 9-33 : définition de l’environnement du test. Nous utilisons le même que celui utilisé par le script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] décrit au paragraphe lien ;
- lignes 39-44 : construction de la couche [dao] ;
- ligne 43 : l’attribut [$this→taxAdminData] contient les données à tester ;
- lignes 47-51 : la méthode [testTaxAdminData] est celle décrite au paragraphe lien ;
Les résultats du test sont les suivants :
Test de la couche [métier]¶
Le test [MetierTest.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// répertoires racines
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/Entities/BaseEntity.php";
require_once ROOT . "/Entities/TaxAdminData.php";
require_once ROOT . "/Entities/TaxPayerData.php";
require_once ROOT . "/Entities/Database.php";
require_once ROOT . "/Entities/ExceptionImpots.php";
require_once ROOT . "/Utilities/Utilitaires.php";
require_once ROOT . "/Dao/InterfaceDao.php";
require_once ROOT . "/Dao/TraitDao.php";
require_once ROOT . "/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/Métier/InterfaceMetier.php";
require_once ROOT . "/Métier/Metier.php";
// bibliothèques tierces
require_once VENDOR . "/autoload.php";
// définition des constantes
const DATABASE_CONFIG_FILENAME = ROOT ."/Data/database.json";
const TAXADMINDATA_FILENAME = ROOT ."/Data/taxadmindata.json";
const RESULTS_FILENAME = ROOT ."/Data/resultats.json";
const ERRORS_FILENAME = ROOT ."/Data/errors.json";
const TAXPAYERSDATA_FILENAME = ROOT ."/Data/taxpayersdata.json";
class MetierTest extends \Codeception\Test\Unit {
// couche métier
private $métier;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// création de la couche [métier]
$this->métier = new Metier($dao);
}
// tests
public function test1() {
$result = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
…………………………………………………………………………………………………………………..
public function test11() {
$result = $this->métier->calculerImpot("oui", 3, 200000);
$this->assertEqualsWithDelta(42842, $result["impôt"], 1);
$this->assertEqualsWithDelta(17283, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
}
|
Commentaires
- lignes 9-33 : définition de l’environnement du test. Nous utilisons le même que celui utilisé par le script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] décrit au paragraphe lien ;
- lignes 39-45 : construction des couches [dao] et [métier] ;
- ligne 44 : l’attribut [$this→métier] référence la couche [métier] ;
- lignes 47-64 : les méthodes [test1, test2…, test11] sont celles décrites au paragraphe lien ;
Les résultats du test sont les suivants :
Exercice d’application – version 6¶
Nous venons d’implémenter la structure en couches suivante :
Le SGBD utilisé dans les exemples était MySQL. Au paragraphe lien nous avions remarqué que rien dans la classe implémentant la couche [dao] ne laissait supposer qu’on utilisait un SGBD particulier. C’est ce que nous allons vérifier maintenant en utilisant un autre SGBD, le SGBD PostgreSQL. L’architecture en couches devient la suivante :
Installation du SGBD PostgreSQL¶
Les distributions du SGBD PostgreSQL sont disponibles à l’URL [https://www.postgresql.org/download/] (mai 2019). Nous montrons l’installation de la version pour Windows 64 bits :
- en [1-4], on télécharge l’installateur du SGBD ;
On lance l’installateur téléchargé :
- en [6], indiquez un dossier d’installation ;
- en [8], l’option [Stack Builder] est inutile pour ce qu’on veut faire ici ;
- en [10], laissez la valeur qui vous sera présentée ;
- en [12-13], on a mis ici le mot de passe [root]. Ce sera le mot de passe de l’administrateur du SGBD qui s’appelle [postgres]. PostgreSQL l’appelle également le super-utilisateur ;
- en [15], laissez la valeur par défaut : c’est le port d’écoute du SGBD ;
- en [17], laissez la valeur par défaut ;
- en [19], le résumé de la configuration de l’installation ;
Sous windows, le SGBD PostgreSQL est installé comme un service windows lancé automatiquement. La plupart du temps ce n’est pas souhaitable. Nous allons modifier cette configuration. Tapez [services] dans la barre de recherche de Windows [24-26] :
- en [29], on voit que le service du SGBD PostgreSQL est en mode automatique. On change cela en accédant aux propriétés du service [30] :
- en [31-32], mettez le démarrage en mode manuel ;
- en [33], arrêtez le service ;
Lorsque vous voudrez démarrer manuellement le SGBD, revenez à l’application [services], cliquez droit sur le service [postgresql] (34) et lancez le (35).
Activation de l’extension PDO du SGBD PostgreSQL¶
Nous allons modifier le fichier [php.ini] qui configure PHP (cf paragraphe lien) :
- en [2], vérifiez que l’extension PDO de PostgreSQL est activée. Ceci fait, sauvegardez la modification puis relancez Laragon pour être sûrs que la modification va être prise en compte. Ensuite vérifiez la configuration de PHP directement à partir de Laragon [3-5].
Administrer PostgreSQL avec l’outil [pgAdmin]¶
Lancez le service windows du SGBD PostgreSQL (cf paragraphe lien). Puis de la même façon que vous avez lancé l’outil [services], lancez l’outil [pgadmin] qui permet d’administrer le SGBD PostgreSQL [1-3] :
Il est possible qu’à un moment donné on vous demande le mot de passe du super-utilisateur. Celui-ci s’appelle [postgres]. Vous avez défini son mot de passe lors de l’installation du SGBD. Dans ce document, nous avons donné le mot de passe [root] au super-utilisateur lors de l’installation.
- en [4], [pgAdmin] est une application web ;
- en [5], la liste des serveurs PostgreSQL détectés par [pgAdmin], ici 1 ;
- en [6], le serveur PostgreSQL que nous avons lancé ;
- en [7], les bases de données du SGBD, ici 1 ;
- en [8], la base [postgresql] est gérée par le super-utilisateur [postgres] ;
Créons tout d’abord un utilisateur [admimpots] avec le mot de passe [mdpimpots] :
- en [17], on a mis [mdpimpots] ;
- en [21], le code SQL que va émettre l’outil [pgAdmin] vers le SGBD PostgreSQL. C’est une façon d’apprendre le langage SQL propriétaire de PostgreSQL ;
- en [22], après validation de l’assistant [Save], l’utilisateur [admimpots] a été créé ;
Maintenant nous créons la base [dbimpots-2019] :
On clique droit sur [23], puis [24-25] pour créer une nouvelle base de données. Dans l’onglet [26], on définit le nom de la base [27] et son propriétaire [admimpots] [28].
- en [30], le code SQL de création de la base ;
- en [31], après validation de l’assistant [Save], la base [dbimpots-2019] est créée ;
Maintenant, nous allons créer la table [tbtranches] avec les colonnes [id, limites, coeffr, coeffn]. Une particularité de PostgreSQL est que les noms de colonnes sont sensibles à la casse (majuscules / minuscules) ce qui n’est habituellement pas le cas avec les autres SGBD. Ainsi avec MySQL, l’ordre [select limites, coeffR, coeffN from tbtranches] fonctionnera même si les colonnes réelles de la table [tbtranches] sont [LIMITES, COEFFR, COEFFN]. Avec PostgreSQL, l’ordre SQL ne fonctionnera pas. On pourrait alors écrire [select LIMITES, COEFFR, COEFFN from tbtranches] mais ça ne fonctionnera toujours pas, car PostgreSQL va exécuter l’ordre [select limites, coeffr, coeffn from tbtranches] : il passe par défaut les noms des colonnes en minuscules. Pour qu’il ne fasse pas cela, il faut écrire : [select « LIMITES », « COEFFR », « COEFFN » from tbtranches], ç-à-d qu’il faut protéger les noms des colonnes avec des guillemets. Pour ces raisons, nous allons donner aux colonnes des noms en minuscules. Les noms des objets d’une base de données peuvent être une source d’incompatibilité entre SGBD, certains noms étant des mots réservés dans certains SGBD et pas dans d’autres.
Nous créons la table [tbtranches] :
- utilisez le bouton [40] pour créer des colonnes ;
- après avoir terminé l’assistant de création par [Save], la table [tbtranches] est créée [52-53] ;
Il nous faut indiquer au SGBD qu’il doit lui-même générer la clé primaire [id] lors de l’insertion d’une ligne dans la table :
- en [56] on accède aux propriétés de la clé primaire [id] ;
- en [59], on indique que la colonne est de type [Identity]. Cela va entraîner que le SGBD va générer les valeurs de la clé primaire ;
- en [62], le code SQL généré pour cette opération ;
La table [tbtranches] est désormais prête.
Nous refaisons les mêmes opérations pour créer la table [tbconstantes]. Nous donnons le résultat à obtenir :
La base [dbimpots-2019] est désormais prête. Nous allons la remplir avec des données.
Comme nous l’avons fait avec MySQL, il est possible d’exporter la base de données [dbimpots-2019] dans un fichier SQL. On peut ensuite importer ce fichier SQL pour recréer la base si on l’a perdue ou détériorée. Nous n’exporterons ici que la structure de la base et non ses données :
Le fichier généré est le suivant :
1 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 | --
-- PostgreSQL database dump
--
-- Dumped from database version 11.2
-- Dumped by pg_dump version 11.2
-- Started on 2019-07-04 08:20:31
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- TOC entry 198 (class 1259 OID 16408)
-- Name: tbconstantes; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.tbconstantes (
plafond_qf_demi_part double precision NOT NULL,
id integer NOT NULL,
plafond_revenus_celibataire_pour_reduction double precision NOT NULL,
plafond_revenus_couple_pour_reduction double precision NOT NULL,
valeur_reduc_demi_part double precision NOT NULL,
plafond_decote_celibataire double precision NOT NULL,
plafond_decote_couple double precision NOT NULL,
plafond_impot_celibataire_pour_decote double precision NOT NULL,
plafond_impot_couple_pour_decote double precision NOT NULL,
abattement_dix_pourcent_max double precision NOT NULL,
abattement_dix_pourcent_min double precision NOT NULL
);
ALTER TABLE public.tbconstantes OWNER TO postgres;
--
-- TOC entry 199 (class 1259 OID 16411)
-- Name: tbconstantes_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
ALTER TABLE public.tbconstantes ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
SEQUENCE NAME public.tbconstantes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);
--
-- TOC entry 196 (class 1259 OID 16399)
-- Name: tbtranches; Type: TABLE; Schema: public; Owner: admimpots
--
CREATE TABLE public.tbtranches (
limites double precision NOT NULL,
id integer NOT NULL,
coeffr double precision NOT NULL,
coeffn double precision NOT NULL
);
ALTER TABLE public.tbtranches OWNER TO admimpots;
--
-- TOC entry 197 (class 1259 OID 16404)
-- Name: tbimpots_id_seq; Type: SEQUENCE; Schema: public; Owner: admimpots
--
ALTER TABLE public.tbtranches ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
SEQUENCE NAME public.tbimpots_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);
--
-- TOC entry 2694 (class 2606 OID 16429)
-- Name: tbconstantes tbconstantes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.tbconstantes
ADD CONSTRAINT tbconstantes_pkey PRIMARY KEY (id);
--
-- TOC entry 2692 (class 2606 OID 16403)
-- Name: tbtranches tbimpots_pkey; Type: CONSTRAINT; Schema: public; Owner: admimpots
--
ALTER TABLE ONLY public.tbtranches
ADD CONSTRAINT tbimpots_pkey PRIMARY KEY (id);
--
-- TOC entry 2821 (class 0 OID 0)
-- Dependencies: 198
-- Name: TABLE tbconstantes; Type: ACL; Schema: public; Owner: postgres
--
GRANT ALL ON TABLE public.tbconstantes TO admimpots;
-- Completed on 2019-07-04 08:20:32
--
-- PostgreSQL database dump complete
--
|
Remplissage de la table [tbtranches]¶
Nous avons déjà fait ce travail avec le SGBD MySQL au paragraphe lien. Il nous suffit de modifier le fichier [database.json] qui décrit la base de données :
Le fichier [database.json] devient le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {
"dsn": "pgsql:host=localhost;dbname=dbimpots-2019",
"id": "admimpots",
"pwd": "mdpimpots",
"tableTranches": "public.tbtranches",
"colLimites": "limites",
"colCoeffR": "coeffr",
"colCoeffN": "coeffn",
"tableConstantes": "public.tbconstantes",
"colPlafondQfDemiPart": "plafond_qf_demi_part",
"colPlafondRevenusCelibatairePourReduction": "plafond_revenus_celibataire_pour_reduction",
"colPlafondRevenusCouplePourReduction": "plafond_revenus_couple_pour_reduction",
"colValeurReducDemiPart": "valeur_reduc_demi_part",
"colPlafondDecoteCelibataire": "plafond_decote_celibataire",
"colPlafondDecoteCouple": "plafond_decote_couple",
"colPlafondImpotCelibatairePourDecote": "plafond_impot_celibataire_pour_decote",
"colPlafondImpotCouplePourDecote": "plafond_impot_couple_pour_decote",
"colAbattementDixPourcentMax": "abattement_dix_pourcent_max",
"colAbattementDixPourcentMin": "abattement_dix_pourcent_min"
}
|
- ligne 2 : le DSN a changé, [pgsql] indiquant qu’on a affaire au SGBD Postgres ;
- lignes 5 et 9 : on a précédé le nom des tables par le nom du schéma auquel elles appartiennent [public]. Ce n’était pas indispensable puisque que [public] est le schéma utilisé par défaut lorsqu’aucun schéma n’est précisé dans le nom de la table ;
- lignes 6-8, 10-19 : les noms des colonnes ont changé ;
Le script [MainTransferAdminDataFromJsonFile2PostgresDatabase.php] de remplissage de la base [dbimpots-2019] est le suivant :
1 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 | <?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");
// inclusion interface et classes
require_once __DIR__ . "/../../version-05/Entities/BaseEntity.php";
require_once __DIR__ . "/../../version-05/Entities/TaxAdminData.php";
require_once __DIR__ . "/../../version-05/Entities/TaxPayerData.php";
require_once __DIR__ . "/../../version-05/Entities/Database.php";
require_once __DIR__ . "/../../version-05/Entities/ExceptionImpots.php";
require_once __DIR__ . "/../../version-05/Utilities/Utilitaires.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao.php";
require_once __DIR__ . "/../../version-05/Dao/TraitDao.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../../version-05/Dao/DaoTransferAdminDataFromJsonFile2Database.php";
//
// définition des constantes
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
//
try {
// création de la couche [dao]
$dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
// transfert des données dans la base
$dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fin
print "Terminé\n";
exit;
|
Commentaires
Seules les lignes 12-21 qui chargent les fichiers nécessaires à l’exécution de l’application changent. Elles changent parce que la valeur [__DIR__] change : elle désigne désormais le dossier [version-07/Main].
Lorsqu’on exécute ce script, on obtient le résultat suivant dans la table [tbtranches] :
- on clique droit sur [1], puis ensuite [2-3] ;
- en [4], on a bien les données des tranches d’impôts ;
On refait la même chose pour la table des constantes [tbconstantes] :
On notera que pour l’exécution du script, l’application Laragon n’a pas besoin d’être active : on n’a besoin ni du serveur Apache, ni du SGBD MySQL. On a seulement besoin du SGBD PostgreSQL dont on a lancé le service windows.
Calcul de l’impôt¶
Les couches [dao] (3) et [métier] (2) ont déjà été écrites. Nous avons déjà écrit le script principal pour le SGBD MySQL au paragraphe lien. Il nous suffit de reprendre le script [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase.php] et de l’adapter au SGBD PostgreSQL. Il s’appelle désormais [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php] :
Le script [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php] est le suivant :
1 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
// 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");
// inclusion interface et classes
require_once __DIR__ . "/../../version-05/Entities/BaseEntity.php";
require_once __DIR__ . "/../../version-05/Entities/TaxAdminData.php";
require_once __DIR__ . "/../../version-05/Entities/TaxPayerData.php";
require_once __DIR__ . "/../../version-05/Entities/Database.php";
require_once __DIR__ . "/../../version-05/Entities/ExceptionImpots.php";
require_once __DIR__ . "/../../version-05/Utilities/Utilitaires.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao.php";
require_once __DIR__ . "/../../version-05/Dao/TraitDao.php";
require_once __DIR__ . "/../../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once __DIR__ . "/../../version-05/Métier/InterfaceMetier.php";
require_once __DIR__ . "/../../version-05/Métier/Metier.php";
//
// définition des constantes
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
const RESULTS_FILENAME = "../Data/resultats.json";
const ERRORS_FILENAME = "../Data/errors.json";
const TAXPAYERSDATA_FILENAME = "../Data/taxpayersdata.json";
try {
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// création de la couche [métier]
$métier = new Metier($dao);
// calcul de l'impôts en mode batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fin
print "Terminé\n";
exit;
|
Commentaires
Seules les lignes 12-22 qui chargent les fichiers nécessaires à l’exécution de l’application changent. Elles changent parce que la valeur [__DIR__] change : elle désigne désormais le dossier [version-07/Main].
Résultats d’exécution
Les mêmes que ceux obtenus dans les versions précédentes.
Tests [Codeception]¶
Comme pour les versions précédentes, nous validons cette version avec des tests [Codeception] :
Test de la couche [dao]¶
Le test [DaoTest.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// répertoires racines
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-06");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/../version-05/Entities/BaseEntity.php";
require_once ROOT . "/../version-05/Entities/TaxAdminData.php";
require_once ROOT . "/../version-05/Entities/TaxPayerData.php";
require_once ROOT . "/../version-05/Entities/Database.php";
require_once ROOT . "/../version-05/Entities/ExceptionImpots.php";
require_once ROOT . "/../version-05/Utilities/Utilitaires.php";
require_once ROOT . "/../version-05/Dao/InterfaceDao.php";
require_once ROOT . "/../version-05/Dao/TraitDao.php";
require_once ROOT . "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
// bibliothèques tierces
require_once VENDOR . "/autoload.php";
// définition des constantes
const DATABASE_CONFIG_FILENAME = ROOT ."../Data/database.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
|
Commentaires
- lignes 9-28 : définition de l’environnement du test. Nous utilisons le même, sans la couche [métier], que celui utilisé par le script principal [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] décrit au paragraphe lien ;
- lignes 34-39 : construction de la couche [dao] ;
- ligne 38 : l’attribut [$this→taxAdminData] contient les données à tester ;
- lignes 42-44 : la méthode [testTaxAdminData] est celle décrite au paragraphe lien ;
Les résultats du test sont les suivants :
Test de la couche [métier]¶
Le test [MetierTest.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// répertoires racines
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-06");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/../version-05/Entities/BaseEntity.php";
require_once ROOT . "/../version-05/Entities/TaxAdminData.php";
require_once ROOT . "/../version-05/Entities/TaxPayerData.php";
require_once ROOT . "/../version-05/Entities/Database.php";
require_once ROOT . "/../version-05/Entities/ExceptionImpots.php";
require_once ROOT . "/../version-05/Utilities/Utilitaires.php";
require_once ROOT . "/../version-05/Dao/InterfaceDao.php";
require_once ROOT . "/../version-05/Dao/TraitDao.php";
require_once ROOT . "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/../version-05/Métier/InterfaceMetier.php";
require_once ROOT . "/../version-05/Métier/Metier.php";
// bibliothèques tierces
require_once VENDOR . "/autoload.php";
// définition des constantes
const DATABASE_CONFIG_FILENAME = ROOT . "../Data/database.json";
class MetierTest extends \Codeception\Test\Unit {
// couche métier
private $métier;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// création de la couche [métier]
$this->métier = new Metier($dao);
}
// tests
public function test1() {
…
}
--------------------------------------------------------------------
public function test11() {
…
}
}
|
Commentaires
- lignes 9-28 : définition de l’environnement du test. Nous utilisons le même que celui utilisé par le script principal [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] décrit au paragraphe lien ;
- lignes 34-40 : construction des couches [dao] et [métier] ;
- ligne 39 : l’attribut [$this→métier] référence la couche [métier]
- lignes 43-49 : les méthodes [test1, test2…, test11] sont celles décrites au paragraphe lien ;
Les résultats du test sont les suivants :
Exercice d’application – version 7¶
Implémentation¶
Nous allons ici reprendre la version 6 en externalisant dans un fichier de configuration les constantes utilisées dans les scripts principaux. Le fichier de configuration sera un fichier jSON dont le contenu sera le suivant :
1 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 | {
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-07",
"databaseFilename": "Data/database.json",
"taxAdminDataFileName": "Data/taxadmindata.json",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": {
"BaseEntity": "/../version-05/Entities/BaseEntity.php",
"TaxAdminData": "/../version-05/Entities/TaxAdminData.php",
"TaxPayerData": "/../version-05/Entities/TaxPayerData.php",
"Database": "/../version-05/Entities/Database.php",
"ExceptionImpots": "/../version-05/Entities/ExceptionImpots.php",
"Utilitaires": "/../version-05/Utilities/Utilitaires.php",
"InterfaceDao": "/../version-05/Dao/InterfaceDao.php",
"TraitDao": "/../version-05/Dao/TraitDao.php",
"InterfaceDao4TransferAdminData2Database": "/../version-05/Dao/InterfaceDao4TransferAdminData2Database.php",
"DaoTransferAdminDataFromJsonFile2Database": "/../version-05/Dao/DaoTransferAdminDataFromJsonFile2Database.php",
"DaoImpotsWithTaxAdminDataInDatabase": "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php",
"InterfaceMetier": "/../version-05/Métier/InterfaceMetier.php",
"Metier": "/../version-05/Métier/Metier.php"
},
"dependencies4calculate": [
"BaseEntity",
"TaxAdminData",
"TaxPayerData",
"Database",
"ExceptionImpots",
"Utilitaires",
"InterfaceDao",
"TraitDao",
"DaoImpotsWithTaxAdminDataInDatabase",
"InterfaceMetier",
"Metier"],
"dependencies4transfer": [
"BaseEntity",
"TaxAdminData",
"Database",
"ExceptionImpots",
"Utilitaires",
"InterfaceDao",
"TraitDao",
"InterfaceDao4TransferAdminData2Database",
"DaoTransferAdminDataFromJsonFile2Database"]
}
|
Dans cette configuration :
- ligne 2 : le dossier à partir duquel tous les chemins de ce fichier de configuration sont mesurés ;
- lignes 3-7 : les chemins de tous les fichiers jSON de l’application ;
- lignes 8-22 : les chemins de tous les fichiers de l’application, sous la formé [clé=>chemin] ;
- lignes 23-34 : les dépendances pour le calcul de l’impôt sous la forme d’une liste de clés du dictionnaire [dependencies] (lignes 8-22) ;
- lignes 35-44 : les dépendances pour le transfert en base des données du fichier jSON [taxadmindata.json] sous la forme d’une liste de clés du dictionnaire [dependencies] (lignes 8-22) ;
Le script de transfert en base des données de l’administration fiscale [MainTransferAdminDataFromFile2PostgresSQLDatabase.php] devient le suivant :
1 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 | <?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.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["dependencies4transfer"] as $dependency) {
require $rootDirectory . $config["dependencies"][$dependency];
}
// définition des constantes
define("DATABASE_CONFIG_FILENAME", "$rootDirectory/{$config["databaseFilename"]}");
define("TAXADMINDATA_FILENAME", "$rootDirectory/{$config["taxAdminDataFileName"]}");
//
try {
// création de la couche [dao]
$dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
// transfert des données dans la base
$dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;
|
Commentaires
Le code reste celui qu’il était au paragraphe lien. La seule différence est l’exploitation du fichier [config.json] en lieu et place des constantes aux lignes 18-26 ;
- ligne 16 : la fonction [file_get_contents] transfère le fichier [config.json] dans une chaîne de caractères. La fonction [json_decode] exploite ensuite cette chaîne pour construire le dictionnaire [$config]. Le second paramètre [true] de la fonction [json_decode] indique qu’on veut construire un dictionnaire ;
- lignes 19-22 : on inclut les dépendances nécessaires au script de
transfert des données du fichier [taxadmindata.json] vers la base
de données ;
- [$config[« dependencies4transfer »]] est le tableau des dépendances nécessaires au script de transfert. C’est une liste de clés. Les chemins des fichiers à inclure dans le projet sont trouvés dans le dictionnaire [$config[« dependencies »]] ;
- $config[« rootDirectory »] représente le chemin avec lequel les fichiers à inclure doivent être préfixés ;
De la même façon, le script de calcul de l’impôt devient le suivant [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] :
1 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
// 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.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["dependencies4calculate"] as $dependency) {
require $rootDirectory . $config["dependencies"][$dependency];
}
// définition des constantes
define("DATABASE_CONFIG_FILENAME", "$rootDirectory/{$config["databaseFilename"]}");
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
try {
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// création de la couche [métier]
$métier = new Metier($dao);
// calcul de l'impôts en mode batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fin
print "Terminé\n";
exit;
|
Tests [Codeception]¶
Cette version comme les précédentes est validée par des tests [Codeception].
Test de la couche [dao]¶
Le test [DaoTest.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de fonctions
declare (strict_types=1);
// espace de noms
namespace Application;
// définition de constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-07");
// chemin du fichier de configuration
define("CONFIG_FILENAME", ROOT."/Data/config.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["dependencies4calculate"] as $dependency) {
require $rootDirectory . $config["dependencies"][$dependency];
}
// autres constantes
define("DATABASE_CONFIG_FILENAME", "$rootDirectory/{$config["databaseFilename"]}");
// test -----------------------------------------------------
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
|
Commentaires
- lignes 9-21 : définition de l’environnement du test. Nous utilisons le même, sans la couche [métier], que celui utilisé par le script principal [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] décrit au paragraphe lien ;
- lignes 29-34 : construction de la couche [dao] ;
- ligne 33 : l’attribut [$this→taxAdminData] contient les données à tester ;
- lignes 37-39 : la méthode [testTaxAdminData] est celle décrite au paragraphe lien ;
Les résultats du test sont les suivants :
Test de la couche [métier]¶
Le test [MetierTest.php] est le suivant :
1 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 de constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-07");
// chemin du fichier de configuration
define("CONFIG_FILENAME", ROOT . "/Data/config.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["dependencies4calculate"] as $dependency) {
require $rootDirectory . $config["dependencies"][$dependency];
}
// autres constantes
define("DATABASE_CONFIG_FILENAME", "$rootDirectory/{$config["databaseFilename"]}");
// classe de test
class MetierTest extends \Codeception\Test\Unit {
// couche métier
private $métier;
public function __construct() {
parent::__construct();
// création de la couche [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// création de la couche [métier]
$this->métier = new Metier($dao);
}
// tests
public function test1() {
…
}
-----------------------------------
public function test11() {
…
}
}
|
Commentaires
- lignes 9-21 : définition de l’environnement du test. Nous utilisons le même que celui utilisé par le script principal [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] décrit au paragraphe lien ;
- lignes 28-34 : construction de la couche [dao] ;
- ligne 33 : l’attribut [$this→métier] est une référence sur la couche [métier] à tester ;
- lignes 37-45 : les méthodes [test1, test2…, test11] sont celles décrites au paragraphe lien ;
Les résultats du test sont les suivants :
Fonctions réseau¶
Nous abordons maintenant les fonctions réseau de PHP qui nous permettent de faire de la programmation TCP / IP (Transfer Control Protocol / Internet Protocol).
Les bases de la programmation internet¶
Généralités¶
Considérons la communication entre deux machines distantes A et B :
Lorsque une application AppA d’une machine A veut communiquer avec une application AppB d’une machine B de l’Internet, elle doit connaître plusieurs choses :
- l’adresse IP (Internet Protocol) ou le nom de la machine B ;
- le numéro du port avec lequel travaille l’application AppB. En effet la machine B peut supporter de nombreuses applications qui travaillent sur l’Internet. Lorsqu’elle reçoit des informations provenant du réseau, elle doit savoir à quelle application sont destinées ces informations. Les applications de la machine B ont accès au réseau via des guichets appelés également des ports de communication. Cette information est contenue dans le paquet reçu par la machine B afin qu’il soit délivré à la bonne application ;
- les protocoles de communication compris par la machine B. Dans notre étude, nous utiliserons uniquement les protocoles TCP-IP ;
- le protocole de dialogue accepté par l’application AppB. En effet, les machines A et B vont se « parler ». Ce qu’elles vont dire va être encapsulé dans les protocoles TCP-IP. Néanmoins, lorsqu’au bout de la chaîne, l’application AppB va recevoir l’information envoyée par l’applicaton AppA, il faut qu’elle soit capable de l’interpréter. Ceci est analogue à la situation où deux personnes A et B communiquent par téléphone : leur dialogue est transporté par le téléphone. La parole va être codée sous forme de signaux par le téléphone A, transportée par des lignes téléphoniques, arriver au téléphone B pour y être décodée. La personne B entend alors des paroles. C’est là qu’intervient la notion de protocole de dialogue : si A parle français et que B ne comprend pas cette langue, A et B ne pourront dialoguer utilement ;
Aussi les deux applications communicantes doivent-elles être d’accord sur le type de dialogue qu’elles vont adopter. Par exemple, le dialogue avec un service ftp n’est pas le même qu’avec un service pop : ces deux services n’acceptent pas les mêmes commandes. Elles ont un protocole de dialogue différent ;
Les caractéristiques du protocole TCP¶
Nous n’étudierons ici que des communications réseau utilisant le protocole de transport TCP dont voici les principales caractéristiques :
le processus qui souhaite émettre établit tout d’abord une connexion avec le processus destinataire des informations qu’il va émettre. Cette connexion se fait entre un port de la machine émettrice et un port de la machine réceptrice. Il y a entre les deux ports un chemin virtuel qui est ainsi créé et qui sera réservé aux deux seuls processus ayant réalisé la connexion ;
tous les paquets émis par le processus source suivent ce chemin virtuel et arrivent dans l’ordre où ils ont été émis ;
l’information émise a un aspect continu. Le processus émetteur envoie des informations à son rythme. Celles-ci ne sont pas nécessairement envoyées tout de suite : le protocole TCP attend d’en avoir assez pour les envoyer. Elles sont stockées dans une structure appelée segment TCP. Ce segment une fois rempli sera transmis à la couche IP où il sera encapsulé dans un paquet IP ;
chaque segment envoyé par le protocole TCP est numéroté. Le protocole TCP destinataire vérifie qu’il reçoit bien les segments en séquence. Pour chaque segment correctement reçu, il envoie un accusé de réception à l’expéditeur ;
lorsque ce dernier le reçoit, il l’indique au processus émetteur. Celui-ci peut donc savoir qu’un segment est arrivé à bon port ;
si au bout d’un certain temps, le protocole TCP ayant émis un segment ne reçoit pas d’accusé de réception, il retransmet le segment en question, garantissant ainsi la qualité du service d’acheminement de l’information ;
le circuit virtuel établi entre les deux processus qui communiquent est full-duplex : cela signifie que l’information peut transiter dans les deux sens. Ainsi le processus destination peut envoyer des accusés de réception alors même que le processus source continue d’envoyer des informations. Cela permet par exemple au protocole TCP source d’envoyer plusieurs segments sans attendre d’accusé de réception. S’il réalise au bout d’un certain temps qu’il n’a pas reçu l’accusé de réception d’un certain segment n° n, il reprendra l’émission des segments à ce point ;
La relation client-serveur
Souvent, la communication sur Internet est dissymétrique : la machine A initie une connexion pour demander un service à la machine B : il précise qu’il veut ouvrir une connexion avec le service SB1 de la machine B. Celle-ci accepte ou refuse. Si elle accepte, la machine A peut envoyer ses demandes au service SB1. Celles-ci doivent se conformer au protocole de dialogue compris par le service SB1. Un dialogue demande-réponse s’instaure ainsi entre la machine A qu’on appelle machine cliente et la machine B qu’on appelle machine serveur. L’un des deux partenaires fermera la connexion.
Architecture d’un client¶
L’architecture d’un programme réseau demandant les services d’une application serveur sera la suivante :
1 2 3 4 5 6 7 8 9 10 | ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
fermer la connexion
|
Architecture d’un serveur¶
L’architecture d’un programme offrant des services sera la suivante :
1 2 3 4 5 | ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
|
Le programme serveur traite différemment la demande de connexion initiale d’un client de ses demandes ultérieures visant à obtenir un service. Le programme n’assure pas le service lui-même. S’il le faisait, pendant la durée du service il ne serait plus à l’écoute des demandes de connexion et des clients ne seraient alors pas servis. Il procède donc autrement : dès qu’une demande de connexion est reçue sur le port d’écoute puis acceptée, le serveur crée une tâche chargée de rendre le service demandé par le client. Ce service est rendu sur un autre port de la machine serveur appelé port de service. On peut ainsi servir plusieurs clients en même temps.
Une tâche de service aura la structure suivante :
1 2 3 4 5 6 | tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
|
Découvrir les protocoles de communication de l’internet¶
Introduction¶
Lorsqu’un client s’est connecté à un serveur, s’établit ensuite un dialogue entre-eux. La nature de celui-ci forme ce qu’on appelle le protocole de communication du serveur. Parmi les protocoles les plus courants de l’internet on trouve les suivants :
- HTTP : HyperText Transfer Protocol - le protocole de dialogue avec un serveur web (serveur HTTP) ;
- SMTP : Simple Mail Transfer Protocol - le protocole de dialogue avec un serveur d’envoi de courriers électroniques (serveur SMTP) ;
- POP : Post Office Protocol - le protocole de dialogue avec un serveur de stockage du courrier électronique (serveur POP). Il s’agit là de récupérer les courriers électroniques reçus et non d’en envoyer ;
- IMAP : Internet Message Access Protocol - le protocole de dialogue avec un serveur de stockage du courrier électronique (serveur IMAP). Ce protocole a remplacé progressivement le protocole POP plus ancien ;
- FTP : File Transfer Protocol - le protocole de dialogue avec un serveur de stockage de fichiers (serveur FTP) ;
Tous ces protocoles ont la particularité d’être des protocoles à lignes de texte : le client et le serveur s’échangent des lignes de texte. Si on a un client capable de :
- créer une connexion avec un serveur TCP ;
- afficher à la console les lignes de texte que le serveur lui envoie ;
- envoyer au serveur les lignes de texte qu’un utilisateur saisirait au clavier ;
alors on est capable de dialoguer avec un serveur TCP ayant un protocole à lignes de texte pour peu qu’on connaisse les règles de ce protocole.
Utilitaires TCP¶
Dans les codes associés à ce document, on trouve deux utilitaires de communication TCP :
- [RawTcpClient] permet de se connecter sur le port P d’un serveur S ;
- [RawTcpServer] permet de créer un serveur qui attend des clients sur un port P ;
Le serveur TCP [RawTcpServer] s’appelle avec la syntaxe [RawTcpServeur port] pour créer un service TCP sur le port [port] de la machine locale (l’ordinateur sur lequel vous travaillez) :
- le serveur peut servir plusieurs clients simultanément ;
- le serveur exécute les commandes tapées par l’utilisateur tapées au
clavier. Celles-ci sont les suivantes :
- list : liste les clients actuellement connectés au serveur. Ceux-ci sont affichés sous la forme [id=x-nom=y]. Le champ [id] sert à identifier les clients ;
- send x [texte] : envoie texte au client n° x (id=x). Les crochets [] ne sont pas envoyés. Ils sont nécessaires dans la commande. Ils servent à délimiter visuellement le texte envoyé au client ;
- close x : ferme la connexion avec le client n° x ;
- quit : ferme toutes les connexions et arrête le service ;
- les lignes envoyées par le client au serveur sont affichées sur la console ;
- l’ensemble des échanges est logué dans un fichier texte portant le
nom [machine-portService.txt] où
- [machine] est le nom de la machine sur laquelle s’exécute le code ;
- [port] est le port de service qui répond aux demandes du client ;
Le client TCP [RawTcpClient] s’appelle avec la syntaxe [RawTcpClient serveur port] pour se connecter au port [port] du serveur [serveur] :
- les lignes tapées par l’utilisateur au clavier sont envoyées au serveur ;
- les lignes envoyées par le serveur sont affichées sur la console ;
- l’ensemble des échanges est logué dans un fichier texte portant le nom [serveur-port.txt] ;
Voyons un exemple. On ouvre deux fenêtres de commandes Windows et on se positionne dans chacune d’elles sur le dossier des utilitaires. Dans l’une des fenêtres on lance le serveur [RawTcpServer] sur le port 100 :
- en [1], nous sommes placés dans le dossier des utilitaires ;
- en [2], nous lançons le serveur TCP sur le port 100 ;
- en [3], le serveur se met en attente d’un client TCP ;
- en [4], le serveur attend une commande tapée par l’utilisateur au clavier ;
Dans l’autre fenêtre de commandes, on lance le client TCP :
- en [5], nous sommes placés dans le dossier des utilitaires ;
- en [6], nous lançons le client TCP : nous lui disons de se connecter au port 100 de la machine locale (celle avec laquelle vous travaillez) ;
- en [7], le client a réussi à se connecter au serveur. On indique les coordonnées du client : il est sur la machine [DESKTOP-528I5CU] (la machine locale dans cet exemple) et utilise le port [50405] pour communiquer avec le serveur :
- en [8], le client attend une commande tapée par l’utilisateur au clavier ;
Revenons sur la fenêtre du serveur. Son contenu a évolué :
- en [9], un client a été détecté. Le serveur lui a donné le n° 1. Le serveur a correctement identifié le client distant (machine et port) ;
- en [10], le serveur se remet en attente d’un nouveau client ;
Revenons sur la fenêtre du client et envoyons une commande au serveur :
- en [11], la commande envoyée au serveur ;
Revenons sur la fenêtre du serveur. Son contenu a évolué :
- en [12], entre cochets, le message reçu par le serveur ;
Envoyons une réponse au client :
- en [13], la réponse envoyée au client 1. Seul le texte entre les crochets est envoyé, pas les crochets eux-mêmes ;
Revenons à la fenêtre du client :
- en [14], la réponse reçue par le client. Le texte reçu est celui entre crochets ;
Revenons à la fenêtre du serveur pour voir d’autres commandes :
- en [15], nous demandons la liste des clients ;
- en [16], la réponse ;
- en [17], nous fermons la connexion avec le client n° 1 ;
- en [18], la confirmation du serveur ;
- en [19], nous arrêtons le serveur ;
- en [20], la confirmation du serveur ;
Revenons à la fenêtre du client :
- en [21], le client a détecté la fin du service ;
Deux fichiers de logs ont été créés, un pour le serveur, un autre pour le client :
- en [25], les logs du serveur : le nom du fichier est le nom du client [machine-port] ;
- en [26], les logs du client : le nom du fichier est le nom du serveur [machine-port] ;
Les logs du serveur sont les suivants :
1 2 | <-- [hello from client]
--> [hello from server]
|
Les logs du client sont les suivants :
1 2 | --> [hello from client]
<-- [hello from server]
|
Obtenir le nom ou l’adresse IP d’une machine de l’Internet¶
Les machines de l’internet sont identifiées par une adresse IP (IPv4 ou IPv6) et le plus souvent par un nom. Mais finalement seule l’adresse IP est utilisée. Il faut donc parfois connaître l’adresse IP d’une machine identifiée par son nom.
Le script [ip-01.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de fonctions
declare (strict_types=1);
//
// gestion des erreurs
error_reporting(E_ALL & E_STRICT);
ini_set("display_errors", "on");
//
// constantes
$HOTES = array("istia.univ-angers.fr", "www.univ-angers.fr", "www.ibm.com", "localhost", "", "xx");
// adresses IP et noms des machines de $HOTES
for ($i = 0; $i < count($HOTES); $i++) {
getIPandName($HOTES[$i]);
}
// fin
print "Terminé\n";
exit;
//------------------------------------------------
function getIPandName(string $nomMachine): void {
//$nomMachine : nom de la machine dont on veut l'adresse IP
//
// nomMachine-->adresse IP
$ip = gethostbyname($nomMachine);
print "---------------\n";
if ($ip !== $nomMachine) {
print "ip[$nomMachine]=$ip\n";
// adresse IP --> nomMachine
$name = gethostbyaddr($ip);
if ($name !== $ip) {
print "name[$ip]=$name\n";
} else {
print "Erreur, machine[$ip] non trouvée\n";
}
} else {
print "Erreur, machine[$nomMachine] non trouvée\n";
}
}
|
Commentaires
- lignes 7-8 : on demande à ce que PHP signale toutes les erreurs (E_ALL & E_STRICT) et que celles-ci soient affichées. Ce mode n’est recommandé qu’en mode développement pour améliorer le code avec les avertissements de PHP. En mode production, ligne 8, on mettrait « off ». Depuis PHP 5.4, le niveau E_STRICT est inclus dans E_ALL ;
- ligne 11 : la liste de machines dont on veut le nom et l’adresse IP ;
Les fonctions réseau de PHP sont utilisées dans la fonction getIpandName de la ligne 21.
- ligne 25 : la fonction gethostbyname($nom) permet d’obtenir l’adresse IP « ip3.ip2.ip1.ip0 » de la machine s’appelant $nom. Si la machine $nom n’existe pas, la fonction rend $nom comme résultat ;
- ligne 30 : la fonction gethostbyaddr($ip) permet d’obtenir le nom de la machine d’adresse $ip de la forme « ip3.ip2.ip1.ip0 ». Si la machine $ip n’existe pas, la fonction rend $ip comme résultat ;
Résultats :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ---------------
ip[istia.univ-angers.fr]=193.49.144.41
name[193.49.144.41]=ametys-fo-2.univ-angers.fr
---------------
ip[www.univ-angers.fr]=193.49.144.41
name[193.49.144.41]=ametys-fo-2.univ-angers.fr
---------------
ip[www.ibm.com]=2.18.220.211
name[2.18.220.211]=a2-18-220-211.deploy.static.akamaitechnologies.com
---------------
ip[localhost]=127.0.0.1
name[127.0.0.1]=DESKTOP-528I5CU
---------------
ip[]=192.168.1.38
name[192.168.1.38]=DESKTOP-528I5CU.home
---------------
Erreur, machine[xx] non trouvée
Terminé
|
Le protocole HTTP (HyperText Transfer Protocol)¶
Exemple 1¶
Lorsqu’un navigateur affiche une URL, il est le client d’un serveur web ou dit autrement d’un serveur HTTP. C’est lui qui prend l’initiative et il commence par envoyer un certain nombre de commandes au serveur. Pour ce premier exemple :
- le serveur sera l’utilitaire [RawTcpServer] ;
- le client sera un navigateur ;
Nous lançons d’abord le serveur sur le port 100 :
Puis avec un navigateur, nous demandons l’URL [localhost:100], ç-a-d que nous disons que le serveur HTTP interrogé travaille sur le port 100 de la machine locale :
Revenons sur la fenêtre du serveur :
- en [3], le client qui s’est connecté ;
- en [4-7], la série de lignes de texte qu’il a envoyées :
- en [4] : cette ligne a le format [GET URL HTTP/1.1]. Elle demande l’URL / et demande au serveur d’utiliser le protocole HTTP 1.1 ;
- en [5] : cette ligne a le format [Host: serveur:port]. La casse de la commande [Host] n’importe pas. On rappelle ici que le client interroge un serveur local opérant sur le port 100 ;
- la commande [User-Agent] donne l’identité du client ;
- la commande [Accept] indique quels types de document sont acceptés par le client ;
- la commande [Accept-Language] indique dans quelle langue sont souhaités les documents demandés s’ils existent en plusieurs langues ;
- la commande [Connection] indique le mode de connexion souhaité : [keep-alive] indique que la connexion doit être maintenue jusqu’à ce que les échanges soient terminés ;
- en [7] : le client termine ses commandes par une ligne vide ;
Nous terminons la connexion en terminant le serveur :
Exemple 2¶
Maintenant que nous connaissons les commandes envoyées par un navigateur pour réclamer une URL, nous allons réclamer cette URL avec notre client TCP [RawTcpClient]. Le serveur Apache de Laragon sera notre serveur web.
Lançons Laragon puis le serveur web Apache :
Maintenant avec un navigateur, demandons l’URL [http://localhost:80]. Ici nous ne précisons que le serveur [localhost:80] et pas d’URL de document. Dans ce cas c’est l’URL / qui est demandée, ç-à-d la racine du serveur web :
- en [1], l’URL demandée. On a tapé initialement [http://localhost:80] et le navigateur (Firefox ici) l’a transformée simplement en [localhost] car le protocole [http] est implicite lorsqu’aucun protocole n’est mentionné et le port [80] est implicite lorsque le port n’est pas précisé ;
- en [2], la page racine / du serveur web interrogé ;
Maintenant, visualisons le texte reçu par le navigateur :
- on clique droit sur la page reçue et on choisit l’option [2]. on obtient le code source suivant :
1 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 | <!DOCTYPE HTML>
<HTML>
<head>
<title>Laragon</title>
<link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">
<style>
HTML, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
display: table;
font-weight: 100;
font-family: 'Karla';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 96px;
}
.opt {
margin-top: 30px;
}
.opt a {
text-decoration: none;
font-size: 150%;
}
a:hover {
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title" title="Laragon">Laragon</div>
<div class="info"><br />
Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11<br />
PHP version: 7.2.11 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
Document Root: C:/myprograms/laragon-lite/www<br />
</div>
<div class="opt">
<div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
</div>
</div>
</div>
</body>
</HTML>
|
Maintenant demandons l’URL [http://localhost:80] avec notre client TCP :
- en [1], nous nous connectons au port 80 du serveur localhost. C’est là qu’opère le serveur web de Laragon ;
Nous tapons maintenant les commandes que nous avons découvertes dans le paragraphe précédent :
- en [1], la commande [GET]. On demande la racine / du serveur web ;
- en [2], la commande [Host] ;
- ce sont les deux seules commandes indispensables. Pour les autres commandes, le serveur web prendra des valeurs par défaut ;
- en [3], la ligne vide qui doit terminer les commandes du client ;
- dessous la ligne 3, vient la réponse du serveur web ;
- en [4] jusqu’à la ligne vide [5] viennent les entêtes HTTP de la réponse du serveur ;
- après la ligne [5] vient le document HTML demandé [6] ;
Nous tapons [quit] pour terminer le client et nous chargeons le fichier de logs [localhost-80.txt] :
1 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 | --> [GET / HTTP/1.1]
--> [Host: localhost:80]
--> []
<-- [HTTP/1.1 200 OK]
<-- [Date: Thu, 16 May 2019 14:24:39 GMT]
<-- [Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11]
<-- [X-Powered-By: PHP/7.2.11]
<-- [Content-Length: 1781]
<-- [Content-Type: text/HTML; charset=UTF-8]
<-- []
<-- [<!DOCTYPE HTML>]
<-- [<HTML>]
<-- [ <head>]
<-- [ <title>Laragon</title>]
<-- []
<-- [ <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">]
<-- []
<-- [ <style>]
<-- [ HTML, body {]
<-- [ height: 100%;]
<-- [ }]
<-- []
<-- [ body {]
<-- [ margin: 0;]
<-- [ padding: 0;]
<-- [ width: 100%;]
<-- [ display: table;]
<-- [ font-weight: 100;]
<-- [ font-family: 'Karla';]
<-- [ }]
<-- []
<-- [ .container {]
<-- [ text-align: center;]
<-- [ display: table-cell;]
<-- [ vertical-align: middle;]
<-- [ }]
<-- []
<-- [ .content {]
<-- [ text-align: center;]
<-- [ display: inline-block;]
<-- [ }]
<-- []
<-- [ .title {]
<-- [ font-size: 96px;]
<-- [ }]
<-- []
<-- [ .opt {]
<-- [ margin-top: 30px;]
<-- [ }]
<-- []
<-- [ .opt a {]
<-- [ text-decoration: none;]
<-- [ font-size: 150%;]
<-- [ }]
<-- [ ]
<-- [ a:hover {]
<-- [ color: red;]
<-- [ }]
<-- [ </style>]
<-- [ </head>]
<-- [ <body>]
<-- [ <div class="container">]
<-- [ <div class="content">]
<-- [ <div class="title" title="Laragon">Laragon</div>]
<-- [ ]
<-- [ <div class="info"><br />]
<-- [ Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11<br />]
<-- [ PHP version: 7.2.11 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />]
<-- [ Document Root: C:/myprograms/laragon-lite/www<br />]
<-- []
<-- [ </div>]
<-- [ <div class="opt">]
<-- [ <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>]
<-- [ </div>]
<-- [ </div>]
<-- []
<-- [ </div>]
<-- [ </body>]
<-- [</HTML>]
|
- lignes 11-79 : le document HTML reçu. Dans l’exemple précédent, Firefox avait reçu le même ;
Nous avons désormais les bases pour programmer un client TCP qui demanderait une URL.
Exemple 3¶
Le script [http-01.php] est un client HTTP configuré par le fichier jSON [config-http-01.json]. Le contenu de celui-ci est le suivant :
1 2 3 4 5 6 7 8 9 10 11 | {
"localhost": {
"port": 80,
"GET": "/",
"Host": "localhost:80",
"User-Agent": "client PHP",
"Accept": "text/HTML",
"Accept-Language": "fr",
"endOfLine":"\r\n"
}
}
|
- ligne 2 : le nom de la machine hébergeant le serveur web à atteindre ;
- ligne 3 : le port sur lequel opère ce serveur web ;
- ligne 4 : l’URL du document désiré ;
- ligne 5 : la machine cible sous la forme machine:port ;
- ligne 6 : l’identification du client HTTP : on peut mettre ce qu’on veut ;
- ligne 7 : le type de document accepté par le client, ici du texte HTML ;
- ligne 8 : la langue souhaitée pour le document demandé ;
- ligne 9 : la marque de fin de ligne pour les commandes envoyées par le client : en effet elle peut différer selon que le serveur est sur une machine Unix (n) ou Windows (rn) ;
Le script [http-01.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
//
// gestion des erreurs
// error_reporting(E_ALL & E_STRICT);
// ini_set("display_errors", "on");
//
// constantes
const CONFIG_FILE_NAME = "config-http-01.json";
//
// on récupère la configuration
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// otenir le texte HTML des URL du fichier de configuration
foreach ($config as $site => $protocole) {
// lecture page index du site $ite
$résultat = getURL($site, $protocole);
// affichage résultat
print "$résultat\n";
}//for
// fin
exit;
//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
// lit l'URL $site["GET"] et la stocke dans le fichier $site.HTML
// le dialogue client /serveur se fait selon le protocole $protocole
//
// ouverture d'une connexion sur le port de $site
$erreurNumber = 0;
$erreur = "";
$connexion = fsockopen($site, $protocole["port"], $erreurNumber, $erreur);
// retour si erreur
if ($connexion === FALSE) {
return "Echec de la connexion au site (" . $site . " ," . $protocole["port"] . " : $erreur";
}
// $connexion représente un flux de communication bidirectionnel
// entre le client (ce programme) et le serveur web contacté
// ce canal est utilisé pour les échanges de commandes et d'informations
// le protocole de dialogue est HTTP
//
// création du fichier $site.HTML
$HTML = fopen("output/$site.HTML", "w");
if ($HTML === FALSE) {
// fermeture connexion client / serveur
fclose($connexion);
// retour erreur
return "Erreur lors de la création du fichier $site.HTML";
}
// le client va commencer le dialogue HTTP avec le serveur
if ($suivi) {
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
}
// selon les serveurs, les lignes du client doivent se terminer par \n ou \r\n
$endOfLine = $protocole["endOfLine"];
// par simplification, on ne teste pas les cas d'erreur dans la communication client /serveur
// le client envoie la commande GET pour demander l'URL $protocole["GET"]
// syntaxe GET URL HTTP/1.1
$commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
// suivi ?
if ($suivi) {
print "--> $commande";
}
// on envoie la commande au serveur
fputs($connexion, $commande);
// émission des autres entêtes HTTP
foreach ($protocole as $verb => $value) {
if ($verb !== "GET" && $verb != "port"" && $verb !="endOfLine") {
// on construit la commande
$commande = "$verb: $value$endOfLine";
// suivi ?
if ($suivi) {
print "--> $commande";
}
// on envoie la commande au serveur
fputs($connexion, $commande);
}
}
// les entêtes (headers) du protocole HTTP doivent se terminer par une ligne vide
fputs($connexion, $endOfLine);
//
// le serveur va maintenant répondre sur le canal $connexion. Il va envoyer toutes
// ses données puis fermer le canal. Le client lit donc tout ce qui arrive de $connexion
// jusqu'à la fermeture du canal
//
// on lit tout d'abord les entêtes HTTP envoyés par le serveur
// ils se terminent eux-aussi par une ligne vide
if ($suivi) {
print "Réponse du serveur [$site] ----------------------------\n";
}
$fini = FALSE;
while (!$fini && $ligne = fgets($connexion, 1000)) {
// a-t-on une ligne vide ?
$champs = [];
preg_match("/^(.*?)\s+$/", $ligne, $champs);
if ($champs[1] !== "") {
if ($suivi) {
// on affiche l'entête HTTP
print "<-- " . $champs[1] . "\n";
}
} else {
// c'était la ligne vide - les entêtes HTTP sont terminés
$fini = TRUE;
}
}
// on lit le document HTML qui va suivre la ligne vide
while ($ligne = fgets($connexion, 1000)) {
// on mémorise la ligne dans le fichier HTML du site
fputs($HTML, $ligne);
}
// le serveur a fermé la connexion - le client la ferme à son tour
fclose($connexion);
// fermeture du fichier $HTML
fclose($HTML);
// retour
return "Fin de la communication avec le site [$site]. Vérifiez le fichier [$site.HTML]";
}
|
Commentaires du code :
- ligne 14 : le fichier de configuration est exploité pour créer un
dictionnaire :
- les clés du dictionnaire sont les serveurs web à interroger ;
- les valeurs fixent le protocole HTTP à respecter ;
- lignes 16-21 : on boucle sur la liste des serveurs web de la configuration ;
- ligne 26 : la fonction getURL($site,$protocole,$suivi) demande un document du site web $site et le stocke dans le fichier texte $site.HTML.Par défaut, les échanges client/serveur sont logués sur la console ($suivi=TRUE) ;
- ligne 33 : la fonction fsockopen($site,$port,$errNumber,$erreur) permet de créer une connexion avec un service TCP / IP travaillant sur le port $port de la machine $site. Si la connexion échoue, [$errNumber] est un n° d’erreur et [$erreur] le message d’erreur associé. Une fois la connexion client / serveur ouverte, de nombreux services TCP / IP échangent des lignes de texte. C’est le cas ici du protocole HTTP (HyperText Transfer Protocol). Le flux du serveur parvenant au client peut alors être traité comme un fichier texte lu avec [fgets]. Il en est de même pour le flux partant du client vers le serveur qui peut être écrit avec [fputs] ;
- lignes 44-50 : création du fichier [$site.HTML] dans lequel on stockera le document HTML reçu ;
- ligne 60 : la première commande du client doit être la commande [GET URL HTTP/1.1] ;
- ligne 66 : la fonction fputs permet au client d’envoyer des données au serveur. Ici la ligne de texte envoyée a la signification suivante : « Je veux (GET) la page [URL] du site web auquel je suis connecté. Je travaille avec le protocole HTTP version 1.1 » ;
- lignes 68-79 : on envoie les autres lignes du protocole HTTP [Host, User-Agent, Accept, Accept-Language]. Leur ordre n’importe pas ;
- ligne 81 : on envoie une ligne vide au serveur pour signifier que le client a terminé d’envoyer ses entêtes HTTP et qu’il attend désormais le document demandé ;
- lignes 92-106 : le serveur va tout d’abord envoyer une série d’entêtes HTTP qui vont donner diverses informations sur le document demandé. Ces entêtes se terminent par une ligne vide ;
- ligne 93 : on lit une ligne envoyée par le serveur avec la fonction PHP [fgets] ;
- ligne 96 : on récupère le corps de la ligne sans les espaces (blancs, marque de fin de ligne) de la fin de ligne ;
- ligne 97 : on regarde si on a récupéré la ligne vide qui marque la fin des entêtes HTTP envoyés par le serveur ;
- lignes 98-101 : si on est en mode [suivi], l’entête HTTP reçu est affiché à la console ;
- lignes 108-111 : les lignes de texte de la réponse du serveur peuvent être lues ligne par ligne avec une boucle while et enregistrées dans le fichier texte [output/$site.HTML]. Lorsque le serveur web a envoyé la totalité de la page qu’on lui a demandée, il ferme sa connexion avec le client. Côté client, cela sera détecté comme une fin de fichier ;
Résultats :
La console affiche les logs suivants :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Client : début de la communication avec le serveur [localhost] ----------------------------
--> GET / HTTP/1.1
--> Host: localhost:80
--> User-Agent: client PHP
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [localhost] ----------------------------
<-- HTTP/1.1 200 OK
<-- Date: Thu, 16 May 2019 15:43:18 GMT
<-- Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
<-- X-Powered-By: PHP/7.2.11
<-- Content-Length: 1781
<-- Content-Type: text/HTML; charset=UTF-8
Fin de la communication avec le site [localhost]. Vérifiez le fichier [localhost.HTML]
|
Dans notre exemple, le fichier [output/localhost.HTML] reçu est le suivant :
1 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 | <!DOCTYPE HTML>
<HTML>
<head>
<title>Laragon</title>
<link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">
<style>
HTML, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
display: table;
font-weight: 100;
font-family: 'Karla';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 96px;
}
.opt {
margin-top: 30px;
}
.opt a {
text-decoration: none;
font-size: 150%;
}
a:hover {
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title" title="Laragon">Laragon</div>
<div class="info"><br />
Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11<br />
PHP version: 7.2.11 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
Document Root: C:/myprograms/laragon-lite/www<br />
</div>
<div class="opt">
<div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
</div>
</div>
</div>
</body>
</HTML>
|
Nous avons bien obtenu le même document qu’avec le navigateur Firefox.
Exemple 4¶
Dans cet exemple, nous allons montrer que le client HTTP que nous avons écrit est insuffisant. Faison évoluer le fichier de configuration [config-http-01.json] de la façon suivante :
1 2 3 4 5 6 7 8 9 10 11 | {
"tahe.developpez.com": {
"port": 443,
"GET": "/",
"Host": "sergetahe.com:443",
"User-Agent": "script PHP 7",
"Accept": "text/HTML",
"Accept-Language": "fr",
"endOfLine":"\n"
}
}
|
Ici, nous allons demander l’URL [http://tahe.developpez.com:443/]. Le port 443 de la machine [tahe.developpez.com] est un port utilisé pour le protocole http sécurisé appelé https. Dans ce protocole, le dialogue client / serveur commence par un échange d’informations qui vont sécuriser la liaison. Le client doit alors parler le protocole [HTTPS] et non le protocole [HTTP], ce que ne fait pas notre client.
Avec ce fichier de configuration, les résultats de la console sont les suivants :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Client : début de la communication avec le serveur [tahe.developpez.com] ----------------------------
--> GET / HTTP/1.1
--> Host: sergetahe.com:443
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [tahe.developpez.com] ----------------------------
<-- HTTP/1.1 400 Bad Request
<-- Date: Fri, 17 May 2019 13:02:26 GMT
<-- Server: Apache/2.4.25 (Debian)
<-- Content-Length: 454
<-- Connection: close
<-- Content-Type: text/HTML; charset=iso-8859-1
Fin de la communication avec le site [tahe.developpez.com]. Vérifiez le fichier [output/tahe.developpez.com.HTML]
|
- ligne 8 : le serveur [tahe.developpez.com] a répondu que la requête du client était incorrecte ;
Le contenu du fichier [output/tahe.developpez.com.HTML] est alors le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
Reason: You're speaking plain HTTP to an SSL-enabled server port.<br />
Instead use the HTTPS scheme to access this URL, please.<br />
</p>
<hr>
<address>Apache/2.4.25 (Debian) Server at 2eurocents.developpez.com Port 443</address>
</body></HTML>
|
Le serveur dit clairement que nous n’avons pas utilisé le bon protocole.
Utilisons maintenant, le fichier de configuration suivant :
1 2 3 4 5 6 7 8 9 10 11 | {
"sergetahe.com": {
"port": 80,
"GET": "/cours-tutoriels-de-programmation/",
"Host": "sergetahe.com:80",
"User-Agent": "script PHP 7",
"Accept": "text/HTML",
"Accept-Language": "fr",
"endOfLine": "\n"
}
}
|
Les résultats console sont alors les suivants :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
--> GET /cours-tutoriels-de-programmation/ HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [sergetahe.com] ----------------------------
<-- HTTP/1.1 200 OK
<-- Date: Fri, 17 May 2019 13:36:06 GMT
<-- Content-Type: text/HTML; charset=UTF-8
<-- Transfer-Encoding: chunked
<-- Server: Apache
<-- X-Powered-By: PHP/7.0
<-- Vary: Accept-Encoding
<-- Set-Cookie: SERVERID68971=2621207|XN64y|XN64y; path=/
<-- Cache-control: private
<-- X-IPLB-Instance: 17106
Fin de la communication avec le site [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
|
- la ligne 11 indique que le serveur envoie le document par morceaux ;
Cela se traduit par la présence de nombres dans le flux envoyé au client : chaque nombre indique au client le nombre de caractères du prochain morceau envoyé par le serveur. Voici ce que ça donne dans le fichier [output/sergetahe.com.HTML] :
- en [1] et [2], la taille en hexadécimal des morceaux 1 et 2 du document ;
Un client HTTP correct ne devrait pas laisser ces nombres dans le document HTML final.
Voici un autre exemple :
1 2 3 4 5 6 7 8 9 10 11 | {
"sergetahe.com": {
"port": 80,
"GET": "/cours-tutoriels-de-programmation",
"Host": "sergetahe.com:80",
"User-Agent": "script PHP 7",
"Accept": "text/HTML",
"Accept-Language": "fr",
"endOfLine": "\n"
}
}
|
Il ressemble à l’exemple précédent mais l’URL demandée en ligne 4 n’a pas le caractère / pour la terminer. Ce ne sont pas les mêmes URL. L’exécution du client HTTP donne alors les résultats console suivants :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
--> GET /cours-tutoriels-de-programmation HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [sergetahe.com] ----------------------------
<-- HTTP/1.1 301 Moved Permanently
<-- Date: Fri, 17 May 2019 13:47:00 GMT
<-- Content-Type: text/HTML; charset=iso-8859-1
<-- Content-Length: 262
<-- Server: Apache
<-- Location: http://sergetahe.com:80/cours-tutoriels-de-programmation/
<-- Set-Cookie: SERVERID68971=2621207|XN67V|XN67V; path=/
<-- Cache-control: private
<-- X-IPLB-Instance: 17095
Fin de la communication avec le site [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
|
- la ligne 8 indique que le document demandé a changé d’URL. La nouvelle URL est donnée ligne 13. Notez cette fois-ci le caractère / qui termine la nouvelle URL ;
Le fichier [output/serge.tahe.com.HTML] est alors le suivant :
1 2 3 4 5 6 7 | <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="http://sergetahe.com/cours-tutoriels-de-programmation/">here</a>.</p>
</body></HTML>
|
Un client HTTP devrait pouvoir suivre les redirections. Ici il devrait redemander automatiquement la nouvelle URL [http://sergetahe.com/cours-tutoriels-de-programmation/].
Exemple 5¶
Les exemples précédents nous ont montré que notre client HTTP était insuffisant. Nous allons maintenant présenter un outil appelé [curl] qui permet de récupérer des documents web en gérant les difficultés mentionnées : protocole https, document envoyé par morceaux, redirections… L’outil [curl] a été installé avec Laragon :
Ouvrons un terminal Laragon [1] :
Dans le terminal nous tapons la commande suivante :
- en [1], le type de la console ;
- en [2], le dossier courant. Ce dossier est particulier : c’est là où le serveur Apache de Laragon vient chercher les documents qu’on lui demande. On évitera donc de polluer ce dossier ;
- en [3], la commande tapée ;
Il est possible que la commande [curl –help] produise une erreur. La cause la plus probable est que vous n’avez pas le bon type de terminal. Dans ce cas, ouvrez un autre terminal avec les commandes [4-6] ;
La commande [curl –help] fait afficher toutes les options de configuration de [curl]. Il y en a plusieurs dizaines. Nous en utiliserons très peu. Pour demander une URL il suffit de taper la commande [curl URL]. Cette commande affichera sur la console le document demandé. Si on veut de plus les échanges HTTP entre le client et le serveur on écrira [curl –verbose URL]. Enfin pour enregistrer le document HTML demandé dans un fichier on écrira [curl –verbose –output fichier URL].
Pour éviter de polluer le dossier [www] de Laragon, déplaçons-nous à un autre endroit du système de fichiers :
- en [1], on se déplace dans le dossier [c:temp]. Si ce dossier n’existe pas, vous pouvez le créer ou en choisir un autre ;
- en [2], on crée un dossier appelé [curl] ;
- en [3], on se positionne dessus ;
- en [4], on liste son contenu. Il est vide ;
Assurez-vous que le serveur Apache de Laragon est lancé et avec [curl] demandez l’URL [http://localhost/] avec la commande [curl –verbose –output localhost.HTML http://localhost/]. On obtient les résultats suivants :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | c:\Temp\curl
λ curl --verbose --output localhost.HTML http://localhost/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying ::1…
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 14:32:47 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
< X-Powered-By: PHP/7.2.11
< Content-Length: 1781
< Content-Type: text/HTML; charset=UTF-8
<
{ [1781 bytes data]
100 1781 100 1781 0 0 14248 0 --:--:-- --:--:-- --:--:-- 14248
* Connection #0 to host localhost left intact
|
- lignes 8-12 : lignes envoyées par [curl] au serveur [localhost]. On reconnaît le protocole HTTP ;
- lignes 13-19 : lignes envoyées en réponse par le serveur ;
- ligne 13 : indique qu’on a bien eu le document demandé ;
Le fichier [localhost.HTML] contient le document demandé. Vous pouvez le vérifier en chargeant le fichier dans un éditeur de texte.
Maintenant demandons l’URL [https://tahe.developpez.com:443/]. Pour avoir cette URL, le client HTTP doit savoir parler HTTPS. C’est le cas du client [curl].
Les résultats console sont les suivants :
1 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 | c:\Temp\curl
λ curl --verbose --output tahe.developpez.com.HTML https://tahe.developpez.com:443/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 87.98.130.52…
* TCP_NODELAY set
* Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: C:\myprograms\laragon-lite\bin\laragon\utils\curl-ca-bundle.crt
CApath: none
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [108 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2558 bytes data]
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
{ [333 bytes data]
* TLSv1.2 (IN), TLS handshake, Server finished (14):
{ [4 bytes data]
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
} [70 bytes data]
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.2 (OUT), TLS handshake, Finished (20):
} [16 bytes data]
* TLSv1.2 (IN), TLS handshake, Finished (20):
{ [16 bytes data]
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=*.developpez.com
* start date: Apr 4 08:25:09 2019 GMT
* expire date: Jul 3 08:25:09 2019 GMT
* subjectAltName: host "tahe.developpez.com" matched cert's "*.developpez.com"
* issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
* SSL certificate verify ok.
} [5 bytes data]
> GET / HTTP/1.1
> Host: tahe.developpez.com
> User-Agent: curl/7.63.0
> Accept: */*
>
{ [5 bytes data]
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 14:39:41 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/HTML
<
{ [6 bytes data]
100 96559 0 96559 0 0 163k 0 --:--:-- --:--:-- --:--:-- 163k
* Connection #0 to host tahe.developpez.com left intact
|
- lignes 10-40 : les échanges client / serveur pour sécuriser la connexion : celle-ci sera chiffrée ;
- lignes 42-45 : les entêtes HTTP envoyés par le client [curl] au serveur ;
- ligne 48 : le document demandé a bien été trouvé ;
- ligne 53 : le document est envoyé par morceaux ;
[curl] gère correctement à la fois le protocole sécurisé HTTPS et le fait que le document soit envoyé par morceaux. Le document envoyé sera trouvé ici dans le fichier [tahe.developpez.com.HTML].
Demandons maintenant l’URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Nous avions vu que pour cette URL, il y avait une redirection vers l’URL [http://sergetahe.com/cours-tutoriels-de-programmation/] (avec un / à la fin).
Les résultats console sont alors les suivants :
1 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 | c:\Temp\curl
λ curl --verbose --output sergetahe.com.HTML --location http://sergetahe.com/cours-tutoriels-de-programmation
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 87.98.154.146…
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Date: Fri, 17 May 2019 15:13:03 GMT
< Content-Type: text/HTML; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=2621207|XN7Pg|XN7Pg; path=/
< Cache-control: private
< X-IPLB-Instance: 17095
<
* Ignoring the response-body
{ [262 bytes data]
100 262 100 262 0 0 1401 0 --:--:-- --:--:-- --:--:-- 1401
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
* Found bundle for host sergetahe.com: 0x1c88548 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 15:13:04 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Vary: Accept-Encoding
< Set-Cookie: SERVERID68971=2621207|XN7Pg|XN7Pg; path=/
< Cache-control: private
< X-IPLB-Instance: 17095
<
{ [14205 bytes data]
100 43101 0 43101 0 0 78795 0 --:--:-- --:--:-- --:--:-- 168k
* Connection #0 to host sergetahe.com left intact
|
- ligne 2 : on utilise l’option [–location] pour indiquer qu’on veut suivre les redirections envoyées par le serveur ;
- ligne 13 : le serveur indique que le document demandé a changé d’URL ;
- ligne 18 : il indique la nouvelle URL du document demandé ;
- ligne 27 : [curl] émet une nouvelle requête vers cette fois la nouvelle URL ;
- ligne 33 : la nouvelle URL est utilisée ;
- ligne 38 : le serveur répond qu’il a trouvé le document demandé ;
- ligne 41 : il l’envoie par morceaux ;
Le document demandé sera trouvé dans le fichier [sergetahe.com.HTML].
Exemple 6¶
PHP possède une extension appelée [libcurl] qui permet d’utiliser les capacités de l’outil [curl] dans un programme PHP. Il faut tout d’abord s’assurer que cette extension est activée dans le fichier [php.ini] décrit au paragraphe lien :
Assurez-vous que la ligne 889 ci-dessus est décommentée.
Nous allons écrire un script [http-02.php] qui exploitera le fichier de configuration jSON suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {
"sergetahe.com": {
"timeout": 5,
"url": "http://sergetahe.com"
},
"tahe.developpez.com": {
"timeout": 5,
"url": "https://tahe.developpez.com"
},
"www.polytech-angers.fr": {
"timeout": 5,
"url": "http://www.polytech-angers.fr"
},
"localhost": {
"timeout": 5,
"url": "http://localhost"
}
}
|
Chaque élément du dictionnaire [clé, valeur] a la structure suivante :
- clé : le nom d’un serveur web ;
- valeur est un dictionnaire avec les clés suivantes :
- timeout : durée maximale d’attente de la réponse du serveur. Au-delà, le client se déconnectera ;
- url : URL du document demandé ;
Le code du script [http-02.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
//
// gestion des erreurs
//error_reporting(E_ALL & E_STRICT);
//ini_set("display_errors", "on");
//
// constantes
const CONFIG_FILE_NAME = "config-http-02.json";
//
// on récupère la configuration
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// obtenir le texte HTML des URL du fichier de configuration
foreach ($config as $site => $infos) {
// lecture URL du site $ite
$résultat = getUrl($site, $infos["url"], $infos["timeout"]);
// affichage résultat
print "$résultat\n";
}//for
// fin
exit;
//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
// lit l'URL $url et la stocke dans le fichier output/$site.HTML
//
// suivi
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
// Initialisation d'une session cURL
$curl = curl_init($url);
if ($curl === FALSE) {
// il y a eu une erreur
return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
}
// options de curl
$options = [
// mode verbose
CURLOPT_VERBOSE => true,
// nouvelle connexion - pas de cache
CURLOPT_FRESH_CONNECT => true,
// timeout de la requête (en secondes)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// ne pas vérifier la validité des certificats SSL
CURLOPT_SSL_VERIFYPEER => false,
// suivre les redirections
CURLOPT_FOLLOWLOCATION => true,
// récupération du document demandé sous la forme d'une chaîne de caractères
CURLOPT_RETURNTRANSFER => true
];
// paramétrage de curl
curl_setopt_array($curl, $options);
// Execution de la requête
$page_content = curl_exec($curl);
// Fermeture de la session cURL
curl_close($curl);
// exploitation du résultat
if ($page_content !== FALSE) {
// enregistrement du résultat dans $site.HTML
$result = file_put_contents("output/$site.HTML", $page_content);
if ($result === FALSE) {
// retour erreur
return "Erreur lors de la création du fichier [output/$site.HTML]";
}
// retour avec succès
return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
} else {
// il y a eu une erreur de communication
return "Erreur de communication avec le serveur [$site]";
}
}
|
Commentaires
- ligne 14 : on exploite le fichier de configuration pour créer le dictionnaire [$config] ;
- lignes 17-22 : on boucle sur la liste de sites trouvés dans la configuration ;
- ligne 19 : pour chacun des sites, on appelle la fonction [getUrl] qui va télécharger l’URL $infos[«url»] avec un timeout $infos[«timeout»] ;
- ligne 34 : on démarre un session [curl]. [curl_init] ne fait pas encore de connexion au serveur web. Elle rend une ressource [$curl] qui va être un paramètre pour toutes les fonctions [curl] suivantes ;
- lignes 35-38 : si l’initialisation de la session [curl] échoue, la fonction [curl_init] rend le booléen FALSE ;
- lignes 40-54 : le dictionnaire [$options] va paramétrer la connexion [curl] au serveur ;
- ligne 57 : les options de la connexion sont transmises à la ressource [$curl] ;
- ligne 59 : connexion à l’URL demandée avec les options définies. A cause de l’option [CURLOPT_RETURNTRANSFER => true], la fonction [curl_exec] rend comme résultat le document envoyé par le serveur comme une chaîne de caractères. La fonction [curl_exec] rend le booléen FALSE en cas d’échec de la connexion ;
- ligne 64 : on analyse le résultat de [curl_exec] ;
- ligne 66 : on enregistre la page reçue dans un fichier local ;
- lignes 69, 72, 75 : on rend le résultat de la fonction [getUrl] ;
Lorsqu’on exécute le script [http-02.php] on obtient les résultats console suivants :
1 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 152 153 154 155 156 157 158 | * Rebuilt URL to: http://sergetahe.com/
Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
* Trying 87.98.154.146…
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET / HTTP/1.1
Host: sergetahe.com
Accept: */*
< HTTP/1.1 302 Found
< Date: Sat, 18 May 2019 08:46:38 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Location: http://sergetahe.com/cours-tutoriels-de-programmation
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
Host: sergetahe.com
Accept: */*
< HTTP/1.1 301 Moved Permanently
< Date: Sat, 18 May 2019 08:46:38 GMT
< Content-Type: text/HTML; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
Host: sergetahe.com
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:39 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Link: <http://sergetahe.com/cours-tutoriels-de-programmation/wp-json/>; rel="https://api.w.org/"
< Link: <http://sergetahe.com/cours-tutoriels-de-programmation/>; rel=shortlink
< Vary: Accept-Encoding
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
Fin de la communication avec le serveur [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
Client : début de la communication avec le serveur [tahe.developpez.com] ----------------------------
* Connection #0 to host sergetahe.com left intact
* Rebuilt URL to: https://tahe.developpez.com/
* Trying 87.98.130.52…
* TCP_NODELAY set
* Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: C:\myprograms\laragon-lite\etc\ssl\cacert.pem
CApath: none
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=*.developpez.com
* start date: Apr 4 08:25:09 2019 GMT
* expire date: Jul 3 08:25:09 2019 GMT
* subjectAltName: host "tahe.developpez.com" matched cert's "*.developpez.com"
* issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
* SSL certificate verify ok.
> GET / HTTP/1.1
Host: tahe.developpez.com
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:42 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/HTML
<
Fin de la communication avec le serveur [tahe.developpez.com]. Vérifiez le fichier [output/tahe.developpez.com.HTML]
Client : début de la communication avec le serveur [www.polytech-angers.fr] ----------------------------
* Connection #0 to host tahe.developpez.com left intact
* Rebuilt URL to: http://www.polytech-angers.fr/
* Trying 193.49.144.41…
* TCP_NODELAY set
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET / HTTP/1.1
Host: www.polytech-angers.fr
Accept: */*
< HTTP/1.1 301 Moved Permanently
< Date: Sat, 18 May 2019 08:46:45 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Location: http://www.polytech-angers.fr/fr/index.HTML
< Cache-Control: max-age=1
< Expires: Sat, 18 May 2019 08:46:46 GMT
< Content-Length: 339
< Content-Type: text/HTML; charset=iso-8859-1
<
* Ignoring the response-body
* Connection #0 to host www.polytech-angers.fr left intact
* Issue another request to this URL: 'http://www.polytech-angers.fr/fr/index.HTML'
* Found bundle for host www.polytech-angers.fr: 0x1fee4ebe390 [can pipeline]
* Re-using existing connection! (#0) with host www.polytech-angers.fr
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET /fr/index.HTML HTTP/1.1
Host: www.polytech-angers.fr
Accept: */*
< HTTP/1.1 200
< Date: Sat, 18 May 2019 08:46:46 GMT
< Server: Apache/2.4.29 (Ubuntu)
< X-Cocoon-Version: 2.1.13-dev
< Accept-Ranges: bytes
< Last-Modified: Sat, 18 May 2019 08:01:36 GMT
< Content-Type: text/HTML; charset=UTF-8
< Content-Length: 47372
< Vary: Accept-Encoding
< Cache-Control: max-age=1
< Expires: Sat, 18 May 2019 08:46:47 GMT
< Content-Language: fr
<
* Connection #0 to host www.polytech-angers.fr left intact
Fin de la communication avec le serveur [www.polytech-angers.fr]. Vérifiez le fichier [output/www.polytech-angers.fr.HTML]
Client : début de la communication avec le serveur [localhost] ----------------------------
* Rebuilt URL to: http://localhost/
* Trying ::1…
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
Host: localhost
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:47 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
< X-Powered-By: PHP/7.2.11
< Content-Length: 1781
< Content-Type: text/HTML; charset=UTF-8
<
* Connection #0 to host localhost left intact
Fin de la communication avec le serveur [localhost]. Vérifiez le fichier [output/localhost.HTML]
|
Commentaires
on obtient les mêmes échanges qu’avec l’outil [curl] ;
en vert, les logs du script ;
en bleu, les commandes envoyées au serveur ;
en jaune, les commandes reçues en réponse par le client ;
Conclusion
Nous avons, dans ce paragraphe, découvert le protocole HTTP et avons écrit un script [http-02.php] capable de télécharger une URL du web.
Le protocole SMTP (Simple Mail Transfer Protocol)¶
Introduction¶
Dans ce chapitre :
[Serveur B] sera un serveur SMTP local que nous installerons ;
[Client A] sera un client SMTP de diverses formes :
le client [RawTcpClient] pour découvrir le protocole SMTP ;
un script PHP rejouant le protocole SMTP du client [RawTcpClient] ;
un script PHP utilisant la bibliothèque [SwiftMailServer] permettant d’envoyer toutes sortes de mails ;
Création d’une adresse [gmail]
Pour faire nos tests SMTP, nous aurons besoin d’une adresse mail à qui écrire. Nous allons créer pour cela une adresse sur Gmail :
- en [5], nous créons l’utilisateur [php7parlexemple] (choisissez autre chose) ;
- en [6], le mot de passe sera lui [PHP7parlexemple] (choisissez autre chose) ;
- en [7], nous validons ces informations ;
- remplir les cases [9-10] puis valider (11) ;
- accepter les conditions d’utilisation de Google (12-13) puis valider (14) ;
- en [15], la boîte de réception (Inbox) de l’utilisateur [PHP7] (16) ;
- en [17], cet utilisateur a une boîte de réception vide ;
- en [18-19], connectez-vous au compte Google de l’utilisateur [php7parlexemple@gmail.com]. Nous allons configurer la sécurité du compte ;
- en [21], autorisez d’autres applications que celles de Google à exploiter le compte [php7parlexemple]. Si on ne fait pas ça, notre serveur local de mails [hMailServer] ne pourra pas communiquer avec le serveur SMTP de Gmail ;
Installation d’un serveur SMTP¶
Pour nos tests, nous installerons le serveur de mail [hMailServer] qui est à la fois un serveur SMTP permettant d’envoyer des mails, un serveur POP3 (Post Office Protocol) permettant de lire les mails stockés sur le serveur, un serveur IMAP (Internet Message Access Protocol) qui lui aussi permet de lire les mails stockés sur le serveur mais va au-delà. Il permet notamment de gérer le stockage des mails sur le serveur.
Le serveur de mail [hMailServer] est disponible à l’URL [https://www.hmailserver.com/] (mai 2019).
Au cours de l’installation, certains renseignements vous seront demandés :
- en [1-2], sélectionnez à la fois le serveur de mails et les outils pour l’administrer ;
- durant l’installation le mot de l’administrateur vous sera demandé : notez le, car il vous sera nécessaire ;
[hMailServer] s’installe comme un service Windows lancé automatiquement au démarrage de la machine. Il est préférable de choisir un démarrage manuel :
- en [3], on tape [services] dans la zone de saisie de la barre d’état ;
- en [4-8], on met le service en mode [manuel] (6), on le lance (7) ;
Une fois démarré, le serveur [hMailServer] doit être configuré. Le serveur a été installé avec un programme d’administration [hMailServer Administrator] :
- en [2], dans la zone de saisie de la barre d’état, taper [hmailserver] ;
- en [3], lancer l’administrateur ;
- en [4], connecter l’administrateur au serveur [hMailServer] ;
- en [5], taper le mot de passe saisi lors de l’installation de [hMailServer] ;
Nous allons créer un compte utilisateur :
- cliquer droit sur [Accounts] (7) puis (8) pour ajouter un nouvel utilisateur ;
- dans l’onglet [General] (9), nous définissons un utilisateur [guest] (10) avec le mot de passe [guest] (11). Il aura l’adresse mail [guest@localhost] (10) ;
- en [12], l’utilisateur [guest] est activé ;
- en [15], on configure le protocole SMTP du serveur de mail ;
- en [16], on configure la distribution des mails ;
- en [17], la configuration de la distribution des mails à destination de la machine hôte (localhost) ;
- en [18], le nom de la machine locale (localhost). Le script du paragraphe lien vous permet d’avoir ce nom ;
- en [19], on configure un serveur SMTP relais : il s’agit ici du serveur qui s’occupera de la distribution des mails non destinés à la machine locale (localhost) ;
- en [20], le serveur SMTP de Gmail. Nous prenons Gmail car nous y avons créé un compte au paragraphe lien ;
- en [21], le port SMTP de Gmail ;
- en [22], le service SMTP de Gmail est un service sécurisé : il faut un compte Gmail pour y accéder ;
- en [23], l’utilisateur [php7parlexemple] créé au paragraphe lien ;
- en [24], le mot de passe de cet utilisateur : [PHP7parlexemple] créé au paragraphe lien ;
- en [25], on indique le type de protocole de sécurité utilisé par Gmail ;
en [27] le port du service SMTP ;
en [28], ce service ne nécessite pas d’authentification ;
en [30], mettez le message de bienvenue que le serveur SMTP enverra à ses clients ;
Le protocole SMTP
Nous allons découvrir le protocole SMTP avec l’environnement suivant :
- le client A sera le client TCP générique [RawTcpClient] ;
- le serveur B sera le serveur de mails [hMailServer] ;
- le client A demandera au serveur B de distribuer un courrier à l’utilisateur [php7parlexemple@gmail.com] ;
- nous vérifierons que cet utilisateur a bien reçu le mail envoyé ;
Nous lançons le client de la façon suivante :
- en [1], on se connecte sur le port 25 de la machine locale, là où opère le service SMTP de [hMailServer]. l’argument [–quit bye] indique que l’utilisateur quittera le programme en tapant la commande [bye]. Sans cet argument, la commande de fin du programme est [quit]. Or [quit] est également une commande du protocole SMTP. Il nous faut donc éviter cette ambiguïté ;
- en [2], le client est bien connecté ;
- en [3], le client attend des commandes tapées au clavier ;
- en [4], le serveur lui envoie son message de bienvenue ;
- en [5], le client envoie la commande [EHLO nom-de-la-machine-client]. Le serveur lui répond par une suite de messages de la forme [250-xx] (6). Le code [250] indique le succès de la commande envoyée par le client ;
- en [7], le client indique l’expéditeur du message, ici [guest@localhost]. Cet utilisateur doit exister sur le serveur de mails [hMailServer]. C’est le cas ici car nous avons créé cet utilisateur précédemment ;
- en [8], la réponse du serveur ;
- en [9], on indique le destinataire du message, ici l’utilisateur Gmail [php7parlexemple@gmail.com] ;
- en [10], la réponse du serveur ;
- en [11], la commande [DATA] indique au serveur que le client va envoyer le contenu du message ;
- en [12], la réponse du serveur ;
- en [13-16], le client doit envoyer une liste de lignes de texte terminée par une ligne ne contenant qu’un unique point. Le message peut contenir des lignes [Subject :, From :, To :] (13) pour définir respectivement le sujet du message, l’expéditeur, le destinataire ;
- en [14], les entêtes précédents doivent être suivis d’une ligne vide ;
- en [15], le texte du message ;
- en [16], la ligne ne contenant qu’un unique point qui indique la fin du message ;
- en [17], une fois que le serveur a reçu la ligne ne contenant qu’un unique point, il met le message en file d’attente ;
- en [18], le client indique au serveur qu’il a fini ;
- en [19], la réponse du serveur ;
- en [20], on constate que le serveur a fermé la connexion qui le liait au client ;
Maintenant vérifions que l’utilisateur [php7parlexemple@gmail.com] a bien reçu le message :
- en [2], on voit que l’utilisateur [php7parlexemple@gmail.com] a bien reçu le message ;
- en [7], l’expéditeur du mail. On voit que ce n’est pas [guest@localhost]. Cela est dû au fait que c’est le serveur relais défini dans la configuration de [hmailServer] qui a délivré le message. Or ce serveur relais est [smtp.gmail.com] associé aux identifiants de l’utilisateur Gmail [php7parlexemple@gmail.com]. Tout mail provenant de [hMailServer] semblera provenir de l’utilisateur [php7parlexemple@gmail.com]. Ce n’est pas ce qu’on voulait ici mais si on n’utilise pas ce serveur relais, le service SMTP de Gmail refuse les mails envoyés par [hMailServer] car le SMTP de Gmail réclame une authentification que [hMailServer] n’envoie pas. Il y a sans doute moyen de contourner ce problème mais je ne l’ai pas trouvé ;
- en [8], on voit que le mail a été reçu de la machine [DESKTOP-528I5CU] qui héberge le serveur de mails [hMailServer] ;
- en [9], l’expéditeur du message. On voit que ce n’est pas [guest@localhost] ;
- en [10], l’expéditeur original du message. Cette fois-ci c’est bien [guest@localhost] ;
- en [11], le sujet ;
- en [12], le destinataire ;
- en [13], le message ;
Finalement, notre client [RawTcpClient] a réussi à envoyer le message même si on a rencontré un problème pour l’expéditeur. Nous avons les bases pour créer un client SMTP écrit en PHP.
Un client SMTP basique écrit en PHP¶
Nous allons reproduire en PHP ce que nous avons appris précédemment du protocole SMTP.
Le script [smtp-01.php] est configuré par le fichier jSON [config-smtp-01.json] suivant :
1 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 | {
"mail to localhost via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "to localhost via localhost",
"message": "ligne 1\nligne 2\nligne 3"
},
"mail to gmail via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "php7parlexemple@gmail.com",
"subject": "to gmail via localhost",
"message": "ligne 1\nligne 2\nligne 3"
},
"mail to gmail via gmail": {
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "guest@localhost",
"to": "php7parlexemple@gmail.com",
"subject": "to gmail via gmail",
"message": "ligne 1\nligne 2\nligne 3"
}
}
|
[config-smtp-01.json] est un tableau où chacun des éléments est un dictionnaire de type [nom=>infos]. La valeur [infos] est elle-même un dictionnaire avec les clés et valeurs suivantes :
- [smtp-server] : le nom du serveur SMTP à utiliser ;
- [smtp-port] : le n° du port du service SMTP ;
- [from] : l’expéditeur du message ;
- [to] : le destinataire du message ;
- [subject] : le sujet du message ;
- [message] : le message à envoyer ;
- Le 1er élément utilise le serveur SMTP [localhost] pour envoyer un mail à un utilisateur de [localhost] ;
- le 2d élément utilise le serveur SMTP [localhost] pour envoyer un mail à un utilisateur de [Gmail] ;
- le 3e élément utilise le serveur SMTP [Gmail] pour envoyer un mail à un utilisateur de [Gmail] ;
Le code [smtp-01.php] du client SMTP est le suivant :
1 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | <?php
// client SMTP (SendMail Transfer Protocol) permettant d'envoyer un message
// protocole de communication SMTP client-serveur
// -> client se connecte sur le port 25 du serveur smtp
// <- serveur lui envoie un message de bienvenue
// -> client envoie la commande EHLO nom de sa machine
// <- serveur répond OK ou non
// -> client envoie la commande MAIL FROM: <expéditeur>
// <- serveur répond OK ou non
// -> client envoie la commande RCPT TO: <destinataire>
// <- serveur répond OK ou non
// -> client envoie la commande DATA
// <- serveur répond OK ou non
// -> client envoie ttes les lignes de son message et termine avec une ligne contenant le
// seul caractère .
// <- serveur répond OK ou non
// -> client envoie la commande QUIT
// <- serveur répond OK ou non
// les réponses du serveur ont la forme xxx texte où xxx est un nombre à 3 chiffres. Tout
// nombre xxx >=500 signale une erreur.
// La réponse peut comporter plusieurs lignes commençant toutes par xxx sauf la dernière
// de la forme xxx(espace)
// les lignes de texte échangées doivent se terminer par les caractères RC(#13) et LF(#10)
//
// client SMTP (SendMail Transfer Protocol) permettant d'envoyer un message
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
//
// les paramètres de l'envoi du courrier
const CONFIG_FILE_NAME = "config-smtp-01.json";
// on récupère la configuration
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// envoi des courriers
foreach ($mails as $name => $infos) {
// suivi
print "Envoi du mail [$name]\n";
// envoi du courrier
$résultat = sendmail($name, $infos, TRUE);
// affichage résultat
print "$résultat\n";
}//for
// fin
exit;
//sendmail
//-----------------------------------------------------------------------
function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
// envoie message[$name,$infos]. Si $verbose=TRUE , fait un suivi des échanges client-serveur
// on récupère le nom du client
$client = gethostbyaddr(gethostbyname(""));
// ouverture d'une connexion avec le serveur SMTP
$connexion = fsockopen($infos["smtp-server"], (int) $infos["smtp-port"]);
// retour si erreur
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion représente un flux de communication bidirectionnel
// entre le client (ce programme) et le serveur smtp contacté
// ce canal est utilisé pour les échanges de commandes et d'informations
// après la connexion le serveur envoie un message de bienvenue qu'on lit
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde EHLO
$erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde MAIL FROM:
$erreur = sendCommand($connexion, sprintf("MAIL FROM: <%s>", $infos["from"]), $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde RCPT TO:
$erreur = sendCommand($connexion, sprintf("RCPT TO: <%s>", $infos["to"]), $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde DATA
$erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// préparation message à envoyer
// il doit contenir les lignes
// From: expéditeur
// To: destinataire
// Subject:
// ligne vide
// Message
// .
$data = sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\r\n.\r\n", $infos["from"], $infos["to"], $infos["subject"], $infos["message"]);
$erreur = sendCommand($connexion, $data, $verbose, FALSE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde quit
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// fin
fclose($connexion);
return "Message envoyé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF): string {
// envoie $commande dans le canal $connexion
// mode verbeux si $verbose=1
// si $withRCLF=1, ajoute la séquence RCLF à échange
// données
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// envoi cmde si $commande non vide
if ($commande!=="") {
fputs($connexion, "$commande$RCLF");
// écho éventuel
if ($verbose) {
affiche($commande, 1);
}
}//if
// lecture réponse
$réponse = fgets($connexion, 1000);
// écho éventuel
if ($verbose) {
affiche($réponse, 2);
}
// récupération code erreur
$codeErreur = (int) substr($réponse, 0, 3);
// dernière ligne de la réponse ?
while (substr($réponse, 3, 1) === "-") {
// lecture réponse
$réponse = fgets($connexion, 1000);
// écho éventuel
if ($verbose) {
affiche($réponse, 2);
}
}//while
// réponse terminée
// erreur renvoyée par le serveur ?
if ($codeErreur >= 500) {
return substr($réponse, 4);
}
// retour sans erreur
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// affiche $échange à l'écran
// si $sens=1 affiche -->$echange
// si $sens=2 affiche <-- $échange sans les 2 derniers caractères RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//switch
}
|
Commentaires
- ligne 39 : on exploite le fichier de configuration ;
- ligne 42 : on boucle sur les éléments du tableau [mails]. Chaque élément est un dictionnaire [name=>infos] où [name] est un nom qui peut être quelconque et [infos] un dictionnaire contenant les informations nécessaires à l’envoi d’un mail ;
- ligne 46 : l’envoi du mail est assuré par la fonction [sendmail]
qui admet trois paramètres :
- $name : le nom donné à cet envoi ;
- $infos : le dictionnaire contenant les informations nécessaires à l’envoi ;
- verbose : un booléen indiquant si les échanges client / serveur doivent être ou non logués sur la console ;
- ligne 46 : la fonction [sendmail] rend un message d’erreur qui est vide s’il n’y a pas eu d’erreur ;
- ligne 56 : la fonction [sendmail] envoie les différentes
commandes que doit envoyer un client SMTP :
- lignes 77-84 : la commande EHLO ;
- lignes 85-92 : la commande MAIL FROM: ;
- lignes 93-100 : la commande RCPT TO: ;
- lignes 101-108 : la commande DATA ;
- lignes 117-124 : envoi du message (From, To, Subject, texte) ;
- lignes 125-132 : la commande QUIT ;
- ligne 140 : la fonction [sendCommand] est chargée d’envoyer les
commandes du client au serveur SMTP. Elle admet quatre paramètres :
- [$connexion] : la connexion qui relie le client au serveur ;
- [$commande] : la commande à envoyer ;
- [$verbose] : si TRUE alors les échanges client / serveur sont logués sur la console ;
- [$withRCLF] : si TRUE, envoie la commande terminée par la séquence \rn. C’est nécessaire pour toutes les commandes du protocole SMTP, mais [sendCommand] sert aussi à envoyer le message. Là on n’ajoute pas la séquence \rn ;
- lignes 150-157 : la commande est envoyée au serveur ;
- lignes 158-163 : lecture de la 1re ligne de la réponse. Celle-ci peut comporter plusieurs lignes. Chaque ligne a la forme XXX-YYY où XXX est un code numérique sauf la dernière ligne de la réponse qui a la forme XXX YYY (absence du caractère -) ;
- lignes 167-174 : lecture de l’ensemble des lignes de la réponse ;
- ligne 177 : si le code numérique XXX est supérieur à 500, alors le serveur a renvoyé une erreur ;
Résultats
L’exécution du script donne les résultats console suivants :
1 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 | Envoi du mail [mail to localhost via localhost]
<-- [220 Bienvenue sur sergetahe@localhost]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-DESKTOP-528I5CU]
<-- [250-SIZE 20480000]
<-- [250-AUTH LOGIN]
<-- [250 HELP]
--> [MAIL FROM: <guest@localhost>]
<-- [250 OK]
--> [RCPT TO: <guest@localhost>]
<-- [250 OK]
--> [DATA]
<-- [354 OK, send.]
--> [From: guest@localhost
To: guest@localhost
Subject: to localhost via localhost
ligne 1
ligne 2
ligne 3
.
]
<-- [250 Queued (0.016 seconds)]
--> [QUIT]
<-- [221 goodbye]
Message envoyé
Envoi du mail [mail to gmail via localhost]
<-- [220 Bienvenue sur sergetahe@localhost]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-DESKTOP-528I5CU]
<-- [250-SIZE 20480000]
<-- [250-AUTH LOGIN]
<-- [250 HELP]
--> [MAIL FROM: <guest@localhost>]
<-- [250 OK]
--> [RCPT TO: <php7parlexemple@gmail.com>]
<-- [250 OK]
--> [DATA]
<-- [354 OK, send.]
--> [From: guest@localhost
To: php7parlexemple@gmail.com
Subject: to gmail via localhost
ligne 1
ligne 2
ligne 3
.
]
<-- [250 Queued (0.000 seconds)]
--> [QUIT]
<-- [221 goodbye]
Message envoyé
Envoi du mail [mail to gmail via gmail]
<-- [220 smtp.gmail.com ESMTP d9sm21623375wro.26 - gsmtp]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-smtp.gmail.com at your service, [90.93.230.110]]
<-- [250-SIZE 35882577]
<-- [250-8BITMIME]
<-- [250-STARTTLS]
<-- [250-ENHANCEDSTATUSCODES]
<-- [250-PIPELINING]
<-- [250-CHUNKING]
<-- [250 SMTPUTF8]
--> [MAIL FROM: <guest@localhost>]
<-- [530 5.7.0 Must issue a STARTTLS command first. d9sm21623375wro.26 - gsmtp]
5.7.0 Must issue a STARTTLS command first. d9sm21623375wro.26 - gsmtp
Done.
|
lignes 1-26 : l’utilisation du serveur SMTP [hMailServer] pour envoyer un mail à [guest@localhost] se passe bien ;
lignes 27-52 : l’utilisation du serveur SMTP [hMailServer] pour envoyer un mail à [php7parlexemple@gmail.com] se passe bien ;
lignes 53-65 : l’utilisation du serveur SMTP [Gmail] pour envoyer un mail à [php7parlexemple@gmail.com] ne se passe pas bien : en ligne 65, le serveur SMTP envoie un code d’erreur 530 avec le message d’erreur. Celui-ci indique que le client SMTP doit au préalable s’authentifier via une connexion sécurisée. Notre client ne l’a pas fait et est donc refusé ;
Un second client SMTP écrit avec la bibliothèque [SwiftMailer]
Le client précédent offre au moins deux insuffisances :
- il ne sait pas utiliser une connexion sécurisée si le serveur la réclame ;
- il ne sait pas joindre des attachements au message ;
Dans notre nouveau script nous allons utiliser la bibliothèque [SwiftMailer] [https://swiftmailer.symfony.com/] (mai 2019). Le mode d’installation de [SwiftMailer] est décrit à l’URL [https://swiftmailer.symfony.com/docs/introduction.HTML] (mai 2019).
Lancez tout d’abord Laragon :
- en [1], ouvrez un terminal ;
- en [3], vérifiez que vous êtes dans le dossier [<laragon>/www] où <laragon> est le dossier d’installation de Laragon ;
- en [3], tapez la commande indiquée (mai 2019). Vérifiez à l’URL [https://swiftmailer.symfony.com/docs/introduction.HTML] la commande exacte ;
- en [4], il est indiqué qu’aucune installation ni mise à jour a été faite. C’est parce que la bibliothèque avait déjà été installée sur ce poste ;
- en [5], le dossier d’installation de [swiftmailer] [6] ;
- en [7], un fichier dont on aura besoin dans notre script ;
Ceci fait, vérifiez que le dossier [<laragon>/www/vendor] [5] est bien dans la branche [Include Path] de Netbeans (cf paragraphe lien).
Enfin la bibliothèque [SwiftMailer] nécessite que l’extension PHP [mbstring] soit active. Pour cela, on vérifie le fichier [php.ini] (cf paragraphe lien) :
Le script [smtp-02.php] utilisera le fichier de configuration jSON [config-smtp-02.json] suivant :
1 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 | {
"mail to localhost via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "test-localhost",
"message": "ligne 1\nligne 2\nligne 3",
"tls": "FALSE",
"attachments": ["/attachments/Hello from SwiftMailer.html",
"/attachments/Hello from SwiftMailer.pdf",
"/attachments/Hello from SwiftMailer.odt",
"/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
"/attachments/test-localhost.eml"
]
},
"mail to gmail via gmail": {
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "php7parlexemple@gmail.com",
"to": "php7parlexemple@gmail.com",
"subject": "test-gmail-via-gmail",
"message": "ligne 1\nligne 2\nligne 3",
"tls": "TRUE",
"user": "php7parlexemple@gmail.com",
"password": "PHP7parlexemple",
"attachments": ["/attachments/Hello from SwiftMailer.html",
"/attachments/Hello from SwiftMailer.pdf",
"/attachments/Hello from SwiftMailer.odt",
"/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
"/attachments/test-localhost.eml"
]
},
"mail to gmail via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "php7parlexemple@gmail.com",
"subject": "test-gmail-via-localhost",
"message": "ligne 1\nligne 2\nligne 3",
"tls": "FALSE",
"attachments": ["/attachments/Hello from SwiftMailer.html",
"/attachments/Hello from SwiftMailer.pdf",
"/attachments/Hello from SwiftMailer.odt",
"/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
"/attachments/test-localhost.eml"
]
}
}
|
On retrouve les mêmes rubriques que dans le fichier [config-smtp-01.json] avec deux rubriques supplémentaires :
- [tls] : à TRUE indique qu’il faut utiliser une connexion
sécurisée avec le serveur SMTP. Dans le cas où [tls] vaut TRUE,
il faut ajouter deux rubriques :
- [user] : le nom de l’utilisateur qui authentifie la connexion ;
- [password] : son mot de passe ;
Dans notre exemple, nous avons utilisé les identifiants de l’utilisateur [php7parlexemple@gmail.com] pour nous connecter au serveur de Gmail. Utilisez les vôtres ;
- [attachments] : donne les noms des fichiers à attacher au mail ;
Le code du script [smtp-02.php] est le suivant :
1 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 | <?php
// client SMTP (SendMail Transfer Protocol) permettant d'envoyer un message
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
//
// les paramètres de l'envoi du courrier
const CONFIG_FILE_NAME = "config-smtp-02.json";
// on récupère la configuration
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// envoi des courriers
foreach ($mails as $name => $infos) {
// suivi
print "Envoi du mail [$name]\n";
// envoi du courrier
$résultat = sendmail($name, $infos);
// affichage résultat
print "$résultat\n";
}//for
// fin
exit;
//-----------------------------------------------------------------------
function sendmail($name, $infos) {
// envoie $infos[message] au serveur smtp $infos[smtp-server] sur le port $infos[smt-port]
// si $infos[tls] est vrai, le support TLS sera utilisé
// le mail est envoyé de la part de $infos[from]
// pour le destinataire $infos['to']
// Le document $info[attachment] est joint au message
// le message a le sujet $infos[subject]
//
// message au format HTML
$messageHTML = str_replace("\n", "<br/>", $infos["message"]);
try {
// création du message
$message = (new \Swift_Message())
// sujet du message
->setSubject($infos["subject"])
// expéditeur
->setFrom($infos["from"])
// destinataires avec un dictionnaire (setTo/setCc/setBcc)
->setTo($infos["to"])
// texte du message
->setBody($infos["message"])
// variante html
->addPart("<b>$messageHTML</b>", 'text/html')
;
// attachements
foreach ($infos["attachments"] as $attachment) {
// chemin de l'attachement
$fileName = __DIR__ . $attachment;
// on vérifie que le fichier existe
if (file_exists($fileName)) {
// on attache le document au message
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
// erreur
print "L'attachement [$fileName] n'existe pas\n";
}
}
// protocole TLS ?
if ($infos["tls"] === "TRUE") {
// TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"], 'tls'))
->setUsername($infos["user"])
->setPassword($infos["password"]);
} else {
// pas de TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
}
// le gestionnaire de l'envoi
$mailer = new \Swift_Mailer($transport);
// envoi du message
$result = $mailer->send($message);
// fin
return "Message [$name] envoyé";
} catch (\Throwable $ex) {
// erreur
return "Erreur lors de l'envoi du message [$name] : " . $ex->getMessage();
}
}
|
Commentaires
- ligne 10 : nous chargeons le fichier [autoload.php] trouvé dans le dossier [<lagagon>/www/vendor] où <laragon> est le dossier d’installation de Laragon. Ce fichier va permettre de charger les fichiers de définition des classes de [SwiftMailer] dès le 1er usage de ces classes. Il nous évite de mettre autant de [require] que de classes et interfaces de SwiftMailer que nous allons utiliser ;
- ligne 32 : la nouvelle fonction [sendmail] qui a deux
paramètres :
- [$name] qui sert à différentier les messages entre-eux ;
- [$infos] : les informations nécessaires pour envoyer le message à son destinataire ;
- ligne 42 : nous aurons deux versions du message : l’une en plain text l’autre en HTML. Ici, nous changeons les marques de fin de ligne en code HTML <br/> ;
- lignes 45-69 : nous définissons le message à l’aide de la classe [SwiftMessage] ;
- ligne 47 : la méthode [SwiftMessage→setSubject] sert à fixer le sujet du message ;
- ligne 49 : la méthode [SwiftMessage→setFrom] sert à fixer l’expéditeur du message ;
- ligne 51 : la méthode [SwiftMessage→setTo] sert à fixer le destinataire du message ;
- ligne 53 : la méthode [SwiftMessage→setBody] sert à fixer le corps du message ;
- ligne 55 : la méthode [SwiftMessage→addPart] sert à fixer différentes versions du message, ici le message au format HTML. Lorsque le message a des variantes, les lecteurs de courrier affichent la variante préférée de l’utilisateur ;
- lignes 58-69 : la méthode [SwiftMessage→addAttachment] (64) permet d’attacher un fichier au message ;
- lignes 70-79 : une fois le message à envoyer défini, il faut définir comment l’envoyer. Le mode de transport du message est défini par la classe [Swift_SmtpTransport]. Il y a au moins deux informations à fournir : le nom et le port du serveur SMTP. Il y en a également une troisième : le serveur SMTP impose-t-il une authentification sécurisée ?
- lignes 73-75 : l’instance [Swift_SmtpTransport] pour une connexion sécurisée au serveur SMTP ;
- ligne 78 : l’instance [Swift_SmtpTransport] pour une connexion non sécurisée au serveur SMTP ;
- ligne 81 : c’est la classe [SwiftMailer] qui envoie les messages. On doit lui passer le mode de transport choisi ;
- ligne 83 : le message [SwiftMessage] est envoyé par le biais du transport [Swift_SmtpTransport] choisi. La méthode [SwiftMailer→send] rend le booléen FALSE si le message n’a pu être envoyé ;
- lignes 86-89 : la bibliothèque [SwiftMailer] lance une exception dès que quelque chose ne se passe pas bien ;
Note : on notera que l’espace de noms des classes de la bibliothèque [SwiftMailer] est la racine \. On a explicitement noté les classes [SwiftMessage, \Swift_SmtpTransport, \SwiftMailer] pour le rappeler ;
Résultats
Lorsqu’on exécute le script [smtp-02.php] on obtient les résultats console suivants :
1 2 3 4 5 6 | Envoi du mail [mail to localhost via localhost]
Message [mail to localhost via localhost] envoyé
Envoi du mail [mail to gmail via gmail]
Message [mail to gmail via gmail] envoyé
Envoi du mail [mail to gmail via localhost]
Message [mail to gmail via localhost] envoyé
|
Si on consulte le compte Gmail de l’utilisateur [php7parlexemple] on a la chose suivante :
- en [1], le sujet ;
- en [2], l’expéditeur ;
- en [3], le destinataire ;
- en [4], le message ;
- en [5-10], les pièces attachées ;
Si on demande à voir le message original, on obtient le document suivant :
1 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 | Return-Path: <php7parlexemple@gmail.com>
Received: from [127.0.0.1] (lfbn-1-11924-110.w90-93.abo.wanadoo.fr. [90.93.230.110])
by smtp.gmail.com with ESMTPSA id e14sm7773816wma.41.2019.05.26.03.11.53
for <php7parlexemple@gmail.com>
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
Sun, 26 May 2019 03:11:54 -0700 (PDT)
Message-ID: <e613c47a421a66e2cf7f8e319616ec49@swift.generated>
Date: Sun, 26 May 2019 10:11:53 +0000
Subject: test-gmail-via-gmail
From: php7parlexemple@gmail.com
To: php7parlexemple@gmail.com
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_"
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
ligne 1
ligne 2
ligne 3
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="Hello from SwiftMailer.html"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.html"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/pdf; name="Hello from SwiftMailer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.pdf"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/vnd.oasis.opendocument.text; name="Hello from SwiftMailer.odt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.odt"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: image/png; name="Cours-Tutoriels-Serge-Tahé-1568x268.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Cours-Tutoriels-Serge-Tahé-1568x268.png"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: message/rfc822; name=test-localhost.eml
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=test-localhost.eml
Return-Path: guest@localhost
Received: from [127.0.0.1] (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Sat, 25 May 2019 09:48:23 +0200
Message-ID: <620f4628882b011feebe4faa30b45092@swift.generated>
Date: Sat, 25 May 2019 07:48:22 +0000
Subject: test-localhost
From: guest@localhost
To: guest@localhost
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_"
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
j'ai =C3=A9t=C3=A9 invit=C3=A9 =C3=A0 d=C3=A9je=C3=BBner
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>j'ai =C3=A9t=C3=A9 invit=C3=A9 =C3=A0 d=C3=A9je=C3=BBner</b>
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_--
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="Hello from SwiftMailer.html"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.html"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/pdf; name="Hello from SwiftMailer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.pdf"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/vnd.oasis.opendocument.text; name="Hello from SwiftMailer.odt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.odt"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: image/png; name="Cours-Tutoriels-Serge-Tahé-1568x268.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Cours-Tutoriels-Serge-Tahé-1568x268.png"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_--
|
- ligne 9 : le sujet ;
- ligne 10 : l’expéditeur ;
- ligne 11 : le destinataire ;
- ligne 13 : le message contient plusieurs parties délimitées par des balises [–_=_swift_xx] ;
- lignes 19-24 : le message en plain text ;
- lignes 27-30 : le message en HTML ;
- lignes 34-36 : le fichier attaché [Hello from SwiftMailer.html] ;
- lignes 40-42 : le fichier attaché [Hello from SwiftMailer.pdf] ;
- lignes 46-48 : le fichier attaché [Hello from SwiftMailer.odt] ;
- lignes 58-60 : le fichier attaché [Cours-Tutoriels-Serge-Tahé-1568x268.png] ;
- lignes 58-60 : le fichier attaché [test-localhost.eml] ;
- lignes 62-114 : le fichier attaché [test-localhost.eml] est lui-même un message dont le contenu est affiché aux lignes 62-114. On peut constater que ce message contient lui-même des attachements ;
Les protocoles POP3 (Post Office Protocol) et IMAP (Internet Message Access Protocol)¶
Introduction¶
Pour lire les mails entreposés dans un serveur de mails, deux protocles existent :
- le protocole POP3 (Post Office Protocol) historiquement le 1er protocole mais peu utilisé maintenant ;
- le protocole IMAP (Internet Message Access Protocol) protocole plus récent que POP3 et le plus utilisé actuellement ;
Pour découvrir le protocole POP3, nous allons utiliser l’architecture suivante :
[Serveur B] sera un serveur POP3 / IMAP local, implémenté par le serveur de mail [hMailServer] ;
[Client A] sera un client POP3 / IMAP de diverses formes :
le client [RawTcpClient] pour découvrir le protocole POP3 ;
un script PHP rejouant le protocole POP3 du client [RawTcpClient] ;
un script PHP utilisant la bibliothèque IMAP de PHP qui permet d’implémenter aussi bien des clients IMAP que POP3 ;
Découverte du protocole POP3
Tout d’abord, nous utilisons le script [smtp-01.php] pour envoyer un mail à l’utilisateur [guest@localhost]. Si vous avez fait les tests associés au script, cet utilisateur a normalement reçu des mails mais on n’a pas pu le vérifier. Pour lui envoyer un nouveau mail, utilisez par exemple le fichier de configuration [config-smtp-01.json] suivant :
1 2 3 4 5 6 7 8 9 10 | {
"mail to localhost via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "to localhost via localhost",
"message": "ligne 1\nligne 2\nligne 3"
}
}
|
Maintenant voyons avec le client [RawTcpClient] comment on peut lire la boîte mail de l’utilisateur [guest@localhost] :
1 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 | C:\Data\st-2019\dev\php7\php5-exemples\exemples\inet\utilitaires>RawTcpClient --quit bye localhost 110
Client [DESKTOP-528I5CU:55593] connecté au serveur [localhost-110]
Tapez vos commandes (bye pour arrêter) :
<-- [+OK Bienvenue sur sergetahe@localhost]
USER guest@localhost
<-- [+OK Send your password]
PASS guest
<-- [+OK Mailbox locked and ready]
LIST
<-- [+OK 2 messages (610 octets)]
<-- [1 305]
<-- [2 305]
<-- [.]
RETR 1
<-- [+OK 305 octets]
<-- [Return-Path: guest@localhost]
<-- [Received: from DESKTOP-528I5CU.home (localhost [127.0.0.1])]
<-- [ by DESKTOP-528I5CU with ESMTP]
<-- [ ; Tue, 21 May 2019 12:59:11 +0200]
<-- [Message-ID: <1356373A-33C9-4F31-BA43-2B119E128CE3@DESKTOP-528I5CU>]
<-- [From: guest@localhost]
<-- [To: guest@localhost]
<-- [Subject: to localhost via localhost]
<-- []
<-- [ligne 1]
<-- [ligne 2]
<-- [ligne 3]
<-- [.]
DELE 1
<-- [+OK msg deleted]
LIST
<-- [+OK 1 messages (305 octets)]
<-- [2 305]
<-- [.]
DELE 2
<-- [+OK msg deleted]
LIST
<-- [+OK 0 messages (0 octets)]
<-- [.]
QUIT
<-- [+OK POP3 server saying goodbye…]
Perte de la connexion avec le serveur…
|
- ligne 1 : le serveur POP3 travaille généralement avec le port 110. C’est le cas ici ;
- ligne 5 : la commande [USER] sert à définir l’utilisateur dont on veut lire la boîte mail ;
- ligne 7 : la commande [PASS] sert à définir son mot de passe ;
- ligne 9 : la commande [LIST] demande la liste des messages présents dans la boîte à lettres de l’utilisateur ;
- ligne 14 : la commande [RETR] demande à voir le message dont on passe le n° ;
- ligne 29 : la commande [DELE] demande la suppression du message dont on passe le n° ;
- ligne 40 : la commande [QUIT] indique au serveur qu’on a terminé ;
La réponse du serveur peut prendre plusieurs formes :
une ligne unique commençant par [+OK] pour indiquer que la commande précédente du client a réussi ;
une ligne unique commençant par [-ERR] pour indiquer que la commande précédente du client a échoué ;
plusieurs lignes où :
la 1re ligne commence par [+OK] ;
la dernière ligne est constituée d’un unique point ;
Un script basique implémentant le protocole POP3
Comme le procole POP3 a la même structure que le protocole SMTP, le script [pop3-01.php] est un portage du script [smtp-01.php]. Il aura le fichier de configuration [config-pop3-01.json] suivant :
1 2 3 4 5 6 7 8 9 | {
"localhost:110": {
"server": "localhost",
"port": "110",
"user": "guest@localhost",
"password": "guest",
"maxmails":5
}
}
|
- lignes 3-4 : le serveur POP3 interrogé est le serveur local [hMailServer] ;
- lignes 5-6 : on veut lire la boîte à lettres de l’utilisateur [guest@localhost] ;
- ligne 7 : on lira au plus 5 mails ;
Le script [pop3-01.php] est le suivant :
1 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | <?php
// client POP3 (Post Office Protocol) permettant de lire des messages d'une boîte à lettres
// protocole de co mmunication POP3 client-serveur
// -> client se connecte sur le port 110 du serveur smtp
// <- serveur lui envoie un message de bienvenue
// -> client envoie la commande USER utilisateur
// <- serveur répond OK ou non
// -> client envoie la commande PASS mot_de_passe
// <- serveur répond OK ou non
// -> client envoie la commande LIST
// <- serveur répond OK ou non
// -> client envoie la commande RETR n° pour chacun des mails
// <- serveur répond OK ou non. Si OK envoie le contenu du mail demandé
// -> serveur envoie ttes les lignes du mail et termine avec une ligne contenant le
// seul caractère .
// -> client envoie la commande DELE n° pour supprimer un mail
// <- serveur répond OK ou non
// // -> client envoie la commande QUIT pour terminer le dialogue avec le serveur
// <- serveur répond OK ou non
// les réponses du serveur ont la forme +OK texte où -ERR texte
// La réponse peut comporter plusieurs lignes. Alors la dernière est constitué d'un unique point
// les lignes de texte échangées doivent se terminer par les caractères RC(#13) et LF(#10)
//
// client POP3 (SendMail Transfer Protocol) permettant de lire des mails
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
//
// les paramètres de l'envoi du courrier
const CONFIG_FILE_NAME = "config-pop3-01.json";
// on récupère la configuration
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// lecture des boîtes à lettres
foreach ($mailboxes as $name => $infos) {
// suivi
print "Lecture de la boîte à lettres [$name]\n";
// lecture de la boîte à lettre
$résultat = readmail($name, $infos, TRUE);
// affichage résultat
print "$résultat\n";
}//for
// fin
exit;
//readmail
//-----------------------------------------------------------------------
function readmail(string $name, array $infos, bool $verbose = TRUE): string {
// lit le contenu de la boîte à lettres [$name]
// importe tous les messages
// chaque message est supprimé aprèsb sa lecture
// Si $verbose=1, fait un suivi des échanges client-serveur
//
// ouverture d'une connexion avec le serveur SMTP
$connexion = fsockopen($infos["server"], (int) $infos["port"]);
// retour si erreur
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion représente un flux de communication bidirectionnel
// entre le client (ce programme) et le serveur pop3 contacté
// ce canal est utilisé pour les échanges de commandes et d'informations
// après la connexion le serveur envoie un message de bienvenue qu'on lit
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde USER
$erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde PASS
$erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde LIST
$premièreLigne = "";
$erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// analyse de la 1re ligne pour connaître le nbre de messages
$champs = [];
preg_match("/^\+OK (\d+)/", $premièreLigne, $champs);
$nbMessages = (int) $champs[1];
// on boucle sur les messages
$iMessage = 0;
while ($iMessage < $nbMessages && $iMessage < $infos["maxmails"]) {
// cmde RETR
$erreur = sendCommand($connexion, "RETR " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// cmde DELE
$erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// msg suivant
$iMessage++;
}
// cmde QUIT
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// fermeture de la connexion
fclose($connexion);
// retour
return $erreur;
}
// fin
fclose($connexion);
return "Terminé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF, string &$premièreLigne = ""): string {
// envoie $commande dans le canal $connexion
// mode verbeux si $verbose=1
// si $withRCLF=1, ajoute la séquence RCLF à échange
// met la 1re ligne de la réponse dans [$premièreLigne
// ]
// données
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// envoi cmde si $commande non vide
if ($commande !== "") {
fputs($connexion, "$commande$RCLF");
// écho éventuel
if ($verbose) {
affiche($commande, 1);
}
}//if
// lecture réponse
$réponse = fgets($connexion, 1000);
// on mémorise la 1re ligne
$premièreLigne = $réponse;
// écho éventuel
if ($verbose) {
affiche($réponse, 2);
}
// récupération code erreur
$codeErreur = substr($réponse, 0, 1);
if ($codeErreur === "-") {
// il y a eu une erreur
return substr($réponse, 5);
}
// cas particuliers des cmdes RETR et LIST qui ont des réponses à plusieurs lignes
$commande = substr(strtolower($commande), 0, 4);
if ($commande === "list" || $commande === "retr") {
// dernière ligne de la réponse ?
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
while (!$match) {
// lecture réponse
$réponse = fgets($connexion, 1000);
// écho éventuel
if ($verbose) {
affiche($réponse, 2);
}
// analyse réponse
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
}//while
}
// retour sans erreur
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// affiche $échange à l'écran
// si $sens=1 affiche -->$echange
// si $sens=2 affiche <-- $échange sans les 2 derniers caractères RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//switch
}
|
Commentaires
Comme nous l’avons dit, [pop3-01.php] est un portage du script [smtp-01.php] que nous avons déjà commenté. Nous ne commenterons que les principales différences :
- ligne 55 : la fonction [readmail] est chargée de lire les mails de la boîte aux lettres. Les informations pour se connecter à cette boîte à lettres sont dans le dictionnaire [$infos] ;
- lignes 61-66 : ouverture d’une connexion avec le serveur POP3 ;
- lignes 71-77 : lecture du message de bienvenue envoyé par le serveur ;
- lignes 78-85 : on envoie la commande [USER] pour identifier l’utilisateur dont on veut les mails ;
- lignes 86-93 : on envoie la commande [PASS] pour donner le mot de passe de cet utilisateur ;
- lignes 94-102 : on envoie la commande [LIST] pour savoir combien il y a de mails dans la boîte à lettres de cet utilisateur.
- ligne 96 : on ajoute le paramètre [$premièreLigne] dans les paramètres de la fonction [readmail]. Dans la 1re ligne de sa réponse à la commande LIST, le serveur indique combien il y a de messages dans la boîte à lettres ;
- lignes 104-106 : on récupère le nombre de messages dans la 1re ligne de la réponse ;
- lignes 109-128 : on boucle sur chacun des messages. Pour chacun d’eux
on émet deux commandes :
- RETR i : pour récupérer le message n° i (lignes 111-117) ;
- DELE i : pour le supprimer une fois qu’il a été lu (lignes 118-125) ;
- lignes 129-136 : on envoie la commande [QUIT] pour dire au serveur qu’on a terminé ;
- lignes 178-194 : pour les commandes [LIST] et [RETR], la réponse du serveur a plusieurs lignes, la dernière étant constituée d’un unique point ;
Résultats
A l’exécution, on obtient les résultats suivants :
1 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 | Lecture de la boîte à lettres [localhost:110]
<-- [+OK Bienvenue sur sergetahe@localhost]
--> [USER guest@localhost]
<-- [+OK Send your password]
--> [PASS guest]
<-- [+OK Mailbox locked and ready]
--> [LIST]
<-- [+OK 1 messages (305 octets)]
<-- [1 305]
<-- [.]
--> [RETR 1]
<-- [+OK 305 octets]
<-- [Return-Path: guest@localhost]
<-- [Received: from DESKTOP-528I5CU.home (localhost [127.0.0.1])]
<-- [ by DESKTOP-528I5CU with ESMTP]
<-- [ ; Tue, 21 May 2019 14:25:39 +0200]
<-- [Message-ID: <5F912826-F9C4-41B6-BDA7-4A29537781C9@DESKTOP-528I5CU>]
<-- [From: guest@localhost]
<-- [To: guest@localhost]
<-- [Subject: to localhost via localhost]
<-- []
<-- [ligne ]
<-- [ligne ]
<-- [ligne 3]
<-- [.]
--> [DELE 1]
<-- [+OK msg deleted]
--> [QUIT]
<-- [+OK POP3 server saying goodbye…]
Terminé
Done.
|
Nous avons là un client POP3 basique auquel il manque certaines capacités :
- la possibilité de dialoguer avec un serveur POP3 sécurisé ;
- la possibilité de lire les pièces attachées à un message ;
Nous allons implémenter la 1re possibilité avec les fonctions [imap] de PHP.
Client POP3 / IMAP implémenté avec les fonctions [imap] de PHP¶
Il nous faut tout d’abord vérifier que les fonctions [imap] sont disponibles dans la version de PHP que nous utilisons. Nous ouvrons le fichier [php.ini] décrit au paragraphe lien et nous cherchons les lignes qui parlent de [imap] :
Ligne 895, vérifiez que l’extension [imap] est bien activée.
Le script [imap-01.php] exploitera le fichier jSON [config-imap-01.json] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {
"{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX": {
"imap-server": "imap.gmail.com",
"imap-port": "993",
"user": "php7parlexemple@gmail.com",
"password": "PHP7parlexemple",
"output-dir": "output/gmail-imap",
"prefix": "message-"
},
"{localhost:110/pop3}": {
"imap-server": "localhost",
"imap-port": "110",
"user": "guest@localhost",
"password": "guest",
"pop3": "TRUE",
"output-dir": "output/localhost-pop3",
"prefix": "message-"
}
}
|
Le fichier [config-imap-01.json] définit un tableau de serveurs IMAP / POP3 à contacter. Chaque élément est une structure [clé:valeur], où :
- [clé] : est le serveur à contacter. Nous en avons deux ici :
- [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX] : désigne le serveur [imap.gmail.com] qui écoute sur le port 993. Le protocole client / serveur est IMAP. Le paramètre /ssl indique que la communication client / serveur est sécurisée. La paramètre /novalidate-cert demande au client de ne pas vérifier le certificat de sécurité que le serveur va lui envoyer. Enfin un serveur IMAP gère un ensemble de boîtes à lettres pour un même utilisateur. En précisant INBOX dans l’URL du serveur IMAP, nous indiquons que nous nous intéressons à la boîte à lettres nommée INBOX qui est normalement celle où arrivent les nouveaux messages ;
- [{localhost:110/pop3}INBOX] : désigne le serveur [localhost] qui écoute sur le port 110. Le protocole client / serveur est ici POP3 ;
- [valeur] : est un dictionnaire précisant les points suivants :
- [imap-server] : le nom du serveur IMAP ou POP3 ;
- [imap-port] : le port du serveur IMAP ou POP3 ;
- [user] : le propriétaire dont on veut lire la boîte à lettres ;
- [password] : son mot de passe ;
- [output-dir] : le dossier dans lequel on doit enregistrer les messages ;
- [prefix] : le nom des fichiers où seront enregistrés les messages seront de la forme prefixN où N est un n° de message ;
- [pop3] : un booléen à TRUE pour dire que le protocole utilisé est POP3. Dans ce cas après avoir lu un message, on le supprimera. C’est le fonctionnement habituel des serveurs POP3 : un message lu n’est pas conservé sur le serveur ;
Le script [imap-01.php] est le suivant :
1 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 | <?php
// client IMAP (Internet Message Access Protocol) permettant de lire des mails
//
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// gestion des erreurs
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
//
// les paramètres de lecture du courrier
const CONFIG_FILE_NAME = "config-imap-01.json";
// on récupère la configuration
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// lecture des boîtes à lettres
foreach ($mailboxes as $name => $infos) {
// suivi
print "------------Lecture de la boîte à lettres [$name]\n";
// lecture de la boîte à lettres
readmailbox($name, $infos);
}
// fin
exit;
//-----------------------------------------------------------------------
function readmailbox(string $name, array $infos): void {
// Tentative de connexion
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
// Test sur le retour de la fonction imap_open()
if (!$imapResource) {
// Échec
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
} else {
// Connexion établie
print "Connexion établie avec le serveur [$name].\n";
// total des messages dans la boîte à lettres
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// messages non lus dans la boîte aux lettres courante
if ($nbmsg > 0) {
print "Récupération de la liste des messages non lus de la boîte à lettres [$name]\n";
$msgNumbers = imap_search($imapResource, 'UNSEEN');
if ($msgNumbers === FALSE) {
print "Il n'y a pas de nouveaux messages dans la boîte à lettres [$name]\n";
} else {
foreach ($msgNumbers as $msgNumber) {
// on récupère des informations sur le message n° $msgNumber
$infosMail = imap_headerinfo($imapResource, $msgNumber);
if ($infosMail === FALSE) {
print "Statut du message n° [$msgNumber] de la boîte à lettres [$name] non récupéré : " . imap_last_error() . "\n";
} else {
print "Statut du message n° [$msgNumber] de la boîte à lettres [$name]\n";
print_r($infosMail);
}
// on récupère le corps du message n° $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// si le protocole est POP3, on supprime le message
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// on supprime le message en deux temps
imap_delete($imapResource, $msgNumber);
imap_expunge($imapResource);
}
}
}
}
}
// fermeture de la connexion
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// Échec
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// réussite
print "Fermeture de la connexion réussie.\n";
}
}
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// on récupère le corps du message n° $msgNumber
$corpsMail = imap_body($imapResource, $msgNumber);
print "Enregistrement du message dans le fichier {$infos["output-dir"]}/{$infos["prefix"]}$msgNumber\n";
// on crée le dossier si besoin est
if (!file_exists($infos["output-dir"])) {
mkdir($infos["output-dir"]);
}
// on enregistre le message
if (!file_put_contents($infos["output-dir"] . "/" . $infos["prefix"] . $msgNumber, $corpsMail)) {
print "Echec de l'enregistrement\n";
}
}
|
Commentaires
- lignes 19-24 : on boucle sur l’ensemble des serveurs trouvés dans le fichier de configuration ;
- ligne 32 : la fonction [raedmailbox] lit la boîte à lettres indiqué dans [$name] ;
- ligne 32 : ouverture d’une connexion IMAP ;
- le 1er paramètre est l’URL IMAP de la boîte à lettre à lire ;
- le second paramètre est le nom de l’utilisateur propriétaire de cette boîte à lettres ;
- le troisième paramètre est son mot de passe ;
La fonction [imap_open] organise la sécurisation de la connexion si l’URL IMAP de la boîte à lettres a le paramètre /ssl ;
- ligne 41 : la fonction [imap_num_msg] permet d’avoir le nombre total de messages de la boîte à lettres ;
- ligne 46 : la fonction [imap_search] permet de chercher certains messages. Ici, nous cherchons les messages qui n’ont pas encore été lus (UNSEEN). Le 2e paramètre est un critère de sélection. Il en existe une bonne vingtaine. La fonction [imap_search] rend un tableau de n°s de messages. Ceux-ci peuvent avoir deux formes : n° de séquence ou identifiant UID de message. Par défaut, La fonction [imap_search] rend un tableau de n°s de séquence. Si on ajoute un troisième paramètre [SE_UID] on aura les identifiants UID des messages ;
- ligne 47 : la fonction [imap_search] rend le booléen FALSE si elle n’a trouvé aucun message ;
- ligne 50 : on boucle sur tous les messages non lus ;
- ligne 52 : un message a des entêtes qu’on peut obtenir avec la fonction [imap_headerinfo]. Son 2e paramètre est normalement un n° de séquence de message. Si on veut mettre un identifiant UID de message, il faut mettre le 3e paramètre à [FT_UID] ;
- ligne 53 : la fonction [imap_headerinfo] rend le booléen FALSE si elle n’a pas pu faire son travail. Sinon elle rend un objet complexe qu’on affiche avec la fonction [print_r], ligne 57 ;
- ligne 60 : après ses entêtes, on demande maintenant le corps du message avec la fonction [imap_body]. Cette fonction rend NULL si elle n’a pas pu faire son travail ;
- lignes 84-87 : on enregistre le corps du message dans un fichier local ;
- lignes 63-68 : si le protocole utilisé était POP3, on supprime le
message qui vient d’être lu :
- la fonction [imap_delete] marque le message comme « à supprimer » mais ne le supprime pas ;
- la fonction [imap_expunge] supprime physiquement tous les messages qui ont été marqués « à supprimer » ;
- ligne 74 : on ferme la connexion avec le serveur IMAP. On utilise pour cela la fonction [imap_close] ;
- ligne 86 : la fonction [imap_body] permet d’avoir le corps d’un message repéré par son n° ;
Exécutons le script [smtp-02.json] pour que l’utilisateur [php7parlexemple] de Gmail et l’utilisateur [guest] de [localhost] aient de nouveaux messages. Ceci fait, exécutons le script [imap-01.php] pour lire leurs boîtes à lettres.
Les résultats console sont les suivants :
1 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 | ------------Lecture de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Connexion établie avec le serveur [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX].
Il y a [27] messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Récupération de la liste des messages non lus de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Statut du message n° [26] de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
stdClass Object
(
[date] => Wed, 22 May 2019 10:08:24 +0000
[Date] => Wed, 22 May 2019 10:08:24 +0000
[subject] => test-gmail-via-gmail
[Subject] => test-gmail-via-gmail
[message_id] => <d8405cac62d57bd9c531ea79c146c72d@swift.generated>
[toaddress] => php7parlexemple@gmail.com
[to] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[fromaddress] => php7parlexemple@gmail.com
[from] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[reply_toaddress] => php7parlexemple@gmail.com
[reply_to] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[senderaddress] => php7parlexemple@gmail.com
[sender] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[Recent] =>
[Unseen] => U
[Flagged] =>
[Answered] =>
[Deleted] =>
[Draft] =>
[Msgno] => 26
[MailDate] => 22-May-2019 10:08:29 +0000
[Size] => 19086
[udate] => 1558519709
)
Enregistrement du message dans le fichier output/gmail-imap/message-26
Statut du message n° [27] de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
stdClass Object
(
…
)
Enregistrement du message dans le fichier output/gmail-imap/message-27
Fermeture de la connexion réussie.
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
Statut du message n° [1] de la boîte à lettres [{localhost:110/pop3}]
stdClass Object
(
…
)
Enregistrement du message dans le fichier output/localhost-pop3/message-1
Fermeture de la connexion réussie.
Done.
|
Si aussitôt après ces résultats, nous réexécutons le script [imap-01.php], les résultats sont alors les suivants :
1 2 3 4 5 6 7 8 9 10 | ------------Lecture de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Connexion établie avec le serveur [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX].
Il y a [27] messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Récupération de la liste des messages non lus de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Il n'y a pas de nouveaux messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Fermeture de la connexion réussie.
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [0] messages dans la boîte à lettres [{localhost:110/pop3}]
Fermeture de la connexion réussie.
|
- ligne 3 : il y a toujours le même nombre de messages dans la boîte à lettres Gmail mais il n’y a plus de nouveaux messages non lus (ligne 5). Ceci montre que l’exécution précédente a passé les messages lus du statut « non lu » au statut « lu » ;
- ligne 9 : il n’y a plus de messages dans la boîte à lettres de l’utilisateur [guest@localhost]. Cela vient du fait que dans l’exécution précédente, les messages lus sur [localhost] étaient ensuite supprimés ;
Les messages ont été enregistrés localement :
Si on regarde par-exemple le contenu du message n° 26 de Gmail, on a la chose suivante :
1 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 | --_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_
Content-Type: multipart/alternative;
boundary="_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_"
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
ligne 1
ligne 2
ligne 3
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_--
--_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_
Content-Type: application/pdf; name=Hello.pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=Hello.pdf
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nHWPuQoCQQyG+3mK1MKMyThHFoaAq7uF3cKAhdh5gIXgNr6+swcWshII
……………………………….…
OTQwODU4RDUzRDVENjU0QzJCNTM3Mjc+IF0KL0RvY0NoZWNrc3VtIC9DMjU3MUY1MUNDRjgwQ0Ex
ODU0OUI0RTQ4NDkwMDM3OAo+PgpzdGFydHhyZWYKMTIzMjYKJSVFT0YK
--_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_--
|
- lignes 11-13 : le message en plain text ;
- ligne 19 : le message HTML ;
- ligne 25 : la pièce attachée ;
Essayons d’améliorer ce script pour avoir dans des fichiers séparés, les différents types de messages ainsi que les pièce attachées.
Client POP3 / IMAP amélioré¶
Dans le script [imap-01.php], on affiche le corps du message n° i comme un fichier texte contenant à la fois les différents types de messages ainsi que le contenu codé des différentes pièce attachées. Il est possible d’obtenir la structure du message pour en connaître ces différentes parties. Dans le script [imap-02.php], nous modifions la fonction [getMailBody] de la façon suivante :
1 2 3 4 5 6 | function getMailBody($imapResource, int $msgNumber, array $infos): void {
// on récupère la structure du message
$structure=imap_fetchstructure($imapResource, $msgNumber);
// on l'affiche
print_r($structure);
}
|
- ligne 3 : nous demandons la structure du message ;
- ligne 5 : nous l’affichons ;
Le but est de connaître les informations contenues dans la structure d’un message pour voir comment on peut en obtenir les différentes parties. Dans notre exemple, le message est envoyé par le script [smtp-02.php] avec la configuration [config-smtp-02.json] suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {
"mail to localhost via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "test-localhost",
"message": "ligne 1\nligne 2\nligne 3",
"tls": "FALSE",
"attachments": [
"/attachments/Hello from SwiftMailer.html",
"/attachments/Hello from SwiftMailer.pdf",
"/attachments/Hello from SwiftMailer.odt",
"/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
"/attachments/test-localhost.eml"
]
}
}
|
C’est donc un message avec cinq attachements qui est envoyé à [guest@localhost] (lignes 11-15). Le script [imap-02.php] est exécuté avec la configuration [config-imap-01.json] suivante :
1 2 3 4 5 6 7 8 9 10 | {
"{localhost:110/pop3}": {
"imap-server": "localhost",
"imap-port": "110",
"user": "guest@localhost",
"password": "guest",
"pop3": "TRUE",
"output-dir": "output/localhost-pop3"
}
}
|
C’est donc la boîte à lettres de [guest@localhost] qui est exploitée (ligne 5). Le script [imap-02.php] affiche alors la structure du message envoyé par [smtp-02.php]. Cette structure, affichée à la console, est la suivante :
1 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => MIXED
[ifdescription] => 0
[ifid] => 0
[bytes] => 253599
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => ALTERNATIVE
[ifdescription] => 0
[ifid] => 0
[bytes] => 429
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872296_1e51aae79dfca4e7e0af112489fe8734_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => PLAIN
[ifdescription] => 0
[ifid] => 0
[lines] => 3
[bytes] => 27
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
[1] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => HTML
[ifdescription] => 0
[ifid] => 0
[lines] => 1
[bytes] => 40
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
)
)
[1] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
[ifdescription] => 0
[ifid] => 0
[bytes] => 16302
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.html
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.html
)
)
)
[2] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => PDF
[ifdescription] => 0
[ifid] => 0
[bytes] => 17514
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.pdf
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.pdf
)
)
)
[3] => stdClass Object
(
…
)
[4] => stdClass Object
(
…
)
[5] => stdClass Object
(
[type] => 2
[encoding] => 3
[ifsubtype] => 1
[subtype] => RFC822
[ifdescription] => 0
[ifid] => 0
[lines] => 1881
[bytes] => 146682
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => test-localhost.eml
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => test-localhost.eml
)
)
[parts] => Array
(
…
)
)
)
)
|
Commentaires
- la documentation PHP de la fonction [imap_fetchstructure] donne la signification des différents champs de l’objet retourné par la fonction :
Les valeurs numériques du champ [type] ont la signification suivante :
Les valeurs numériques du champ [encoding] ont la signification suivante :
Le message enregistré par [imap-01.php] commençait par le texte suivant :
- Return-Path: <php7parlexemple@gmail.com>
- Received: from [127.0.0.1] (lfbn-1-11924-110.w90-93.abo.wanadoo.fr. [90.93.230.110])
- by smtp.gmail.com with ESMTPSA id e14sm7773816wma.41.2019.05.26.03.11.53
- for <php7parlexemple@gmail.com>
- (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
- Sun, 26 May 2019 03:11:54 -0700 (PDT)
- Message-ID: <e613c47a421a66e2cf7f8e319616ec49@swift.generated>
- Date: Sun, 26 May 2019 10:11:53 +0000
- Subject: test-gmail-via-gmail
- From: php7parlexemple@gmail.com
- To: php7parlexemple@gmail.com
- MIME-Version: 1.0
- Content-Type: multipart/mixed; boundary= »_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_ »
- –_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
- Content-Type: multipart/alternative; boundary= »_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_ »
- –_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
- Content-Type: text/plain; charset=utf-8
- Content-Transfer-Encoding: quoted-printable
- ligne 1
- ligne 2
- ligne 3
- –_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
- Content-Type: text/HTML; charset=utf-8
- Content-Transfer-Encoding: quoted-printable
- <b>ligne 1<br/>ligne 2<br/>ligne 3</b>
- –_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_–
- –_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
- lignes o) et ag) délimitent le message de type [multipart/mixed] (ligne m) ;
- lignes r) et z) délimitent la 1re partie du message : le message en plain text ;
- lignes z) et af) délimitent la seconde partie du message : le message HTML ;
Nous retrouvons les différentes informations du message ci-dessus dans l’objet retourné par [imap_fetchstructure] :
1 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 | stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => MIXED
[ifdescription] => 0
[ifid] => 0
[bytes] => 253599
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => ALTERNATIVE
[ifdescription] => 0
[ifid] => 0
[bytes] => 429
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872296_1e51aae79dfca4e7e0af112489fe8734_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => PLAIN
[ifdescription] => 0
[ifid] => 0
[lines] => 3
[bytes] => 27
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
[1] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => HTML
[ifdescription] => 0
[ifid] => 0
[lines] => 1
[bytes] => 40
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
)
)
|
- ligne 3 : le message est de type MIME (Multipurpose Internet Mail Extensions) [multipart] ;
- ligne 4 : le message est encodé en 7 bits ;
- ligne 5 : [ifsubtype]=1 indique qu’il y a un champ [subtype] dans la structure ;
- ligne 6 : le champ [subtype] désigne un sous-type MIME, ici le type [mixed]. Au total le type MIME du document est [multipart/mixed] ;
- ligne 7 : [ifdescription]=0 indique qu’il n’y a pas de champ [description] dans la structure ;
- ligne 8 : [ifid]=0 indique qu’il n’y a pas de champ [id] dans la structure ;
- ligne 10 : [ifdisposition]=0 indique qu’il n’y a pas de champ [disposition] dans la structure ;
- ligne 11 : [ifdparameters]=0 indique qu’il n’y a pas de champ [dparameters] dans la structure ;
- ligne 12 : [ifparameters]=1 indique qu’il y a un champ [parameters] dans la structure ;
- ligne 13 : le champ [parameters] décrit les paramètres du message. Ici il n’y en a qu’un ;
- lignes 15-19 : cet objet décrit la ligne suivante du message texte :
1 | boundary="_=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_"
|
Ces lignes servent à délimiter le message. Dans le message récupéré par [imap-01.php], la partie du message qui vient d’être décrite correspond à la ligne m). L’attribut [boundary] n’est pas le même car les copies d’écran correspondent au même message mais envoyé à des moments différents ;
- ligne 23 : commencent ici la structure des différentes parties du message ;
- lignes 25-45 : cette 1re partie est de type [multipart/alternative]. Elle correspond à la ligne p) du texte du message ;
- ligne 47 : cette 1re partie a elle-même des sous-parties ;
- lignes 47-70 : cette 1re sous-partie est de type [text/plain] (lignes 51, 54), est encodée en type [ENCQUOTEDPRINTABLE] (ligne 52) et a un paramètre [charset=utf-8] (lignes 66-67) ;
- les lignes 49-72 décrivent les lignes s-x du message texte ;
- lignes 74-99 : décrivent la seconde sous-partie de la partie [multipart/alternative] ;
- lignes 74-99 : cette seconde sous-partie est de type [text/HTML] (lignes 76, 79), est encodée en type [ENCQUOTEDPRINTABLE] (ligne 77) et a un paramètre [charset=utf-8] (lignes 89-93) ;
- les lignes 74-99 décrivent les lignes aa-ad du message texte ;
La partie [multipart/alternative] est désormais terminée. Commence la partie [application/vnd.openxmlformats-officedocument.wordprocessingml.document] décrite par le texte suivant :
- Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name= »Hello from SwiftMailer.html »
- Content-Transfer-Encoding: base64
- Content-Disposition: attachment; filename= »Hello from SwiftMailer.html »
Là encore, ces informations se retrouvent dans l’objet rapporté par la fonction [imap_fetchstructure] :
1 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 | [1] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
[ifdescription] => 0
[ifid] => 0
[bytes] => 16302
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.html
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.html
)
)
)
|
- ligne 1 : c’est la seconde partie du message global. On rappelle que la 1re partie était de type [multipart/alternative] ;
- lignes 3-6 : cette seconde partie est de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] (lignes 3 et 6), est encodée en Base 64 (ligne 4) ;
- ligne 11 : cette seconde partie est une pièce attachée (ligne 11) et a deux paramètres : [filename=Hello from SwiftMailer.html] (lignes 15-21) et [name=Hello from SwiftMailer.html] (lignes 26-32). On remarquera que ce dernier paramètre n’existe pas dans le message texte. Il a donc été rajouté dans la fonction [imap_fetchstructure] ;
Les lignes 1-36 sont reproduites pour chacun des cinq attachements du message.
La fonction [imap_fetch_structure] nous permet donc d’avoir la structure d’un message. Celle-ci définit des parties qui elles-mêmes peuvent avoir des sous-parties. Pour avoir le texte d’une partie ou sous-partie on utilise la fonction [imap_fetchbody].
Nous modifions la fonction [getMailBody] qui nous permet d’avoir le corps d’un message de la façon suivante :
1 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 | function getMailBody($imapResource, int $msgNumber, array $infos, object $infosMail): void {
// on récupère la structure du message
$structure = imap_fetchstructure($imapResource, $msgNumber);
if ($structure !== FALSE) {
// on récupère ces différentes parties
getParts($imapResource, $msgNumber, $infos, $infosMail, $structure);
}
}
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings=["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : ".$encodings[$part->encoding]."\n";
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
}
}
|
Commentaires
- ligne 3 : nous récupérons la structure du message ;
- ligne 6 : nous demandons à voir ses différentes parties qui sont dans le tableau [parts] de la structure ;
- ligne 10 : la fonction [getParts] reçoit les paramètres
suivants :
- [$imapResource] : la connexion au serveur IMAP ;
- [$msgNumber] : le n° de séquence du message dont on veut les parties ;
- [$infos] : des informations pour savoir où stocker les parties qu’on va trouver, dans le système de fichiers local ;
- [$infosMail] : des informations générales sur le mail (expéditeur, destinataire(s), sujet… ;
- [$part] : un objet qui représente une partie du message ;
- [$sectionNumber] : un n° de section (ou de partie) du message ;
- lignes 17-34 : on affiche le type de contenu de la partie n° [$section] du message. Pour cela on s’aide des champs [$part→type] et [$part→subtype] de la partie [$part] ;
- lignes 36-37 : on affiche le type de codage de la partie [$sectionNumber] ;
- lignes 40-47 : peut-être que la partie dont on vient d’afficher les informations a elle-même des sous-parties ;
- lignes 41-46 : si c’est le cas, on demande à voir le type de contenu des différentes sous-parties de la partie que l’on vient d’afficher. On fait ici, un appel récursif à la fonction [getParts] ;
De nouveau nous envoyons un mail à l’utilisateur Gmail [php7parlexemple@gmail.com] avec le script [smtp-02.php] et nous le lisons avec le script précédent [imap-02.php]. Cela donne les résultats console suivants :
1 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 | ------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
-----contenu de la partie n° [6.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : 7 bits
Fermeture de la connexion réussie.
|
On arrive bien à récupérer les différents types de contenu du message ainsi que leur type de codage. La numérotation des parties suit la règle suivante :
- lignes 6-7 : la partie [multipart/mixed] qui représente la totalité du message porte le n° 0. Les différentes parties de cet objet vont alors porter les n°s 1, 2…
Le message a au total cinq parties :
- lignes 9-10 : la partie [multipart/alternative] qui porte le n° 1 ;
- lignes 17-18 : la partie [APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT] qui porte le n° 2. C’est l’attachement d’un fichier Word ;
- lignes 20-21 : la partie [APPLICATION/PDF] qui porte le n° 3. C’est l’attachement d’un fichier PDF ;
- lignes 23-24 : la partie [APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT] qui porte le n° 4. C’est l’attachement d’un fichier OpenOffice ;
- lignes 26-27 : la partie [UNKNOWN/PNG] qui porte le n° 5. C’est l’attachement d’un fichier image ;
- lignes 30-31 : la partie [MESSAGE/RFC822] qui porte le n° 6. C’est l’attachement d’un mail ;
Lorsqu’une partie a des sous-parties, celles-ci sont numérotées x.1, x.2… où x est le n° de la partie englobante. Ainsi :
- lignes 11-12 : la 1re partie de la partie [multipart/alternative] porte le n° 1.1. C’est un contenu de type [text/plain] : le message du mail ;
- lignes 14-15 : la 2e partie de la partie [multipart/alternative] porte le n° 1.2. C’est un contenu de type [text/HTML] : le message du mail en HTML ;
- lignes 32-33 : la 1re partie de l’attachement [MESSAGE/RFC822] porte le n° 6.1. C’est un contenu de type [text/plain]. En fait selon le standard MIME, la numérotation des parties d’un attachement de mail [MESSAGE/RFC822] diffère de la règle décrite précédemment. Ainsi la 1re partie de l’attachement [MESSAGE/RFC822] ne porte pas le n° 6.1 mais un autre n° ;
Maintenant que nous savons comment repérer les différentes parties et sous-parties d’un mail, il nous reste à récupérer leur contenu.
Le code du script évolue de la façon suivante :
1 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 | function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// est-ce un message ?
if ($part->type === TYPEMESSAGE) {
// on ne va pas gérer les sous-parties de ce message (mail attaché)
// on affiche le corps du mail attaché
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
} else {
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// il n'y a pas de sous-parties - on affiche alors le corps du message
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
}
}
}
|
Commentaires
- ligne 46 : la fonction [imap_fetchbody] récupère le corps de la partie n° [$sectionNumber] du message. La numérotation des parties d’un message suit la règle expliquée précédemment ;
- ligne 1 : on commence avec la section “0”;
- ligne 41 : les sous-parties de cette section vont alors être numérotées “0.1”, “0.2”, alors qu’elles devraient être numérotées “1”, “2”…
- lignes 3-5 : on corrige cette anomalie;
- lignes 37-43 : si la partie courante a des sous-parties, alors on boucle sur chacune d’elles (lignes 38-43). Leur n° de section est [$sectionNumber.$i] ;
- lignes 44-47 : lorsqu’il n’y a plus de sous-parties, on affiche le corps de la partie courante avec la fonction [imap_fetchbody]. Dans notre exemple, il s’agit des parties [text/plain], [text/HTML] et les attachements ;
L’exécution de ce script donne les résultats suivants :
1 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 | ------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
ligne 1
ligne 2
ligne 3
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
UEsDBBQABgAIAAAAIQDfpNJsWgEAACAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAAC
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
…
AAAAAAAAAF0mAABkb2NQcm9wcy9jb3JlLnhtbFBLAQItABQABgAIAAAAIQCdxkmwcgEAAMcCAAAQ
AAAAAAAAAAAAAAAAAAgpAABkb2NQcm9wcy9hcHAueG1sUEsFBgAAAAALAAsAwQIAALArAAAAAA==
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nHWNvQoCMRCE+zzF1sLF2WSTSyAEPD0Lu4OAhdj5AxaC1/j6Rk4s5GSa
…
PDcxQUJGQ0JGQURGODYxM0NBNUJDODNFMDNDNjI1QkQwPgo8NzFBQkZDQkZBREY4NjEzQ0E1QkM4
M0UwM0M2MjVCRDA+IF0KL0RvY0NoZWNrc3VtIC9DMTRCN0Q5N0YwNUU1OTYxQzhDODg0NEI3NkNF
OEIwRQo+PgpzdGFydHhyZWYKMTIzMTQKJSVFT0YK
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
UEsDBBQAAAgAAAs9uU5exjIMJwAAACcAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2Fz
aXMub3BlbmRvY3VtZW50LnRleHRQSwMEFAAACAAACz25TgAAAAAAAAAAAAAAABwAAABDb25maWd1
…
AQIUABQACAgIAAs9uU42l0SORAQAABIRAAALAAAAAAAAAAAAAAAAAI8bAABjb250ZW50LnhtbFBL
AQIUABQACAgIAAs9uU4Uf52+LgEAACUEAAAVAAAAAAAAAAAAAAAAAAwgAABNRVRBLUlORi9tYW5p
ZmVzdC54bWxQSwUGAAAAABEAEQBlBAAAfSEAAAAA
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
iVBORw0KGgoAAAANSUhEUgAABiAAAAEMCAYAAABN1n5OAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAg
AElEQVR4nOy9e5TdV3Xn+Zm7aqprlBq1Rq1Wq7XU6opGrXaMMI6jAcfj9ihu4hAehkAghBASICF0
…
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA2Mb8f9Q5r2ohJn6/AAAAAElFTkSuQmCC
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
UmV0dXJuLVBhdGg6IGd1ZXN0QGxvY2FsaG9zdA0KUmVjZWl2ZWQ6IGZyb20gWzEyNy4wLjAuMV0g
KGxvY2FsaG9zdCBbMTI3LjAuMC4xXSkNCglieSBERVNLVE9QLTUyOEk1Q1Ugd2l0aCBFU01UUA0K
…
cjJvaEpuNi9BQUFBQUVsRlRrU3VRbUNDDQotLV89X3N3aWZ0XzE1NTg3NzA1MDJfYzRiODA4Yzk5
YzI3ZGVkMDQ1OTViZDExZjRiYWQxMWJfPV8tLQ0K
Fermeture de la connexion réussie.
|
Commentaires
- lignes 14-16 : le contenu du message texte codé en [quoted-printable] (ligne 13) ;
- ligne 20 : le contenu du message HTML codé en [quoted-printable] (ligne 19) ;
- lignes 24-28 : le contenu du fichier Word codé en [base64] (ligne 23) ;
- lignes 32-37 : le contenu du fichier PDF codé en [base64] (ligne 31) ;
- lignes 41-45 : le contenu du fichier OpenOffice codé en [base64] (ligne 40) ;
- lignes 50-55 ; le contenu du fichier image codé en [base64] (ligne 49) ;
- lignes 59-63 : le contenu du mail attaché codé en [base64] ligne 58) ;
Maintenant que :
- nous savons retrouver les textes des différentes parties d’un mail ;
- nous connaissons l’encodage de ces textes ;
nous pouvons sauvegarder ces textes dans des fichiers.
Le code évolue de la façon suivante :
1 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 | function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// est-ce un message ?
if ($part->type === TYPEMESSAGE) {
// on ne va pas gérer les sous-parties de ce message
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
} else {
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// il n'y a pas de sous-parties - on sauvegarde alors le corps du message
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
}
}
}
|
- lignes 33 et 45 : l’affichage du texte d’une partie [$imapResource, $msgNumber, $sectionNumber] du mail est désormais remplacée par sa sauvegarde dans un fichier ;
La fonction [savePart] est la suivante :
1 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 | // sauvegarde d'une partie de message
function savePart($imapResource, int $msgNumber, string $sectionNumber, array $infos, object $infosMail): void {
// dossier de sauvegarde
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
// si le dossier n'existe pas, on le crée
if (!file_exists($outputDir)) {
mkdir($outputDir);
}
// structure de la partie à sauvegarder
$struct = imap_bodystruct($imapResource, $msgNumber, $sectionNumber);
// type de document
$type = $struct->type;
// sous-type de document
$subtype = "";
if (isset($struct->subtype)) {
$subtype = strtolower($struct->subtype);
}
// on analyse le type de la partie
switch ($type) {
case TYPETEXT:
// cas du message texte : text/xxx
switch ($subtype) {
case plain:
saveText("$outputDir/message.txt", 0, imap_fetchBody($imapResource, $msgNumber, $sectionNumber), $infosMail, $struct);
break;
case HTML:
saveText("$outputDir/message.HTML", 1, imap_fetchBody($imapResource, $msgNumber, $sectionNumber), $infosMail, $struct);
break;
}
break;
default:
// autres cas - on ne s'intéresse qu'aux attachements
if (isset($struct->disposition)) {
$disposition = strtolower($struct->disposition);
if ($disposition === "attachment") {
// on a affaire à un attachement - on le sauvegarde
saveAttachment($imapResource, $msgNumber, $sectionNumber, $outputDir, $struct);
}
} else {
// on ne traitera pas cette partie
print "Partie [$sectionNumber] ignorée\n";
}
break;
}
}
|
- lignes 3-8 : création du dossier de sauvegarde. Celui-ci porte le n° du message dont on analyse les parties ;
- ligne 10 : la partie de message à sauvegarder est définie de façon unique par les trois paramètres [$imapResource, $msgNumber, $sectionNumber]. On demande la structure de cette partie avec la fonction [imap_bodystruct] ;
- ligne 12 : on récupère le type principal de la partie de message ;
- lignes 13-17 : on récupère son sous-type ;
- lignes 20-30 : on traite les deux types de contenu : [text/plain] (lignes 23-25) et [text/HTML] (lignes 26-28). Les autres types [text/xx] sont ignorés ;
- ligne 24 : le texte de la partie [text/plain] sera sauvegardé dans un fichier [message.txt] ;
- ligne 27 : le texte de la partie [text/HTML] sera sauvegardé dans un fichier [message.HTML] ;
- lignes 31-43 : on traite le cas des parties dont le type principal n’est pas [text] ;
- ligne 35 : on ne s’intéresse qu’aux attachements du message ;
- ligne 37 : ceux-ci sont sauvegardés dans un fichier à l’aide de la fonction [saveAttachment] ;
Si on résume le code précédent :
- sauvegarde les parties [text/plain] et [text/HTML] à l’aide de la fonction [saveText]. Ces parties représentent le contenu du mail ;
- sauvegarde les différentes pièces attachées à l’aide de la fonction [saveAttachment] ;
La fonction [saveText] est la suivante :
1 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 | // sauvegarde du texte [$text] du message
function saveText(string $fileName, int $type, string $text, object $infosMail, object $struct) {
// préparation du texte à sauvegarder
// $text est encodé - on le décode
switch ($struct->encoding) {
case ENCBASE64:
$text = base64_decode($text);
break;
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
}
// entêtes du message
// from
$from = "From: ";
foreach ($infosMail->from as $expéditeur) {
$from .= $expéditeur->mailbox . "@" . $expéditeur->host . ";";
}
// to
$to = "To: ";
foreach ($infosMail->to as $destinataire) {
$to .= $destinataire->mailbox . "@" . $destinataire->host . ";";
}
// subject
$subject = "Subject: " . $infosMail->subject;
// création du texte à enregistrer
switch ($type) {
case 0:
// text/plain
$contents = "$from\n$to\n$subject\n\n$text";
break;
case 1:
// text/HTML
$contents = "$from<br/>\n$to<br/>\n$subject<br/>\n<br/>\n$text";
break;
}
// création du fichier
print "sauvegarde d'un message dans [$fileName]\n";
// création du fichier
if (! file_put_contents($fileName, $contents)) {
// échec de la création du fichier
print "Impossible de créer le fichier [$fileName]\n";
}
}
|
Commentaires
- ligne 1 :
- [$fileName] est le nom du fichier dans lequel sera sauvegardé le texte [$text] ;
- [$type] : vaut 0 pour un fichier texte, 1 pour un fichier HTML ;
- [$text] : est le texte à sauvegarder. Mais il faut d’abord le décoder car il est codé ;
- [$infosMail] : contient des informations générales sur le mail. Nous allons utiliser les champs [from, to, subject] ;
- [$struct] : est la structure qui décrit la partie de mail qu’on est en train de sauvegarder. Cela va nous permettre de connaître le type d’encodage du texte à sauvegarder ;
- lignes 4-12 : on décode le texte à sauvegarder ;
- lignes 13-25 : on récupère les informations [from, to, subject] du mail ;
- lignes 27-36 : selon le type, 0 ou 1, du texte à sauvegarder, on construit un texte plain (ligne 30) ou un texte HTML (ligne 34) ;
- ligne 40 : le texte total est enregistré dans le fichier [$fileName] ;
Les attachements sont eux sauvegardés avec la fonction [saveAttachment] suivante :
1 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 | // sauvegarde d'un attachement
function saveAttachment($imapResource, int $msgNumber, string $sectionNumber, string $outputDir, object $struct) {
// on analyse la structure de l'attachement
// on cherche à récupérer le nom du fichier dans lequel sauvegarder l'attachement
// ce nom se trouve dans les [dparameters] de la structure
if (isset($struct->dparameters)) {
// on récupère les [dparameters]
$dparameters = $struct->dparameters;
$fileName = "";
// on parcourt le tableau des [dparameters]
foreach ($dparameters as $dparameter) {
// chaque [dparameter] est un objet avec deux attributs [attribute, value]
$attribute = strtolower($dparameter->attribute);
// l'attribut [filename] correspond au nom du fichier à créer
// dans ce cas le nom du fichier est dans [$dparameter->value]
if ($attribute === "filename") {
$fileName = $dparameter->value;
break;
}
}
// si on n'a pas trouvé de nom de fichier, on regarde dans l'attribut [parameters] de la structure
if ($fileName === "" && isset($struct->parameters)) {
// on récupère les [parameters]
$parameters = $struct->parameters;
foreach ($parameters as $parameter) {
// chaque paramètre est un dictionnaire à deux clés [attribute, value]
$attribute = strtolower($parameter->attribute);
// si l'attribut est [name], alors le [value] est le nom du fichier
if ($attribute === "name") {
$fileName = $parameter->value;
// le nom du fichier peut être encodé
// par exemple =?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep
// on récupère l'encodage avec une expression régulière
$champs = [];
$match = preg_match("/=\?(.+?)\?/", $fileName, $champs);
// si concordance, alors on décode le nom du fichier
if ($match) {
$fileName = iconv_mime_decode($fileName, 0, $champs[1]);
}
break;
}
}
}
}
// si on a trouvé un nom de fichier, alors on sauvegarde l'attachement
if ($fileName !== "") {
// sauvegarde de l'attachement
$fileName = "$outputDir/$fileName";
print "sauvegarde de l'attachement dans [$fileName]\n";
// création fichier
if ($file = fopen($fileName, "w")) {
// on récupère le texte encodé de l'attachement
$text = imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
// l'attachement est encodé - on le décode
switch ($struct->encoding) {
// base 64
case ENCBASE64:
$text = base64_decode($text);
break;
// quoted printable
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
default:
// on ignore les autres cas
break;
}
// écriture du texte dans le fichier
fputs($file, $text);
// fermeture fichier
fclose($file);
} else {
// échec de la création du fichier
print "L'attachement n'a pu être sauvegardé dans [$fileName]\n";
}
}
}
|
Commentaires
- ligne 2 : la fonction [saveAttachment] admet les paramètres
suivants :
- [$imapResource, int $msgNumber, string $sectionNumber] définissent de façon unique la partie IMAP à sauvegarder ;
- [string $outputDir] est le dossier de sauvegarde ;
- [object $struct] décrit la structure de la partie de message à sauvegarder ;
- lignes 6-44 : on cherche le nom du fichier associé à l’attachement. On utilisera ce même nom de fichier pour le sauvegarder. Le nom du fichier de l’attachement se trouve dans le tableau [$struct→dparameters] ou le tableau [$struct→parameters], voire les deux ;
- lignes 30-40 : si le nom du fichier contient des caractères non codés sur 7 bits, alors il a été encodé en [quoted-printable]. Dans ce cas, dans [$struct→dparameters], l’attribut s’appelle [fileName*] au lieu de [fileName]. Cela signifie qu’il n’a pas satisfait à la condition de la ligne 16. Le nom du fichier est alors cherché dans le tableau [$struct→parameters] ;
- ligne 32 : un exemple de nom de fichier encodé. Il a la forme suivante =?codage_original?codage_actuel?nom_encodé. Ainsi le nom [=?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep] signifie que le nom du fichier était en UTF-8 et qu’il est actuellement en [quoted-printable] (Q) ;
- ligne 38 : le nom du fichier est décodé avec la fonction
[iconv_mime_decode] qui admet ici trois paramètres :
- la chaîne à décoder ;
- laisser à 0 par défaut ;
- le jeu de caractères à utiliser pour représenter la chaîne décodée. Ce paramètre est présent dans la chaîne à décoder. On l’obtient avec une expression régulière aux lignes 34-35 ;
- lignes 45-75 : on sauvegarde l’attachement dans un fichier portant le nom qu’on a trouvé ;
Pour tester le script [imap-02.php], on envoie d’abord un mail à [guest@localhost] avec la configuration suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {
"mail to localhost via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "test-localhost",
"message": "ligne 1\nligne 2\nligne 3",
"tls": "FALSE",
"attachments": [
"/attachments/Hello from SwiftMailer.html",
"/attachments/Hello from SwiftMailer.pdf",
"/attachments/Hello from SwiftMailer.odt",
"/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
"/attachments/test-localhost.eml"
]
}
}
|
Il y a donc cinq attachements.
On lit le mail envoyé avec [imap-02.php] et la configuration suivante :
1 2 3 4 5 6 7 8 9 10 | {
"{localhost:110/pop3}": {
"imap-server": "localhost",
"imap-port": "110",
"user": "guest@localhost",
"password": "guest",
"pop3": "TRUE",
"output-dir": "output/localhost-pop3"
}
}
|
Les résultats console sont les suivants :
1 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 | ------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
sauvegarde d'un message dans [output/localhost-pop3/message-1/message.txt]
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
sauvegarde d'un message dans [output/localhost-pop3/message-1/message.HTML]
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.html]
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.pdf]
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.odt]
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/test-localhost.eml]
Fermeture de la connexion réussie.
Done.
|
On retrouve les fichiers sauvegardés dans le dossier [output/localhost-pop3/message-N] :
Client POP3 / IMAP avec la bibliothèque [php-mime-mail-parser]¶
Dans le script précédent [imap-02.php], nous avons pu sauvegarder :
- les contenus [text/plain] et [text/HTML] du mail ;
- les attachements du mail ;
Pour un attachement de type [message/rfc822] nous avons également sauvegardé le contenu de l’attachement. Or ce type d’attachement est lui-même un mail qui, à son tour, a des contenus [text/plain] et [text/HTML] ainsi que des attachements. On peut alors être dans la situation suivante :
- un [mail 1] dont la structure est analogue celle d’un attachement de type [message/rfc822] ;
- un [mail 2] attaché au mail 1 ;
- un [mail 3] attaché au mail 2 ;
- etc…
Le script [imap-02.php] sauvegarde le contenu de [mail 1] (textes et attachements). Il sauvegarde [mail 2] comme document attaché mais s’arrête là. Il n’essaie pas d’analyser [mail 2] pour en extraire les textes et attachements. On pourrait penser qu’il suffit d’appliquer à [mail 2] ce qui a été fait pour [mail 1]. Un appel récursif à la méthode qui traitait [mail 1] pourrait alors suffire pour avoir le contenu de tous les mails imbriqués les uns dans les autres. Malheureusement les parties de [mail 2] sont numérotées avec une logique différente de celle utilisée pour [mail 1], ce qui empêche d’utiliser le même algorithme dans les deux cas à moins d’utiliser une logique assez complexe pour calculer les n°s des parties d’un mail, quelque soit la position de celui-ci dans l’ensemble des mails imbriqués.
Le script [imap-02.php] était déjà complexe. Pour éviter de le complexifier encore davantage pour gérer les contenus des mails imbriqués, nous allons utiliser la bibliothèque [php-mime-mail-parser] disponible sur Github (mai 2019) à l’URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] et écrite par Vincent Dauce.
Installation de la bibliothèque [php-mime-mail-parser]¶
La page de présentation de la bibliothèque indique comment l’installer sous Windows :
Il y a deux étapes pour l’OS Windows :
- télécharger une DLL ;
- modifier le fichier [php.ini] qui configure PHP ;
LA DLL de la bibliothèque [mailparse] est disponible à l’URL [http://pecl.php.net/package/mailparse] (mai 2019) ;
- en [2], choisir la version la plus récente et stable de la bibliothèque ;
- en [3] choisir la version du PHP que vous utilisez (dans ce document c’est PHP 7.2) ;
- en [4], choisir la version de votre OS Windows (ici c’est un Windows 64 bits). On prend la version [Thread Safe] ;
Pour connaître la version de PHP téléchargée avec Laragon, ouvrez un [Terminal] à partir de la fenêtre de Laragon et tapez la commande suivante :
1 2 3 4 5 | C:\myprograms\laragon-lite\www
λ php -v
PHP 7.2.11 (cli) (built: Oct 10 2018 02:04:07) ( ZTS MSVC15 (Visual C++ 2017) x64 )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
|
La version de PHP 7.2.11 est donnée ligne 3. La même ligne donne la version de Windows utilisée pour la compilation (32 ou 74 bits).
Une fois la DLL obtenue, il faut la copier dans le dossier [<laragon>/bin/php/<version-php>/ext] [5] :
Ceci fait, il faut activer cette extension dans le fichier [php.ini] qui configure PHP (cf paragraphe lien) :
Il est probable que la ligne [7] n’existera pas et qu’il faudra l’ajouter par vous-même.
Une fois l’extension activée on peut vérifier sa validité en tapant la commande suivante dans un terminal de Laragon :
1 2 3 4 5 6 | C:\myprograms\laragon-lite\www
λ php --ini
Configuration File (php.ini) Path: C:\windows
Loaded Configuration File: C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed: (none)
|
La commande [php –-ini] charge le fichier de configuration de la ligne 4. Elle va alors charger les DLL de toutes les extensions activées dans [php.ini]. Si l’une d’elles est erronée, cela sera signalé. Ainsi la validité de la DLL ajoutée [php_mailparse.dll] sera-t-elle vérifiée. Elle peut être déclarée incorrecte pour diverses raisons dont les plus fréquentes sont les suivantes :
- vous avez téléchargé une DLL ne correspondant pas à la version de PHP utilisée ;
- vous avez téléchargé une DLL 32 bits alors que vous avez un PHP 64 bits ou vice-versa ;
Une fois l’extension activée et vérifiée, on peut passer à l’installation de la bibliothèque [php-mime-mail-parser] :
La commande [8] est à taper dans un terminal Laragon (cf paragraphe lien) :
- en [1], vérifiez que vous êtes positionné sur le dossier [<laragon>/www] ;
- en [2], la commande d’installation de la bibliothèque [php-mime-mail-parser] ;
- en [3], rien n’a été installé ici car la bibliothèque [php-mime-mail-parser] était déjà installée ;
L’installation de bibliothèque [php-mime-mail-parser] se fait dans le dossier [<laragon>/www/vendor] :
- en [2-3], les sources de la bibliothèque [php-mime-mail-parser] ;
Maintenant que l’environnement de travail a été installé, on peut passer à l’écriture du script [imap-03.php].
Le script [imap-03.php]¶
Le script [imap-03.php] utilise le même fichier de configuration [config-imap-01.json] que les précédents scripts :
1 2 3 4 5 6 7 8 9 10 | {
"{localhost:110/pop3}": {
"imap-server": "localhost",
"imap-port": "110",
"user": "guest@localhost",
"password": "guest",
"pop3": "TRUE",
"output-dir": "output/localhost-pop3"
}
}
|
Le script [imap-03.php] est le suivant :
1 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 | <?php
// client IMAP (Internet Message Access Protocol) permettant de lire des mails
// écrit avec la bibliothèque [php-mime-mail-parser]
// disponible à l'URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] (mai 2019)
//
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// gestion des erreurs
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
// les paramètres de lecture du courrier
const CONFIG_FILE_NAME = "config-imap-01.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration " . CONFIG_FILE_NAME . " n'existe pas";
exit;
}
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// lecture des boîtes à lettres
foreach ($mailboxes as $name => $infos) {
// suivi
print "------------Lecture de la boîte à lettres [$name]\n";
// lecture de la boîte à lettres
readmailbox($name, $infos);
}
// fin
exit;
|
Commentaires
- lignes 18-23 : le contenu du fichier de configuration est mis dans le dictionnaire [$mailboxes] ;
- lignes 26-31 : chaque boîte à lettres est lue par la fonction [readmailbox] (ligne 30). Cette fonction lit en fait les messages non lus de la boîte à lettres. Une boîte à lettres correspond à l’adresse mail d’un utilisateur donné ;
La fonction [readmailbox] est la suivante :
1 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 | function readmailbox(string $name, array $infos): void {
// on se connecte
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
if (!$imapResource) {
// échec
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
exit;
}
// Connexion établie
print "Connexion établie avec le serveur [$name].\n";
// total des messages dans la boîte à lettres
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// messages non lus dans la boîte aux lettres courante
if ($nbmsg > 0) {
print "Récupération de la liste des messages non lus de la boîte à lettres [$name]\n";
$msgNumbers = imap_search($imapResource, 'UNSEEN');
if ($msgNumbers === FALSE) {
print "Il n'y a pas de nouveaux messages dans la boîte à lettres [$name]\n";
} else {
// on parcourt la liste des messages non lus
foreach ($msgNumbers as $msgNumber) {
print "---message n° [$msgNumber]\n";
// on récupère le corps du message n° $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// si le protocole est POP3, on supprime le message après l'avoir récupéré
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// on marque le message comme "à supprimer"
imap_delete($imapResource, $msgNumber);
}
}
// fin de la lecture des messages non lus
if ($pop3 !== NULL) {
// on supprime les messages marqués comme "à supprimer"
imap_expunge($imapResource);
}
}
}
// fermeture de la connexion
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// échec
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// réussite
print "Fermeture de la connexion réussie.\n";
}
}
|
Commentaires
Le code de la fonction [readmailbox] est le même que dans les scripts précédents.
La fonction [getMailBody] (ligne 25) qui analyse le corps d’un message (contenu + attachements) est la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // analyse du corps du message
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// on récupère le texte entier du message
$text = imap_fetchbody($imapResource, $msgNumber, "");
if ($text === FALSE) {
print "Le corps du message [$msgNumber] n'a pu être récupéré";
return;
}
// on crée un parseur qui va analyser le texte du message
$parser = (new PhpMimeMailParser\Parser())->setText($text);
// on récupère les différentes parties du message
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
getParts($parser, $msgNumber, $outputDir);
}
|
Commentaires
- ligne 2 : la fonction [getMailBody] accepte trois paramètres :
- [$imapResource] : la ressource IMAP à laquelle on est connecté ;
- [$msgNumber] : le n° du message (dans la boîte à lettres) à exploiter ;
- [$infos] : informations diverses sur la boîte à lettres exploitée ;
- ligne 4 : on récupère la totalité du message n° [$msgNumber] ;
- lignes 5-8 : cas où le contenu du message n’a pu être récupéré ;
- ligne 10 : on commence à utiliser la bibliothèque [php-mime-mail-parser]. L’objet [$parser] va être chargé d’analyser le texte du message ;
- ligne 12 : [$outputDir] va être le dossier dans lequel vont être sauvegardés les contenus texte et les attachements du message n° [$msgNumber] ;
- ligne 13 : on demande à la fonction [getParts] de trouver les différentes parties (contenus texte et attachements) du message n° [$msgNumber] et de les sauvegarder dans le dossier [$outputDir] ;
La fonction [getParts] est la suivante :
1 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 | // récupération des différentes parties d'un message
function getParts(PhpMimeMailParser\Parser $parser, int $msgNumber, string $outputDir): void {
// on crée le dossier de sauvegarde du message si besoin est
if (!file_exists($outputDir)) {
if (!mkdir($outputDir)) {
print "Le dossier [$outputDir] n'a pu être créé\n";
return;
}
}
// on récupère les entêtes du message
$arrayHeaders = $parser->getHeaders();
// on sauvegarde les messages texte
$parts = $parser->getInlineParts("text");
for ($i = 1; $i <= count($parts); $i++) {
print "-- Sauvegarde d'un message de type [text/plain]\n";
saveMessage($parts[$i - 1], 0, $arrayHeaders, "$outputDir/message_$i.txt");
}
// on sauvegarde les messages html
$parts = $parser->getInlineParts("html");
for ($i = 1; $i <= count($parts); $i++) {
print "-- Sauvegarde d'un message de type [text/html]\n";
saveMessage($parts[$i - 1], 1, $arrayHeaders, "$outputDir/message_$i.html");
}
// on récupère les attachements du message
$attachments = $parser->getAttachments();
// n° de l'attachement
$iAttachment = 0;
// on parcourt la liste des attachements
foreach ($attachments as $attachment) {
// type d'attachement
$fileType = $attachment->getContentType();
print "-- Sauvegarde d'un attachement de type [$fileType] dans le fichier [$outputDir/{$attachment->getFilename()}]\n";
// on sauvegarde l'attachement
try {
$attachment->save($outputDir, PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX);
} catch (Exception $e) {
print "L'attachement n'a pu être sauvegardé : " . $e->getMessage() . "\n";
}
// cas particulier du type message/rfc822
if ($fileType === "message/rfc822") {
// l'attachement est lui-même un message - on va le parser lui aussi
// on change de répertoire de sauvegarde
$iAttachment++;
$outputDir = $outputDir . "/rfc822-$iAttachment";
// on change le contenu à parser
$parser->setText($attachment->getContent());
// on analyse le message de façon récursive
getParts($parser, $msgNumber, $outputDir);
}
}
}
|
Commentaires
- ligne 2 : la fonction [getParts] admet trois paramètres :
- un parseur [$parser] à qui on a transmis le texte total du message à analyser ;
- [$msgNumber] est le n° du message en cours d’analyse ;
- [$outputDir] est le dossier dans lequel les contenus et attachements du message doivent être sauvegardés ;
- lignes 4-9 : création du dossier [$outputDir] ;
- ligne 11 : on récupère les entêtes du message en cours d’analyse (from, to, subject…) ;
- ligne 13 : on récupère les parties du mail ayant le type [text/plain]. On récupère un tableau ;
- lignes 14-17 : on sauvegarde tous les éléments du tableau récupéré, en donnant à chacun un nom de fichier différent ;
- ligne 19 : on récupère les parties du mail ayant le type [text/html]. On récupère un tableau ;
- lignes 20-23 : on sauvegarde tous les éléments du tableau récupéré, en donnant à chacun un nom de fichier différent ;
- ligne 25 : on récupère la liste des attachements du message analysé ;
- ligne 29 : on parcourt cette liste ;
- ligne 24 : on récupère le type de l’attachement (attribut Content-Type) ;
- lignes 34-38 : sauvegarde de l’attachement dans le dossier [$outputDir]. Le second paramètre [PhpMimeMailParserParser::ATTACHMENT_DUPLICATE_SUFFIX] est une stratégie de nommage des pièces attachées. Si [$attachment→getFilename()] vaut X et que le fichier X existe déjà, alors la bibliothèque [php-mime-mail-parser] essaie les noms [X_1], [X_2], etc. jusqu’à trouver un nom de fichier qui n’existe pas ;
- ligne 40 : on regarde si la pièce attachée est un mail ;
- lignes 41-48 : si c’est le cas, alors ce mail est analysé à son tour pour en extraire contenus et attachements ;
- ligne 44 : si [$outputDir] vaut X et que parmi les attachements du message analysé il y a deux mails, alors le 1er sera sauvegardé dans le dossier [$outputDir/rfc822-1] et le second dans le dossier [$outputDir/rfc822-2] ;
- ligne 46 : le contenu du mail attaché devient le nouveau texte à parser ;
- ligne 48 : on appelle la fonction [getParts] de façon récursive pour analyser le nouveau texte ;
La fonction [saveMessage] sauve les contenus texte du message à analyser :
1 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 | // sauvegarde d'un message texte
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
// contenu à sauvegarder
$contents = "";
// ajout des entêtes
switch ($type) {
case 0:
// text/plain
foreach ($arrayHeaders as $key => $value) {
$contents .= "$key: $value\n";
}
$contents .= "\n";
break;
case 1:
// text/HTML
foreach ($arrayHeaders as $key => $value) {
$contents .= "$key: $value<br/>\n";
}
$contents .= "<br/>\n";
}
// ajout du texte du message
$contents .= $text;
// sauvegarde du tout
if (!file_put_contents($filename, $contents)) {
// échec
print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
} else {
// réussite
print "Le message a été sauvegardé dans le fichier [$filename]\n";
}
}
|
Commentaires
- la fonction [saveMessage] admet les paramètres suivants :
- [$text] : le texte à sauvegarder ;
- [$type] : le type du texte (0 : text/plain, 1 : text/HTML) ;
- [$arrayHeaders] : les entêtes du message analysé ;
- [$filename] : le nom du fichier dans lequel doit être sauvegardé [$text] ;
- ligne 4 : [$contents] va représenter la totalité du texte à sauvegarder ;
- lignes 6-20 : on sauvegardera d’abord tous les entêtes du message (from, to, subject…) ;
- lignes 16-19 : dans le cas d’un texte HTML, on termine chaque ligne par la balise <br/> pour que chaque entête apparaisse seule sur sa ligne dans un navigateur ;
- ligne 22 : on ajoute aux entêtes le texte du message à sauvegarder ;
- lignes 24-30 : l’ensemble est sauvegardé dans le fichier [$filename] ;
L’utilisation de la bibliothèque [php-mime-mail-parser] facilite considérablement l’écriture du script de lecture de mails.
Le script [smtp-02.php] est utilisé pour envoyer un mail à l’utilisateur [guest@localhost] avec la configuration suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {
"mail to localhost via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "test-localhost",
"message": "ligne 1\nligne 2\nligne 3",
"tls": "FALSE",
"attachments": [
"/attachments/Hello from SwiftMailer.html",
"/attachments/Hello from SwiftMailer.pdf",
"/attachments/Hello from SwiftMailer.odt",
"/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
"/attachments/test-localhost-2.eml"
]
}
}
|
- lignes 11-15 : il y a cinq attachements ;
- ligne 15 : [test-localhost-2.eml] est un mail structuré de la
façon suivante :
- [test-localhost-2.eml] contient 4 attachements (les mêmes qu’aux lignes 11-14) et un mail attaché ;
- le mail attaché à [test-localhost-2.eml] contient 4 attachements (les mêmes qu’aux lignes 11-14) ;
Le script [imap-03.php] est utilisé pour lire la boîte à lettres de l’utilisateur [guest@localhost] avec la configuration suivante :
1 2 3 4 5 6 7 8 9 10 | {
"{localhost:110/pop3}": {
"imap-server": "localhost",
"imap-port": "110",
"user": "guest@localhost",
"password": "guest",
"pop3": "TRUE",
"output-dir": "output/localhost-pop3"
}
}
|
Après exécution, l’arborescence du dossier [output/localhost-pop3] est devenue la suivante :
- en [1], les 5 attachements du mail reçu par [guest@localhost] ;
- en [2], les 5 attachements du mail [test-localhost-2.eml] de [1] ;
- en [3], les 4 attachements du mails [test-localhost.eml] de [2] ;
Les affichages console sont les suivants :
1 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 | ------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
---message n° [1]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.html]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-- Sauvegarde d'un attachement de type [message/rfc822] dans le fichier [output/localhost-pop3/message-1/test-localhost-2.eml]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.html]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-- Sauvegarde d'un attachement de type [message/rfc822] dans le fichier [output/localhost-pop3/message-1/rfc822-1/test-localhost.eml]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.html]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
Fermeture de la connexion réussie.
|
Si on visualise [message_1.HTML] de [3] dans un navigateur, on obtient la chose suivante :
Services web¶
Note : par service web, on entend ici tout application web délivrant des données brutes consommées par un client, un script console dans les exemples qui vont suivre. On ne s’intéresse pas à une technologie particulière, REST (REpresentational State Transfer) ou SOAP (Simple Object Access Protocol) par exemple, qui délivrent des données plus ou moins brutes dans un format bien défini. REST délivre du jSON alors que pour SOAP c’est du XML. Chacune de ces technologies décrit précisément la façon dont le client doit interroger le serveur et la forme que doit prendre la réponse de celui-ci. Dans ce cours, on sera beaucoup plus souple quant à la nature de la requête client et celle de la réponse du serveur. Cependant, les scripts écrits et les outils utilisés sont proches de ceux de la technologie REST.
Introduction¶
Les programmes PHP pouvant être exécutés par un serveur WEB, un tel programme devient un programme serveur pouvant servir plusieurs clients. Du point de vue du client, appeler un service web revient à demander l’URL de ce service. Le client peut être écrit avec n’importe quel langage, notamment en PHP. Dans ce dernier cas, on utilise alors les fonctions réseau que nous venons de voir. Il nous faut par ailleurs savoir « converser » avec un service web, c’est-à-dire comprendre le protocole http de communication entre un serveur WEB et ses clients. C’était le but du paragraphe lien.
Le client web décrit au paragraphe lien nous a permis de découvrir une partie du protocole HTTP.
Dans leur version la plus simple, les échanges client / serveur sont les suivants :
- le client ouvre une connexion avec le port 80 du serveur web ;
- il fait une requête concernant un document ;
- le serveur web envoie le document demandé et ferme la connexion ;
- le client ferme à son tour la connexion ;
Le document peut être de nature diverse : un texte au format HTML, une image, une vidéo… Ce peut être un document existant (document statique) ou bien un document généré à la volée par un script (document dynamique). Dans ce dernier cas, on parle de programmation web. Le script de génération dynamique de documents peut être écrit dans divers langages : PHP, Python, Perl, Java, Ruby, C#, VB.net…
Dans la suite, nous allons utiliser des scripts PHP pour générer dynamiquement des documents texte.
- en [1], le client ouvre une connexion avec le serveur, demande un script PHP, envoie ou non des paramètres à destination de ce script ;
- en [2], le serveur web fait exécuter le script PHP par l’interpréteur PHP. Le script génère un document qui est envoyé au client [3] ;
- le serveur clôt la connexion. Le client en fait autant ;
Le serveur web peut traiter plusieurs clients à la fois.
Avec le paquetage logiciel [Laragon], le serveur web est un serveur Apache, un serveur open source de l’Apache Foundation (http://www.apache.org/). Dans les applications qui suivent, [Laragon] doit être lancé :
Cela lance le serveur web Apache ainsi que le SGBD MySQL.
Les scripts exécutés par le serveur web seront écrits avec l’outil Netbeans. Nous avons écrit jusqu’à maintenant des scripts PHP exécutés dans un contexte console :
L’utilisateur utilise la console pour demander l’exécution d’un script PHP et en recevoir les résultats.
Dans les applications client /serveur qui vont suivre :
- le script du client est exécuté dans un contexte console ;
- le script du serveur est exécuté dans un contexte web ;
Le script PHP du serveur ne peut pas être n’importe où dans le système de fichiers. En effet, le serveur web cherche dans des endroits précisés par configuration, les documents statiques et dynamiques qu’on lui demande. La configuration par défaut de Laragon fait que les documents sont cherchés dans le dossier <Laragon>/www où <Laragon> est le dossier d’installation de Laragon. Ainsi si un client web demande un document D avec l’URL [http://localhost/D], le serveur web lui servira le document D de chemin [<Laragon>/www/D].
Dans les exemples qui suivent, nous mettrons les scripts serveur dans le dossier [www/php7/scripts-web]. Si un script serveur s’appelle S.php, il sera demandé au serveur web avec l’URL [http://localhost/php7/scripts-web/S.php]. Le document [<Laragon>/www/php7/scripts-web/S.php] lui sera alors servi.
- en [1], le dossier [<laragon>/www] ;
- en [2], le dossier [php7/scripts-web] ;
Pour créer des scripts serveur avec Netbeans, nous procéderons de la façon suivante :
- en [1-2], nous créons un nouveau projet
- en [3-4], nous prenons la catégorie [PHP] et le projet [PHP Application]
- en [5], le nom du projet ;
- en [6], le dossier du projet dans le système de fichiers. Notez que celui-ci est dans le dossier [<laragon>/www] où il doit être ;
- en [7-8], acceptez les valeurs par défaut proposées ;
- en [9-10], acceptez les valeurs par défaut proposées. En [10], notez que l’URL des scripts que nous placerons dans ce projet commencera par le chemin [http://localhost/php7/scripts-web/] ;
- en [11], des frameworks web écrits en PHP vous sont proposés. Ces frameworks sont indispensables dès que l’application web prend un peu d’ampleur ;
- en [12], on peut ajouter des bibliothèques PHP à l’aide de
l’outil [Composer]. Nous avons utilisé cet outil à deux reprises
dans une fenêtre [Terminal] de Laragon :
- pour installer la bibliothèque [SwiftMailer] qui permet d’envoyer des mails ;
- pour installer la bibliothèque [php-mime-mail-parser] qui permet de lire des mails ;
- en [13], une fois l’assistant de création du projet validé, celui-ci apparaît en [13] dans l’onglet des projets ;
Ecriture d’une page statique¶
Note **: Pour la suite, il faut que **[Laragon] soit lancé.
Nous allons montrer comment créer une page statique HTML (HyperText Markup Language) à l’aide de Netbeans :
- en [1-5], nous créons un dossier nommé [01] ;
- en [6-12], nous créons un fichier HTML [exemple-01.html] ;
Le fichier [exemple-01.html] est généré prérempli de la façon suivante (mai 2019) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div>TODO write content</div>
</body>
</html>
|
Faisons évoluer son contenu de la façon suivante :
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html>
<html>
<head>
<title>PHP7 par l'exemple</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div><b>Ceci est un exemple de page statique</b></div>
</body>
</html>
|
Nous avons changé le titre de la page (ligne 4) ainsi que son contenu (ligne 9).
Maintenant faisons afficher cette page HTML par le serveur Apache de Laragon :
- en [1-2], nous faisons afficher la page par le serveur Apache de Laragon ;
- en [3], l’URL de la page affichée ;
- en [4], le titre que nous avons modifié ;
- en [5], le contenu que nous avons modifié ;
La page affichée est une page statique : on peut la charger autant de fois que l’on veut dans le navigateur (F5), c’est toujours le même contenu qui est affiché.
La plupart des navigateurs donnent accès aux données échangées entre le client et le serveur, celles qui ont été décrites au paragraphe lien. Avec le navigateur Firefox (mai 2019), il faut faire F12 pour avoir accès à ces données :
Comme indiqué en [1], rechargeons la page (F5) :
- en [2], le document chargé par le navigateur : nous le sélectionnons ;
- en [5], le document à analyser est sélectionné ;
- en [3-4], nous demandons à voir les échanges client / serveur ;
- en [6], ces échanges ;
- en [7], on sélectionne l’onglet des entêtes ;
- en [8], l’URL demandée par le navigateur ;
- en [9], la commande envoyée au serveur est [GET http://localhost/php7/scripts-web/01/exemple-01.html HTTP/1.1] ;
- en [10], les entêtes HTTP envoyés ensuite par le navigateur (le client) ;
- en [11], les entêtes HTTP de la réponse du serveur ;
- en [12-14], la réponse du serveur envoyée après les entêtes HTTP ;
- en [14], on voit que le navigateur client a reçu la page HTML que nous avons construite. Il a ensuite interprété ce code pour afficher la chose suivante :
Création d’une page dynamique en PHP¶
Nous écrivons maintenant une page dynamique en PHP :
- en [1-8], nous créons une page [exemple-01.php] ;
Le fichier [exemple-01.php] est généré prérempli de la façon suivante (mai 2019) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<?php
// put your code here
?>
</body>
</html>
|
Nous faisons évoluer le code ci-dessus de la façon suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Exemple de page dynamique</title>
</head>
<body>
<?php
// time : nb de millisecondes entre le moment présent et le 01/01/1970
// format affichage date-heure
// d : jour sur 2 chiffres
// m : mois sur 2 chiffres
// y : année sur 2 chiffres
// H : heure 0,23
// I : minutes
// s: secondes
print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
?>
</body>
</html>
|
Commentaires
- ligne 5 : nous avons changé le titre de la page ;
- ligne 17 : écrit la date et l’heure du moment présent ;
Basiquement, le script PHP ci-dessus écrit l’heure courante sur la console. Cependant, lorsqu’il est exécuté par un serveur web, le flux de sortie de l’instruction [print] qui est habituellement associé à la console d’exécution du script est redirigé ici vers la connexion qui lie le serveur à son client. Donc, dans un contexte web, le script ci-dessus envoie l’heure courante sous forme de texte au client, ici un navigateur.
Exécutons le script [exemple-01.php] :
- en [3], l’URL demandée au serveur web Apache ;
- en [4], le titre de la page que nous avons changé ;
- en [5], le contenu généré par l’instruction [print] ;
Nous avons ici une page dynamique car si on recharge plusieurs fois la page dans le navigateur (F5), son contenu change (l’heure change).
Le navigateur a reçu un flux HTML. Pour connaître celui-ci, il faut faire apparaître le code source de la page dans le navigateur :
- pour avoir le menu [1], cliquer droit sur la page dans le navigateur ;
- en [2], l’URL de la page [exemple-01.php] mais préfixée par [view-source :] [3] ;
- en [4], le contenu HTML que le navigateur a affiché ;
Il faut donc se rappeler qu’un script PHP destiné à être exécuté par un serveur web doit produire un flux HTML.
Regardons (F12) maintenant les entêtes HTTP envoyés par le serveur au navigateur client :
- en [3], un entête HTTP qui n’était pas présent lorsqu’on a demandé la page statique. Cet entête montre que la réponse du serveur a été générée par un script PHP ;
Nous avons vu que la réponse (le flux HTML ici) du serveur pouvait être générée par un script PHP. Le script peut également générer les entêtes HTTP et à peu près tous les éléments de la réponse du serveur.
Rudiments du langage HTML¶
Ce chapitre ne va pas s’appesantir sur la programmation WEB en PHP. Une application web MVC est développée au paragraphe lien. Ce chapitre s’intéresse plutôt aux services web : des pages PHP qui délivrent, via un serveur web, des données à destination d’autres clients PHP. Néanmoins il nous a semblé utile de donner quelques rudiments de HTML au lecteur.
Un navigateur web peut afficher divers documents, le plus courant étant le document HTML (HyperText Markup Language). Celui-ci est un texte formaté avec des balises de la forme <balise>texte</balise>. Ainsi le texte <b>important</b> affichera le texte important en gras. Il existe des balises seules, telles que la balise <hr/> qui affiche une ligne horizontale. Nous ne passerons pas en revue les balises que l’on peut trouver dans un texte HTML. Il existe de nombreux logiciels WYSIWYG permettant de construire une page WEB sans écrire une ligne de code HTML. Ces outils génèrent automatiquement le code HTML d’une mise en page faite à l’aide de la souris et de contrôles prédéfinis. On peut ainsi insérer (avec la souris) dans la page un tableau puis consulter le code HTML généré par le logiciel pour découvrir les balises à utiliser pour définir un tableau dans une page WEB. Ce n’est pas plus compliqué que cela. Par ailleurs, la connaissance du langage HTML est indispensable puisque les applications web dynamiques doivent générer elles-mêmes le code HTML à envoyer aux clients WEB. Ce code est généré par programme et il faut bien sûr savoir ce qu’il faut générer pour que le client ait la page web qu’il désire.
Pour résumer, il n’est nul besoin de connaître la totalité du langage HTML pour démarrer la programmation web. Cependant cette connaissance est nécessaire et peut être acquise au travers de l’utilisation de logiciels WYSIWYG de construction de pages WEB tels que DreamWeaver et des dizaines d’autres. Une autre façon de découvrir les subtilités du langage HTML est de parcourir le web et d’afficher le code source des pages qui présentent des caractéristiques intéressantes et encore inconnues pour vous.
Considérons l’exemple suivant qui présente quelques éléments qu’on peut trouver dans un document WEB tels que :
- un tableau ;
- une image ;
- un lien.
Un document HTML a la forme générale suivante :
L’ensemble du document est encadré par les balises <html>…</html>. Il est formé de deux parties :
- <head>…</head> : c’est la partie non affichable du document. Elle donne des renseignements au navigateur qui va afficher le document. On y trouve souvent la balise <title>…</title> qui fixe le texte qui sera affiché dans la barre de titre du navigateur. On peut y trouver d’autres balises notamment des balises définissant les mots clés du document, mot clés utilisés ensuite par les moteurs de recherche. On peut trouver également dans cette partie des scripts, écrits le plus souvent en javascript ou vbscript et qui seront exécutés par le navigateur.
- <body attributs>…</body> : c’est la partie qui sera affichée par le navigateur. Les balises HTML contenues dans cette partie indiquent au navigateur la forme visuelle « souhaitée » pour le document. Chaque navigateur va interpréter ces balises à sa façon. Deux navigateurs peuvent alors visualiser différemment un même document web. C’est généralement l’un des casse-têtes des concepteurs web.
Le code HTML de notre document exemple est le suivant :
1 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 | <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Quelques balises HTML</title>
</head>
<body style="background-image: url(images/standard.jpg)">
<h1 style="text-align: left">Quelques balises HTML</h1>
<hr />
<table border="1">
<thead>
<tr>
<th>Colonne 1</th>
<th>Colonne 2</th>
<th>Colonne 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>cellule(1,1)</td>
<td style="text-align: center;">cellule(1,2)</td>
<td>cellule(1,3)</td>
</tr>
<tr>
<td>cellule(2,1)</td>
<td>cellule(2,2)</td>
<td>cellule(2,3</td>
</tr>
</tbody>
</table>
<br/><br/>
<table border="0">
<tr>
<td>Une image</td>
<td>
<img border="0" src="images/cerisier.jpg"/></td>
</tr>
<tr>
<td>Le site de Polytech'Angers</td>
<td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
</tr>
</table>
</body>
</html>
|
Elément | balises et exemples HTML |
---|---|
titre du document | <title>Quelques balises HTML</title> (ligne 5) le texte [Quelques balises HTML] apparaîtra dans la barre de titre du navigateur qui affichera le document |
barre horizontale | <hr /> : affiche un trait horizontal (ligne 10) |
tableau | <table attributs>….</table> : pour définir le tableau (lignes 12, 32) <thead>…</thead> : pour définir les entêtes des colonnes (lignes 13, 19) <tbody>…</tbody> : pour définir le contenu du tableau (ligne 20, 31) <tr attributs>…</tr> : pour définir une ligne (lignes 21, 25) <td attributs>…</td> : pour définir une cellule (ligne 22) exemples : <table border= »1 »>…</table> : l’attribut border définit l’épaisseur de la bordure du tableau <td style= »text-align: center; »>cellule(1,2)</td> (ligne 23) : définit une cellule dont le contenu sera cellule(1,2). Ce contenu sera centré horizontalement (text-align :center). |
image | <img border= »0 » src= »images/cerisier.jpg »/> (ligne 38) : définit une image sans bordure (border=0 ») dont le fichier source est [images/cerisier.jpg] sur le serveur web (src= »images/cerisier.jpg »). Ce lien se trouve sur un document web obtenu avec l’URL http://lo calhost/php7/scripts-web/01/balises.html. Aussi, le navigateur demandera-t-il l’URL http://localhos t/php7/scripts-web/01/images/cerisier.jpg pour avoir l’image référencée ici. |
lien | <a href= »http:/ /www.polytech-angers.fr/fr/index.html »>ici</a> (ligne 42) : fait que le texte ici sert de lien vers l’URL http://www.polytech-angers.fr/fr/index.html. |
fond de page | <body style= »background-image: url(images/standard.jpg) »> (ligne 8) : indique que l’image qui doit servir de fond de page se trouve à l’URL [images/standard.jpg] du serveur WEB. Dans le contexte de notre exemple, le navigateur demandera l’URL http://loc alhost/php7/scripts-web/01/images/standard.jpg pour obtenir cette image de fond. |
On voit dans ce simple exemple que pour construire l’intéralité du document, le navigateur doit faire trois requêtes au serveur :
- http://localhost/php7/scripts-web/01/images/balises.html pour avoir le source HTML du document
- http://localhost/php7/scripts-web/01/images/cerisier.jpg pour avoir l’image cerisier.jpg
- http://localhost/php7/scripts-web/01/images/standard.jpg pour obtenir l’image de fond standard.jpg
C’est ce que montrent les échanges réseau entre le client et le serveur (F12 dans le navigateur) :
- en [3-5], on voit les trois requêtes faites par le navigateur ;
Rendre dynamique une page statique¶
Montrons comment nous pouvons dynamiser la page HTML [exemple-01.html]. Copions le contenu
Nous avons recopié le contenu de [exemple-01.html] dans le fichier [page-01.php]. Si nous exécutons [2] ce script web, nous obtenons la chose suivante dans le navigateur :
- en [3], l’URL demandée ;
- en [4], le titre de la page ;
- en [5], le contenu de la page ;
Si on fait afficher le code reçu par le navigateur, on trouve ceci :
- en [7], on a le code HTML placé dans le script [exemple-01.php]
L’interpréteur PHP a interprété le script [page-01.php] et a produit le même flux HTML que la page statique [exemple-01.html]. Dans le script [page-01.php], il n’y avait pas de PHP, uniquement du HTML. On apprend ainsi une chose : lorsque l’interpréteur PHP trouve du HTML dans un script PHP, il n’y touche pas et l’envoie tel quel au client.
Maintenant mettons quelques instructions PHP dans le script [page-01.php] pour que l’interpréteur PHP ait quelque chose à faire :
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html>
<html>
<head>
<title><?php print $page->title ?></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div><b><?php print $page->contents ?></b></div>
</body>
</html>
|
Lignes 4 et 9 on a mis du code PHP pour générer dynamiquement le titre et le contenu de la page. On fait ici l’hypothèse que la variable [$page] est un objet qui a les données à afficher.
Si nous exécutons ce nouveau code, on a le résultat suivant dans le navigateur :
- en [1], l’URL demandée ;
- en [2], le titre de la page n’a pu être affiché parce que la variable [$page] n’était pas définie ;
- en [3], pareil pour le contenu ;
Maintenant, écrivons le script web [exemple-02.php] suivant :
Le script [exemple-02.php] sera le suivant :
1 2 3 4 5 6 7 8 | <?php
// on définit les éléments de la page à afficher
$page=new \stdclass();
$page->title="Un nouveau titre";
$page->contents="Un nouveau contenu généré dynamiquement";
// on fait afficher [page-01]
require_once "page-01.php";
|
- lignes 4-6 : on définit l’objet [$page] ;
- ligne 8 : on inclut le script [page-01.php]. Le code de ce script
va être interprété à son tour :
- la variable [$page] est maintenant définie et l’interpréteur PHP va l’utiliser ;
- le code HTML de [page-01.php] va être envoyé tel quel au client ;
- les résultats des opérations PHP [print] vont être inclus dans le flux texte envoyé au client ;
Maintenant si nous exécutons le script web [exemple-02.php], nous obtenons la chose suivante dans le navigateur :
Si nous visualisons le contenu texte reçu par le navigateur :
- les codes PHP qui étaient en [2] et [3] ont été remplacés par les résultats des deux commandes [print] ;
De cet exemple, on retiendra deux choses :
- les pages HTML destinées au navigateur peuvent être isolées dans des scripts PHP ne contenant que ce code HTML et quelques partie dynamiques générées par du code PHP. Il doit y avoir le moins de PHP possible dans ces pages ;
- toute la logique qui génère les données dynamiques incluses dans les pages HTML doit être isolée dans des scripts PHP purs, ne comportant aucun code de présentation des pages (HTML, CSS, Javascript…) ;
Cela autorise une séparation des tâches :
- la tâche de réalisation des pages web à afficher (HTML, CSS, Javascript…) ;
- la tâche de la logique de l’application web que l’on construit. Cette logique pourra être implémentée avec une architecture trois couches, exactement comme nous l’avons fait avec les scripts console ;
Par la suite, nous allons construire des scripts web particuliers ;
- ils n’enverront que des données au client et aucune décoration (HTML, CSS, Javascript). Ce seront donc des serveurs de données plutôt que des page web ;
- les clients de ces scripts web seront des scripts console qui se chargeront de récupérer les données envoyées par le serveur et d’en faire quelque chose ;
Application client/ serveur de date/heure¶
Nous nous plaçons maintenant dans la configuration suivante :
Nous allons écrire :
- un script web [1] qui envoie à son client la date et l’heure du moment présent ;
- un script console [2] qui sera le client du script web : il va récupérer la date et heure envoyées par le script web et les afficher sur la console ;
en [1], le script web [date-time-server.php] ;
en [2], le script console [date-time-client] client du script web ;
Le script serveur
Nous avons déjà écrit un script web générant la date et l’heure du moment présent au paragraphe lien. C’était le script [exemple-01.php] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Exemple de page dynamique</title>
</head>
<body>
<?php
// time : nb de millisecondes depuis 01/01/1970
// format affichage date-heure
// d: jour sur 2 chiffres
// m: mois sur 2 chiffres
// y : année sur 2 chiffres
// H : heure 0,23
// i : minutes
// s: secondes
print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
?>
</body>
</html>
|
Nous avons dit que nous allions écrire des serveurs de données : des données brutes sans habillage HTML. Le script serveur [date-time-server.php] sera alors le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php
// on fixe l'entête HTP [Content-Type]
header('Content-Type: text/plain; charset=UTF-8');
//
// on envoie date et heure
// time : nb de millisecondes depuis 01/01/1970
// format affichage date-heure
// d: jour sur 2 chiffres
// m: mois sur 2 chiffres
// y : année sur 2 chiffres
// H : heure 0,23
// i : minutes
// s: secondes
print date("d/m/y H:i:s", time());
|
- ligne 4 : on fixe l’entête HTTP [Content-Type] qui dit au client la nature du document qu’il va recevoir. Jusqu’à maintenant, le [Content-Type] était : [Content-Type: text/html; charset=UTF-8]. Ici, nous indiquons au client que le document est du texte sans habillage HTML. Ce n’est pas important pour notre client console qui ne cherchera pas à exploiter cet entête. C’est plus important pour les navigateurs clients qui eux exploitent cet entête ;
Exécutons ce script serveur :
Si nous examinons dans le navigateur la réponse du serveur (F12), nous voyons en [5] l’entête HTTP que le script serveur a fixé et en [8], le document texte reçu ;
Le script client¶
Au paragraphe lien nous avons développé plusieurs clients HTTP. Nous pourrions les utiliser pour récupérer le document texte envoyé par le script serveur [date-time-server.php]. Nous n’allons pas le faire. Comme nous l’avons fait pour les protocole SMTP et IMAP, nous allons utiliser une bibliothèque tierce, à savoir le composant [HttpClient] du framework Symfony [https://symfony.com/doc/master/components/http_client.html].
Comme pour les deux précédentes bibliothèques, on utilise l’outil [Composer] pour installer le composant [HttpClient] de Symfony. Dans une fenêtre [Terminal] de Laragon (cf paragraphe lien), on tape la commande suivante :
- en [3], vérifiez que vous êtes positionné sur le dossier [<laragon>/www/] où <laragon> est le dossier d’installation de Laragon ;
- en [4], la commande [composer] qui installe la bibliothèque [HttpClient] de Symfony ;
- en [5], rien n’est installé car la bibliothèque [HttpClient] avait déjà été installée sur ce poste ;
- en [6-7], de nouveaux dossiers apparaissent dans [<laragon>/www/vendor/symfony] ;
A la place de [5], vous devriez avoir quelque chose comme suit :
1 2 3 4 5 6 7 8 9 10 11 12 13 | C:\myprograms\laragon-lite\www
? composer require symfony/http-client
Using version ^4.3 for symfony/http-client
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
- Installing symfony/polyfill-php73 (v1.11.0): Downloading (100%)
- Installing symfony/http-client-contracts (v1.1.1): Downloading (100%)
- Installing psr/log (1.1.0): Loading from cache
- Installing symfony/http-client (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files
|
Assurez-vous que le dossier [<laragon>/www/vendor] fait partie de la branche [Include Path] de votre projet (cf paragraphe lien) :
Ceci fait, nous pouvons écrire le script console [date-time-client.php] :
Le script console [date-time-client.php] exploitera le fichier jSON [config-date-time-client.json] suivant :
1 2 3 | {
"url": "http://localhost/php7/scripts-web/02/date-time-server.php"
}
|
- ligne 2 : l’URL du script serveur ;
Le script client [date-time-client.php] sera le suivant :
1 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 | <?php
// client du service date / heure
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-date-time-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on fait la requête
$response = $httpClient->request('GET', $config['url']);
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on récupère le corps de la réponse
$content = $response->getContent();
// on l'affiche
print "---Réponse du serveur : [$content]\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
exit;
}
|
Commentaires
- ligne 10 : comme nous l’avions fait pour les bibliothèques précédentes, nous chargeons le fichier [<laragon>/www/vendor/autoload.php] ;
- ligne 11 : nous déclarons la classe [HttpClient] que nous allons utiliser ;
- lignes 13-24 : on récupère la configuration du script dans le dictionnaire [$config] ;
- ligne 27 : on crée un objet de type [HttpClient] ;
- ligne 31 : on demande l’URL du script serveur à l’aide d’une commande GET : [GET URL HTTTP/1.1]. Cette opération est asynchrone. L’exécution se poursuit en ligne 33 sans attendre que la réponse soit obenue ;
- ligne 33 : on demande le statut de la réponse. Ce statut se trouve dans le 1er entête HTTP renvoyé par le serveur. Ainsi si cet entête est [HTTP/1.1 200 OK], le statut de la réponse est 200. Cette opération est bloquante : on n’en revient que lorsque le client a reçu toute la réponse du serveur ;
- ligne 37 : on demande les entêtes HTTP de la réponse ;
- ligne 42 : on demande le document renvoyé par le serveur : on sait que ce document est ici un texte.
- lignes 45-49 : en cas d’erreur, on affiche le message d’erreur ;
Lorsqu’on exécute le script client (il faut que Laragon soit lancé pour que le script serveur puisse être atteint), on obtient le résultat suivant sur la console :
1 2 3 4 5 6 7 8 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Thu, 30 May 2019 14:42:03 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
content-length: 17
content-type: text/plain; charset=UTF-8
---Réponse du serveur : [30/05/19 14:42:03]
|
On récupère bien la date et l’heure du moment présent en ligne 8.
On peut avoir la curiosité de savoir ce qu’à envoyé le script client au serveur. Pour cela nous allons utiliser notre serveur TCP générique (cf paragraphe lien) :
- en [1], le dossier des utilitaires ;
- en [2], le serveur TCP est lancé sur le port 100 ;
- en [3], attente d’une commande tapée au clavier ;
Nous modifions le fichier de configuration du script [date-time-client.php] :
1 2 3 | {
"url": "http://localhost:100/php7/scripts-web/02/date-time-server.php"
}
|
Cette fois-ci, le client contacte le serveur [localhost] sur le port 100. C’est donc notre serveur TCP générique qui va être sollicité. Lorsque nous exécutons le script console [date-time-client.php], la console du serveur TCP générique évolue de la façon suivante :
- en [3], la commande HTTP GET construite par le script client ;
- en [4], la signature du script console ;
- en [5], la réponse du serveur au script client. On notera que ce
n’est pas une réponse HTTP valide :
- il devrait y avoir des entêtes HTTP ;
- puis une ligne vide ;
- puis le document texte envoyé au client ;
- en [6], on ferme la communication avec le script client pour que celui-ci détecte qu’il a eu la totalité de la réponse ;
Côté script client, on a l’affichage console suivant :
en [7], ce qu’a reçu le client Symfony ;
Le script serveur – version 2
De base, les fonctions PHP pour écrire un script web ne sont pas orientées objet. Côté serveur, on est alors amenés à mélanger classes et fonctions PHP classiques. Pour avoir une écriture plus homogène, nous allons utiliser la bibliothèque [HttpFoundation] du framework Symfony. Elle a encapsulé toutes les fonctions PHP classiques pour un service web dans un système de classes et interfaces. La documentation de la bibliothèque est disponible à l’URL [https://symfony.com/doc/current/components/http_foundation.html] (mai 2019).
Pour installer la bibliothèque, nous procédons de la façon suivante dans un terminal Laragon (cf paragraphe lien) :
- [2-3] : assurez-vous d’être positionné dans le dossier [<laragon>/www] ;
- [4] : la commande [composer] qui va installer la bibliothèque [HttpFoundation] ;
- [5] : sur cet exemple, la bibliothèque était déjà installée ;
A la première installation, vous devriez avoir des logs console ressemblant à ceci :
1 2 3 4 5 6 7 8 9 10 11 | C:\myprograms\laragon-lite\www
? composer require symfony/http-foundation
Using version ^4.3 for symfony/http-foundation
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing symfony/mime (v4.3.0): Downloading (100%)
- Installing symfony/http-foundation (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files
|
La seconde version du serveur web [date-time-server-2.php] est la suivante :
1 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 | <?php
// usage des bibliothèques de Symfony
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Response;
// on fixe l'entête Content-Type
$response=new Response();
$response->headers->set("content-type","text/plain");
$response->setCharset("utf-8");
// on fixe le contenu de la réponse
//
// on envoie date et heure
// time : nb de millisecondes depuis 01/01/1970
// format affichage date-heure
// d: jour sur 2 chiffres
// m: mois sur 2 chiffres
// y : année sur 2 chiffres
// H : heure 0,23
// i : minutes
// s: secondes
$response->setContent(date("d/m/y H:i:s", time()));
// on envoie la réponse
$response->send();
|
Commentaires
ligne 7 : la classe [Response] de la bibliothèque [HttpFoundation] de Symfony gère la totalité de la réponse aux clients du service web ;
ligne 10 : création d’une instance de la classe [Response] ;
ligne 11 : on indique que la réponse est de type [text/plain] ;
ligne 12 : la réponse est du texte UTF-8 ;
ligne 25 : on fixe le document de la réponse, ce qu’a demandé le client ;
ligne 28 : on envoie la réponse au client ;
Le script client – version 2
Le script client ne change pas. On change seulement son fichier de configuration [config-date-time-client.json] :
1 2 3 | {
"url": "http://localhost/php7/scripts-web/02/date-time-server-2.php"
}
|
Les résultats sont les mêmes que dans la version 1.
Un serveur de données jSON¶
La réponse d’un script web peut être composée de plusieurs données qu’on peut rassembler dans des tableaux et des objets. Le script peut alors envoyer ces divers éléments au sein d’une chaîne jSON que le client décodera.
Le script serveur¶
Le script [json-server.php] utilise la classe [Personne] suivante :
1 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 | <?php
namespace Modèles;
class Personne implements \JsonSerializable {
// attributs
private $nom;
private $prénom;
private $âge;
// conversion d'un tableau associatif vers un objet [Personne]
public function setFromArray(array $assoc): Personne {
// on initialise l'objet courant avec le tableau associatif
foreach ($assoc as $attribute => $value) {
$this->$attribute = $value;
}
// résultat
return $this;
}
// getters et setters
public function getNom() {
return $this->nom;
}
public function getPrénom() {
return $this->prénom;
}
public function setNom($nom) {
$this->nom = $nom;
return $this;
}
public function setPrénom($prénom) {
$this->prénom = $prénom;
return $this;
}
public function getÂge() {
return $this->âge;
}
public function setÂge($âge) {
$this->âge = $âge;
return $this;
}
// toString
public function __toString(): string {
return "Personne [$this->prénom, $this->nom, $this->âge]";
}
// implémente l'interface JsonSerializable
public function jsonSerialize(): array {
// on rend un tableau associatif avec pour clés les attributs de l'objet
// ce tableau pourra ensuite être encodé en jSON
return get_object_vars($this);
}
// conversion d'un jSON vers un objet [Personne]
public static function jsonUnserialize(string $json): Personne {
// on crée une personne à partir de la chaîne jSON
return (new Personne())->setFromArray(json_decode($json, true));
}
}
|
Commentaires
- ligne 5 : la classe implémente l’interface PHP [JsonSerializable]. Cela lui impose d’implémenter la méthode [jsonSerialize] des lignes 55-59. La méthode doit rendre un tableau associatif qui devra être sérialisé en jSON. Lorsqu’on utilise l’expression [json_encode($personne)], la fonction [json_encode] regarde si la classe [Personne] implémente l’interface [JsonSerializable]. Si oui, l’expression devient [json_encode($personne→serialize())] ;
- lignes 12-19 : la classe n’a pas de constructeur mais un initialiseur. La classe [Personne] peut être alors instanciée par l’expression [(new Personne())→setFromArray($array)]. On peut avoir divers types d’initialiseurs alors qu’on ne peut avoir qu’un constructeur. Ces initialiseurs permettent divers modes d’instanciation du type [(new Personne())→initialiseuri(…)] ;
- lignes 62-65 : la fonction statique [jsonUnserialize] permet de créer un objet [Personne] à partir de sa chaîne jSON ;
Le script [json-server.php] sera le suivant :
1 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 | <?php
// dépendances
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
// on fixe l'entête Content-Type et la bibliothèque de caractères utilisée
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// on crée un objet Personne
$personne = (new Personne())->setFromArray([
"nom" => "de la Hûche",
"prénom" => "jean-paul",
"âge" => 27]);
// un tableau associatif
$assoc = ["attr1" => "value1",
"attr2" => [
"prenom" => "Jean-Paul",
"nom" => "de la Hûche"
]
];
// le contenu de la réponse est du jSON
$response->setContent(json_encode([$personne, $assoc]));
// envoi de la réponse
$response->send();
|
Commentaires
- lignes 4-5 : on importe la classe [Personne] ;
- ligne 11 : on indique que le document sera de type [application/json]. A la réception de cet entête, les navigateurs afficheront une mise en forme de la chaîne jSON plutôt que d’afficher du texte brut ;
- ligne 12 : la chaîne jSON contiendra des caractères UTF-8 ;
- lignes 15-18 : on crée un objet [Personne] ;
- lignes 20-25 : on crée un tableau associatif à deux niveaux ;
- ligne 27 : on envoie au client la chaîne jSON d’un tableau :
- l’élément [$personne] sera sérialisée en jSON grâce à sa méthode [jsonSerialize] ;
- l’élemént [$assoc] sera nativement sérialisé en jSON ;
Si on exécute ce script serveur (Laragon doit être lancé), on obtient la réponse suivante dans un navigateur :
Commentaires
en [2], la réponse jSON mise en forme ;
en [4], la réponse jSON brute. On remarquera l’encodage des caractères accentués ;
en [6], c’est le type de contenu [application/json] envoyé par le serveur qui a conduit le navigateur à faire cette mise en forme ;
Le client
Le client [json-client.php] est configuré par le fichier jSON [config-json-client.json] suivant :
1 2 3 | {
"url": "http://localhost/php7/scripts-web/03/json-server.php"
}
|
Le script [json-client.php] est le suivant :
1 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
// client d'un service jSON
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
// la configuration du client
const CONFIG_FILE_NAME = "config-json-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on fait la requête
$response = $httpClient->request('GET', $config['url']);
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on récupère le corps jSON de la réponse
list($personne, $assoc) = json_decode($response->getContent(), true);
// on instancie une personne à partir du tableau de ses attributs
$personne = (new Personne())->setFromArray($personne);
// on affiche la réponse du serveur
print "---Réponse du serveur\n";
print "$personne\n";
print "tableau=" . json_encode($assoc, JSON_UNESCAPED_UNICODE) . "\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
Commentaires
- lignes 12-13 : importation de la classe [Personne] ;
- ligne 30 : création du client HTTP ;
- ligne 44 : on décode la chaîne jSON envoyée par le serveur. On sait que ce qui a été encodé est un tableau à deux éléments comprenant deux tableaux associatifs ;
- ligne 46 : on crée un objet [Personne] pour l’afficher ensuite en ligne 49 ;
- ligne 50 : on affiche le 2e tableau associatif. L’instruction [print] ne sait pas afficher des tableaux. Aussi transforme-t-on celui-ci en chaîne jSON. Pour avoir correctement les caractères accentués, il faut mettre le second paramètre [JSON_UNESCAPED_UNICODE]. On a vu qu’effectivement les caractères accentués sont encodés dans la chaîne jSON ;
L’exécution du script client donne les résultats suivants :
1 2 3 4 5 6 7 8 9 10 11 12 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 09:56:29 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 143
connection: close
content-type: application/json
---Réponse du serveur
Personne [jean-paul, de la Hûche, 27]
tableau={"attr1":"value1","attr2":{"prenom":"Jean-Paul","nom":"de la Hûche"}}
|
Lignes 11 et 12, on a récupéré correctement les caractères accentués.
Récupération des variables d’environnement du service web¶
Un script serveur s’exécute dans un environnement web qu’il peut connaître. Cet environnement est stocké dans le dictionnaire $_SERVER, une variable globale de PHP. Si nous utilisons la bibliothèque [HttpFoundation], cet environnement sera trouvé dans le champ [Request→server] où [Request] est la requête HTTP traitée par le script web.
Le script serveur¶
Nous écrivons une application serveur qui envoie à ses clients son environnement d’exécution.
Le script web [env-server.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
// on récupère la requête
$request = Request::createFromGlobals();
// on élabore la réponse
$response = new Response();
// le contenu de la réponse est du json utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// on fixe le contenu jSON de la réponse
$response->setContent(json_encode($request->server->all()));
// envoi de la réponse
$response->send();
|
- ligne 9 : on récupère l’objet de type [Request] qui encapsule la totalité des informations disponibles sur la requête HTTP reçue par le script web ainsi que sur l’environnement d’exécution de celui-ci ;
- lignes 13-14 : on va envoyer du texte brut avec caractères UTF-8 au client ;
- ligne 16 : l’information envoyée au client sera une chaîne de caractères obtenue par sérialisation jSON de l’objet [$request→server→all()] : [$request→server] représente l’environnement d’exécution du script web. C’est un objet de type [ServerBag], une sorte de dictionnaire. [$request→server→all()] est lui un vrai dictionnaire, celui du contenu du [ServerBag] ;
- ligne 18 : on envoie l’information ;
Si on exécute ce script à partir de Netbeans, le navigateur affiche la page suivante :
en [2], les différentes clés du dictionnaire de l’environnement ;
en [3], les valeurs de ces clés ;
Le script client
Le script client [env-client.php] est configuré par le fichier jSON [config-env-client.json] suivant :
1 2 3 | {
"url": "http://localhost/php7/scripts-web/04/env-server.php"
}
|
Le script client [env-client.php] est le suivant :
1 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 | <?php
// environnement d'un script serveur
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-env-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on fait la requête au serveur
$response = $httpClient->request('GET', $config['url']);
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on affiche la réponse du serveur
print "---Réponse du serveur\n";
$env = json_decode($response->getContent());
foreach ($env as $key => $value) {
print "[$key]=>$value\n";
}
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
Commentaires
- ligne 42 : on désérialise la réponse jSON du serveur. On obtient un tableau associatif ;
- lignes 43-45 : on affiche toutes les valeurs de ce tableau associatif ;
On obtient le résultat console suivant :
1 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 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 17:35:50 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 1505
connection: close
content-type: application/json
---Réponse du serveur
[HTTP_HOST]=>localhost
[HTTP_USER_AGENT]=>Symfony HttpClient/Curl
[HTTP_ACCEPT_ENCODING]=>deflate, gzip
[PATH]=>C:\Program Files (x86)\Mail Enable\BIN;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files (x86)\Mail Enable\BIN64;C:\Users\serge\AppData\Local\Microsoft\WindowsApps;;C:\myprograms\Microsoft VS Code\bin
[SystemRoot]=>C:\windows
[COMSPEC]=>C:\windows\system32\cmd.exe
[PATHEXT]=>.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
[WINDIR]=>C:\windows
[SERVER_SIGNATURE]=>
[SERVER_SOFTWARE]=>Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
[SERVER_NAME]=>localhost
[SERVER_ADDR]=>::1
[SERVER_PORT]=>80
[REMOTE_ADDR]=>::1
[DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[REQUEST_SCHEME]=>http
[CONTEXT_PREFIX]=>
[CONTEXT_DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[SERVER_ADMIN]=>admin@example.com
[SCRIPT_FILENAME]=>C:/myprograms/laragon-lite/www/php7/scripts-web/04/env-server.php
[REMOTE_PORT]=>63744
[GATEWAY_INTERFACE]=>CGI/1.1
[SERVER_PROTOCOL]=>HTTP/1.1
[REQUEST_METHOD]=>GET
[QUERY_STRING]=>
[REQUEST_URI]=>/php7/scripts-web/04/env-server.php
[SCRIPT_NAME]=>/php7/scripts-web/04/env-server.php
[PHP_SELF]=>/php7/scripts-web/04/env-server.php
[REQUEST_TIME_FLOAT]=>1559496950.644
[REQUEST_TIME]=>1559496950
|
Voici la signification de certaines des variables (pour windows. Sous Linux, elles seraient différentes) :
HTTP_HOST
|
la valeur xxx de l’entête HTTP [Host: xxx] envoyée par le client |
---|---|
HTTP_USER_AGENT
|
la valeur xxx de l’entête HTTP [User_Agent: xxx] envoyée par le client |
HTTP_ACCEPT_ENCODING
|
la valeur xxx de l’entête HTTP [Accept-Encoding: xxx] envoyée par le client |
PATH
|
le chemin des exécutables sur la machine sur laquelle s’exécute le script serveur |
COMSPEC
|
le chemin de l’interpréteur de commandes DOS |
PATHEXT
|
les extensions des fichiers exécutables |
WINDIR
|
le dossier d’installation de Windows |
SERVER_SIGNATURE
|
la signature du serveur web. Ici rien. |
SERVER_SOFTWARE
|
le type du serveur web |
SERVER_NAME
|
le nom Internet de la machine du serveur web |
SERVER_PORT
|
le port d’écoute du serveur web |
SERVER_ADDR
|
l’adresse IP de la machine du serveur web, ici 127:0:0:1 |
REMOTE_ADDR
|
l’adresse IP du client. Ici le client était sur la même machine que le serveur. |
REMOTE_PORT
|
le port de communication du client |
DOCUMENT_ROOT
|
la racine de l’arborescence des documents servis par le serveur web |
REQUEST_SCHEME
|
le protocole TCP de la requête d’URL http://localhost/php7/… |
SERVER_ADMIN
|
l’adresse électronique de l’administrateur du serveur web |
SCRIPT_FILENAME
|
le chemin complet du script serveur |
REMOTE_PORT
|
le port à partir duquel le client a fait sa demende |
SERVER_PROTOCOL
|
la version du protocole HTTP utilisée par le serveur web |
REQUEST_METHOD
|
l’ordre HTTP utilisé par le client. Il y en a quatre : GET, POST, PUT, DELETE |
QUERY_STRING
|
les paramètres envoyés avec un ordre GET /url?paramètres |
REQUEST_URI
|
l’URL demandée par le client. Si le navigateur demande l’URL http://machine[:port]/uri, on aura REQUEST_URI=uri |
SCRIPT_NAME
|
$_SERVER[“S CRIPT_FILENAME”]=$_SERVER[“DOCUME NT_ROOT”].$_SERVER[“SCRIPT_NAME”] |
Récupération par le serveur de paramètres envoyés par un client¶
Introduction¶
Dans le protocole HTTP, un client a deux méthodes pour passer des paramètres au serveur WEB :
- il demande l’URL du service sous la forme
GETurl?param1=val1¶m2=val2¶m3=val3… HTTP/1.0
où les valeurs vali doivent au préalable subir un encodage afin que certains caractères réservés soient remplacés par leur valeur hexadécimale ;
- il demande l’URL du service sous la forme
POSTurl HTTP/1.0
puis parmi les entêtes HTTP envoyés au serveur place l’entête suivant :
Content-length=N
La suite des entêtes envoyés par le client se terminent par une ligne vide. Il peut alors envoyer ses données sous la forme
val1¶m2=val2¶m3=val3…
où les valeurs vali doivent, comme pour la méthode GET, être préalablement encodées. Le nombre de caractères envoyés au serveur doit être N où N est la valeur déclarée dans l’entête
Content-length=N
Le script PHP du service web qui récupère les paramètres parami précédents envoyés par le client obtient leurs valeurs dans le tableau :
- $_GET[« parami »] pour une commande GET ;
- $_POST[« parami »] pour une commande POST ;
ceci pour les fonctions de base de PHP. Si on utilise la bibliothèque [HttpFoundation], ces paramètres seront trouvés dans :
- [Request]->query->get(‘parami’) pour une commande GET ;
- [Request]->request->get(‘parami’) pour une commande POST ;
où [Request] représente la totalité des informations sur la requête reçue par le script web ;
Le client GET – version 1¶
Les scripts clients sont configurés par le fichier jSON [config-parameters-client.json] suivant :
1 2 3 4 | {
"url-get": "http://localhost/php7/scripts-web/05/parameters-server.php",
"url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
|
- ligne 1 : l’URL du script web cible des clients GET ;
- ligne 2 : l’URL du script web cible du client POST ;
Les clients GET envoient trois paramètres [nom, prenom, age] au serveur. Le client [parameters-get-client.php] est le suivant :
1 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 | <?php
// client GET d'un serveur web
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-parameters-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on prépare les paramètres
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// on encode les informations
$parameters = "prenom=" . urlencode($prenom) .
"&nom=" . urlencode($nom) .
"&age=$age”;
// on fait la requête
$response = $httpClient->request('GET', $config['url-get'] . "?$parameters");
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on affiche la réponse du serveur
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
Commentaires
- lignes 33-35 : encodage des paramètres envoyés au serveur. Les paramètres [$prenom, $nom] qui peuvent avoir des caractères UTF-8 sont encodés avec la fonction [urlencode]. Tous les caractères non alphanumériques (au sens des expressions relationnelles) sont remplacés par %xx où xx est la valeur hexadécimale du caractère. Les espaces sont eux remplacés par le signe + ;
- ligne 37 : l’URL demandée est $URL?$parameters où $parameters est de la forme *nom=val1&prenom=val2&age=val3 *;
- ligne 48 : le client se contentera d’afficher a réponse du client ;
On peut avoir la curiosité de voir ce que reçoit le serveur lors d’une requête GET paramétrée. Pour cela, nous lançons notre serveur générique [RawTcpServer] sur le port 100 de la machine locale à partir d’un terminal Laragon (cf paragraphe lien) :
Vérifiez qu’en [4], vous êtes bien dans le dossier des utilitaires.
Nous modifions le fichier jSON [parameters-get-client.json] qui configure les clients GET et POST :
1 2 3 4 | {
"url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
"url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
|
- ligne 2 : nous avons changé le port du serveur web. Ce sera donc [RawTcpServer] qui sera contacté ;
Nous exécutons le client. Dans la fenêtre de [RawTcpServer] nous obtenons les informations suivantes :
en [1], la commande GET paramétrée envoyée par le client. On voit clairement l’encodage de certains caractères ;
Le serveur GET / POST
Le script serveur [parameters-server.php] est le suivant :
1 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 | <?php
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
// on récupère la requête
$request = Request::createFromGlobals();
// on récupère les paramètres de la requête
$getParameters = $request->query->all();
$bodyParameters = $request->request->all();
// on élabore la réponse
$response = new Response();
// le contenu de la réponse est du texte utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// contenu de la réponse - un tableau encodé en jSON
$response->setContent(json_encode([
"method" => $request->getMethod(),
"uri" => $request->getRequestUri(),
"getParameters" => $getParameters,
"bodyParameters" => $bodyParameters
], JSON_UNESCAPED_UNICODE));
// envoi de la réponse
$response->send();
|
Commentaires
- ligne 9 : création de l’objet [Request] du script web. Cet objet encapsule la totalité des informations que le script web a reçues du client ;
- ligne 11 : l’objet [Request→query] est de type [ParameterBag] et rassemble les paramètres de l’éventuelle opération GET d’un client. L’expression [Request→query→get(«X»)] permet d’avoir le paramètre nommé X dans les paramètres du GET [nom=val1&prenom=val2&age=val3]. L’expression [Request→query→all()] permet d’avoir le dictionnaire des paramètres du GET ;
- ligne 12 : l’objet [Request→request] est de type [ParameterBag] et rassemble les paramètres envoyés comme document du client au serveur. On dit également de ces paramètres qu’ils sont uploadés parce qu’ils appartiennent à un document que le client envoie au serveur. L’expression [Request→request→get(«X»)] permet d’avoir le paramètre nommé X dans les paramètres uploadés [nom=val1&prenom=val2&age=val3]. L’expression [Request→request→all()] permet d’avoir le dictionnaire des paramètres uploadés ;
- lignes 17-18 : on indique au client qu’on va lui envoyer du jSON codé en UTF-8 ;
- lignes 20-25 : le serveur renvoie au client tous les paramètres qu’il a reçus ainsi, le type d’opération [GET / POST / …] faite par le client, et l’URI demandée. Cette méthode est obtenue par l’expression [$request→getMethod()]. Le document envoyé au client est la chaîne jSON d’un tableau associatif dont certaines valeurs sont eux-mêmes des tableaux associatifs. Le paramètre [JSON_UNESCAPED_UNICODE] demande à ce que les caractères Unicode (comme les caractères accentués par exemple) soient envoyés tels quels et pas encodés ;
- ligne 27 : la réponse est envoyée au client ;
L’exécution du script client donne les résultats suivants :
1 2 3 4 5 6 7 8 9 10 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 10:08:45 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 207
connection: close
content-type: application/json
---Réponse du serveur [{"method":"GET","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom=jean-paul&nom=de+la+h%C3%BBche&age=45","getParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"},"bodyParameters":[]}]
|
ligne 10 :
[method] : la méthode est GET ;
[uri] : on voit les paramètres url-encodés de la requête GET dans l’URI demandée ;
[getParameters] : le tableau des paramètres du GET ;
[bodyParameters] : le tableau des paramètres uploadés : il est vide ;
Le client GET – version 2
Dans la version précédente du script client, nous avons url-encodé nous-mêmes les paramètres envoyés au serveur, dans un but pédagogique. L’objet [HttpClient] sait faire ce travail lui-même. C’est le script [parameters-get-client-2.php] suivant :
1 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 | <?php
// client GET d'un serveur web
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-parameters-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on prépare les paramètres
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// on fait la requête au serveur
$response = $httpClient->request('GET', $config['url-get'],
["query" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
]]);
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on affiche la réponse du serveur
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
Commentaires
lignes 33-37 : l’ajout de paramètres à la requête GET de la ligne 32. L’objet [HttpClient] s’occupera lui-même de l’encodage de l’URL ;
Le client POST
Un client HTTP envoie au serveur web la séquence de texte suivante : entêtes HTTP, ligne vide, document. Dans le client précédent, cette séquence était la suivante :
1 2 3 | GET /url?paramètres HTTP/1.1
… autres entêtes HTTP
ligne vide
|
Il n’y avait pas de document. Il existe une autre façon de transmettre des paramètres, la méthode dite POST. Dans ce cas, la séquence de texte envoyée au serveur web est la suivante :
1 2 3 4 | POST /url HTTP/1.1
… autres entêtes HTTP
ligne vide
paramètres
|
Cette fois-ci, les paramètres qui pour le client GET étaient inclus dans les entêtes HTTP, font partie, dans le client POST, du document envoyé après les entêtes.
Le script du client POST [parameters-postclient.php] est le suivant :
1 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 | <?php
// client POST d'un serveur web
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-parameters-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on prépare les paramètres
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// on fait la requête au serveur
$response = $httpClient->request('POST', $config['url-post'],
["body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
]]);
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on affiche la réponse du serveur
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
- ligne 32 : on a maintenant une requête HTTP de type POST ;
- lignes 33-37 : les paramètres du POST sont appelés le corps (body) de la requête POST : c’est le document envoyé par le client au serveur. Ici, trois paramètres sont envoyés [nom, prenom, age] ;
- ligne 48 : on affiche la réponse jSON du serveur ;
Les résultats de l’exécution du script client sont les suivants :
1 2 3 4 5 6 7 8 9 10 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 11:43:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 163
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php","getParameters":[],"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
|
- ligne 10 : la méthode est [Post] et les paramètres sont de type [bodyParameters]. Il n’y a pas de paramètres [getParameters] comme le montre l’[uri] ;
On peut avoir la curiosité de voir ce que reçoit le serveur lors d’une requête POST. Pour cela, nous lançons notre serveur générique [RawTcpServer] sur le port 100 de la machine locale à partir d’un terminal Laragon (cf paragraphe lien) :
Vérifiez qu’en [4], vous êtes bien dans le dossier des utilitaires.
Nous modifions le fichier jSON [config-parameters-client.json] qui configure le client POST :
1 2 3 4 | {
"url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
"url-post": "http://localhost:100/php7/scripts-web/05/parameters-server.php"
}
|
- ligne 3 : nous avons changé le port du serveur web. Ce sera donc [RawTcpServer] qui sera contacté ;
Nous exécutons le client. Dans la fenêtre de [RawTcpServer] nous obtenons les informations suivantes :
en [6], la commande POST ;
en [7] : l’entête HTTP [Content-Length] donne le nombre d’octets du document que va envoyer le client au serveur. L’entête HTTP [Content-Type] donne la nature de ce document. Le type [application/x-www-form-urlencoded] désigne un texte url-encodé ;
en [8], la ligne vide qui annonce la fin des entêtes HTTP et le début du document de 44 octets. Ce que ne montre pas la copie d’écran c’est le document lui-même. C’est la chaîne url-encodée des paramètres : [prenom=jean-paul&nom=de+la+h%C3%BBche&age=45]. Le lecteur pourra vérifier qu’elle a bien 44 caractères ;
Un client POST mixte
Dans un POST, on peut mixer les paramètres encodés dans l’URL et ceux encodés dans le document envoyé par le client après les entêtes HTTP. Voici un exemple [parameters-mixte-postclient.php] :
1 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 | <?php
// client POST d'un serveur web
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-parameters-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on prépare les paramètres
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// on fait la requête au serveur
$response = $httpClient->request('POST', $config['url-post'],
[
// paramètres du document (body)
"body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
],
// paramètres de l'URL (query)
"query" => [
"prenom2" => $prenom,
"nom2" => $nom,
"age2" => $age
]]);
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on affiche la réponse du serveur
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
Commentaires
- ligne 32 : une requête POST ;
- lignes 40-45 : les paramètres url-encodés dans l’URL ;
- lignes 35-39 : les paramètres url-encodés dans le corps (body, document) de la requête ;
A l’exécution, on obtient les résultats console suivants :
1 2 3 4 5 6 7 8 9 10 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 12:34:23 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 270
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom2=jean-paul&nom2=de%20la%20h%C3%BBche&age2=45","getParameters":{"prenom2":"jean-paul","nom2":"de la hûche","age2":"45"},"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
|
ligne 10 : on voit que le serveur a été capable de récupérer les deux types de paramètres ;
Un client GET mixte
On essaie de faire la même chose que précédemment avec une requête GET. Le script [parameters-mixte-get-client.php] est le suivant :
1 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 | <?php
// client POST d'un serveur web
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-parameters-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on prépare les paramètres
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// on fait la requête au serveur
$response = $httpClient->request('GET', $config['url-post'],
[
// paramètres du document (body)
"body" => [
"prenom" => $prenom,
"nom" => $nom,
"age" => $age
],
// paramètres de l'URL (query)
"query" => [
"prenom2" => $prenom,
"nom2" => $nom,
"age2" => $age
]]);
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on affiche la réponse du serveur
print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
Commentaires
- ligne 32 : une requête POST ;
- lignes 40-45 : les paramètres url-encodés dans l’URL ;
- lignes 35-39 : les paramètres url-encodés dans le corps (body, document) de la requête ;
A l’exécution, on obtient les résultats console suivants :
1 2 3 4 5 6 7 8 9 10 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 12:41:19 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 217
connection: close
content-type: application/json
---Réponse du serveur [{"method":"GET","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom2=jean-paul&nom2=de%20la%20h%C3%BBche&age2=45","getParameters":{"prenom2":"jean-paul","nom2":"de la hûche","age2":"45"},"bodyParameters":[]}]
|
- ligne 10 : on constate que le serveur n’a pas reçu de paramètres url-encodés dans le document envoyé par le client. Lorsqu’on regarde les entêtes HTTP envoyés par celui-ci, on s’aperçoit qu’il a bien envoyé un document de 44 caractères mais le serveur ne l’a pas exploité ;
Finalement quelle méthode choisir pour envoyer de l’information au serveur ?
- la méthode [GET URL?param1=val1¶m2=val2&…] utilise une URL paramétrée qui peut servir de lien. C’est son principal avantage : l’utilisateur peut mettre dans son marque-pages de tels liens ;
- dans d’autres applications, on peut ne pas souhaiter afficher dans une URL les paramètres envoyés au serveur. Pour des raisons de sécurité par exemple. Alors on utilisera une méthode [POST] et on mettra les paramètres url-encodés dans un document envoyé au serveur ;
Gestion des sessions web¶
Dans les exemples client / serveur précédents on avait le fonctionnement suivant :
- le client ouvre une connexion vers le port 80 de la machine du service web ;
- il envoie la séquence de texte : en-têtes HTTP, ligne vide, [document] ;
- en réponse, le serveur envoie une séquence du même type ;
- le serveur clôt la connexion vers le client ;
- le client clôt la connexion vers le serveur ;
Si le même client fait peu après une nouvelle demande au serveur web, une nouvelle connexion est créée entre le client et le serveur. Celui-ci ne peut pas savoir si le client qui se connecte est déjà venu ou si c’est une première demande. Entre deux connexions, le serveur « oublie » son client. Pour cette raison, on dit que le protocole HTTP est un protocole sans état. Il est pourtant utile que le serveur se souvienne de ses clients. Ainsi si une application est sécurisée, le client va envoyer au serveur un login et un mot de passe pour s’identifier. Si le serveur « oublie » son client entre deux connexions, celui-ci devra s’identifier à chaque nouvelle connexion, ce qui n’est pas envisageable.
Pour faire le suivi d’un client, le serveur procède de la façon suivante : lors d’une première demande d’un client, il inclut dans sa réponse un identifiant que le client doit ensuite lui renvoyer à chaque nouvelle demande. Grâce à cet identifiant, différent pour chaque client, le serveur peut reconnaître un client. Il peut alors gérer une mémoire pour ce client sous la forme d’une mémoire associée de façon unique à l’identifiant du client.
Techniquement cela se passe ainsi :
- dans la réponse à un nouveau client, le serveur inclut l’en-tête HTTP Set-Cookie : MotClé=Identifiant. Il ne fait cela qu’à la première demande ;
- dans ses demandes suivantes, le client va renvoyer son identifiant via l’en-tête HTTP Cookie : MotClé=Identifiant afin que le serveur le reconnaisse ;
On peut se demander comment le serveur fait pour savoir qu’il a affaire à un nouveau client plutôt qu’à un client déjà venu. C’est la présence de l’en-tête HTTP Cookie dans les en-têtes HTTP du client qui le lui indique. Pour un nouveau client, cet en-tête est absent.
L’ensemble des connexions d’un client donné est appelé une session.
Le fichier de configuration [php.ini]¶
Pour que la gestion des sessions fonctionne correctement avec PHP, il faut vérifier que celui-ci est correctement configuré. Sous windows, son fichier de configuration est php.ini. Selon le contexte d’exécution (console, web), le fichier de configuration [php.ini] doit être recherché dans des dossiers différents. Pour découvrir ceux-ci, on utilisera le script suivant :
1 2 3 4 | <?php
// infos PHP
phpinfo();
|
Ligne 4, la fonction phpinfo donne des informations sur l’interpréteur PHP qui exécute le script. Elle donne notamment le chemin du fichier de configuration [php.ini] utilisé.
Nous avons déjà utilisé ce script dans un environnement console (cf paragraphe lien). Dans un environnement web, on obtient le résultat suivant :
- en [1-2], le fichier [php.ini] qui configure l’interpréteur des scripts web. On trouve dans ce fichier une section session :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | [Session]
session.save_handler = files
session.save_path = "C:/myprograms/laragon-lite/tmp"
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 36000
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5
|
ligne 2 : les données d’une session client sont sauvegardées dans un fichier ;
ligne 3 : le dossier de sauvegarde des données de session. Si ce dossier n’existe pas, aucune erreur n’est signalée et la gestion des sessions ne fonctionne pas ;
lignes 4-6 : indiquent que l’identifiant de session est géré par les en-têtes HTTP Set-Cookie et Cookie ;
ligne 7 : l’entête Set-Cookie sera de la forme Set-Cookie : PHPSESSID=identifiant_de_session ;
ligne 8 : une session client n’est pas démarrée automatiquement. Le script serveur doit la demander explicitement par une instruction session_start() ;
ligne 9 : le cookie de session est valable tant que le navigateur client n’a pas été fermé ;
ligne 10 : le chemin pour lequel le cookie de session doit être renvoyé. Si [session.cookie_path = /xxx], alors à chaque fois que le navigateur demande une URL de type [/xxx/yyy/zzz] alors il doit renvoyer le cookie. Ici le chemin [/] indique que le cookie doit être renvoyé pour toute URL du site ;
ligne 13 : certains objets mis en session doivent être sérialisés pour pouvoir être stockés dans un fichier. C’est PHP qui assure cette sérialisation / désérialisation avec les fonctions [serialize / unserialize] ;
ligne 16 : durée de vie au-delà de laquelle les objets de session stockés dans le fichier de sauvegarde sont considérés comme obsolètes ;
ligne 19 : durée de vie d’une session. Au-delà de cette durée, une nouvelle session est créée et les objets sauvegardés dans la précédente session sont perdus ;
Exemple 1
Le serveur
La gestion de l’identifiant de session est transparent pour un service web. Cet identifiant est géré par le serveur web. Un service web a accès à la session du client via l’instruction session_start(). A partir de ce moment, le service web peut lire / écrire des données dans la session du client via le dictionnaire $_SESSION. Si on utilise la bibliothèque [HttpFoundation], la session est disponible via l’expression [Request→getSession].
Le code suivant [session-server.php] montre la gestion en session de trois compteurs. A chaque nouvelle requête, le script web incrémente ces compteurs et les met en session pour qu’ils puissent être récupérés lors de la requête suivante.
1 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 | <?php
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
//
// on récupère la requête
$request = Request::createFromGlobals();
// session
$session = new Session();
$session->start();
// on récupère trois compteurs dans la session
if ($session->has("N1")) {
// incrémentation du compteur N1
$session->set("N1", (int) $session->get("N1") + 1);
} else {
// le compteur N1 n'est pas en session - on le crée
$session->set("N1", 0);
}
if ($session->has("N2")) {
// incrémentation du compteur N2
$session->set("N2", (int) $session->get("N2") + 1);
} else {
// le compteur N2 n'est pas en session - on le crée
$session->set("N2", 10);
}
if ($session->has("N3")) {
// incrémentation du compteur N3
$session->set("N3", (int) $session->get("N3") + 1);
} else {
// le compteur N3 n'est pas en session - on le crée
$session->set("N3", 100);
}
// on élabore la réponse
$response = new Response();
// le contenu de la réponse est du texte utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// la réponse sera le jSON d'un tableau contenant les trois compteurs
$response->setContent(json_encode([
"N1" => $session->get("N1"),
"N2" => $session->get("N2"),
"N3" => $session->get("N3")]));
// envoi de la réponse
$response->send();
|
- ligne 10 : l’objet [$request] encapsule la totalité des informations sur la requête reçue par le script web ;
- lignes 12-13 : on crée une session et on l’active. L’objet [Session] encapsule les données de la session correspondant au cookie de session envoyé par le client. Si celui-ci n’a pas envoyé un tel cookie, alors il n’y aucune donnée mémorisée dans [Session]. Le script web va inclure dans sa 1re réponse l’entête HTTP [Set-Cookie : PHPSESSID=xxx]. Dans ses requêtes ultérieures, le client enverra l’entête HTTP [Cookie : PHPSESSID=xxx] pour indiquer la session dont il veut utiliser le contenu. Une session est la mémoire d’un client ;
- ligne 15 : on regarde si la session a une clé nommée [N1]. Ce
sera le nom de notre 1er compteur. Si ce n’est pas le cas
(ligne 20), on lui donne la valeur 0 et on le met en session. Si
c’est le cas (ligne 23), on :
- le récupère dans la session ;
- incrémente de 1 sa valeur ;
- le remet dans la session ;
- lignes 22-35 : on fait la même chose pour les deux autres compteurs N2 et N3 ;
- lignes 36-40 : on prépare une réponse de type [application/json] ;
- lignes 42-45 : la réponse sera la chaîne jSON d’un tableau contenant les trois compteurs ;
- ligne 48 : on envoie la réponse au client ;
Dans la relation client / serveur, la gestion de la session client sur le serveur dépend des deux acteurs, le client et le serveur :
- le serveur a la charge d’envoyer un identifiant à son client lors de sa première demande
- le client a la charge de renvoyer cet identifiant à chaque nouvelle demande. S’il ne le fait pas, le serveur croira que c’est un nouveau client et génèrera un nouvel identifiant pour une nouvelle session.
Résultats
Nous utilisons comme client, un navigateur web. Par défaut (par configuration en fait), celui-ci renvoie bien au serveur les identifiants de session que celui-ci lui envoie. Au fil des requêtes, le navigateur va recevoir les trois compteurs envoyés par le serveur et va voir leurs valeurs s’incrémenter.
- En [2], la 1re demande au service web ;
- en [4], la 4e demande montre que les compteurs sont bien incrémentés. Il y a bien mémorisation des valeurs des compteurs au fil des demandes ;
Utilisons le mode développement pour voir les en-têtes HTTP échangés entre le serveur et le client. Nous fermons Firefox pour terminer la session courante avec le serveur, le rouvrons et activons le mode développement (F12). Ceci va supprimer la session courante du navigateur qui va donc en recommencer une nouvelle. Nous demandons le service [session-server.php] :
En [5], on voit l’identifiant de session envoyé par le serveur dans sa réponse à la 1re demande du client. Il utilise l’en-tête HTTP Set-Cookie.
Faisons une nouvelle demande en rafraîchissant (F5) la page dans le navigateur web :
Ci-dessus on remarquera deux choses :
en [11], le navigateur web renvoie l’identifiant de session avec l’en-tête HTTP Cookie.
En [12], dans sa réponse, le service web n’inclut plus cet identifiant. C’est désormais le client qui a la charge de l’envoyer à chacune de ses demandes.
Le client
Nous écrivons maintenant un script client du script serveur précédent. Dans sa gestion de la session, il doit se comporter comme le navigateur web :
- dans la réponse du serveur à sa première demande, il doit trouver l’identifiant de session que le serveur lui envoie. Il sait qu’il le trouvera dans l’en-tête HTTP Set-Cookie.
- il doit, à chacune de ses demandes ultérieures, renvoyer au serveur l’identifiant qu’il a reçu. Il le fera avec l’en-tête HTTP Cookie.
Le client [session-client] est configuré par le fichier jSON [config-session-client.json] suivant :
1 2 3 | {
"url": "http://localhost/php7/scripts-web/06/session-server.php"
}
|
Le code du client [session-client] est le suivant :
1 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 | <?php
// gestion d'une session
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-session-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create();
try {
// on va faire 10 requêtes
for ($i = 0; $i < 10; $i++) {
// on fait la requête au serveur
if (!isset($sessionCookie)) {
// sans session
$response = $httpClient->request('GET', $config['url']);
} else {
// avec session
$response = $httpClient->request('GET', $config['url'],
["headers" => ["Cookie" => $sessionCookie]]);
}
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on récupère le cookie de session s'il existe
if (isset($headers["set-cookie"])) {
// cookie de session ?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// on affiche la réponse jSON du serveur
print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
Commentaires
- ligne 27 : création du client HTTP ;
- ligne 30 : on va faire 10 fois la même requête au server [session-server.php] ;
- ligne 32 : la variable [$sessionCookie] aura pour valeur la valeur de l’entête HTTP [Set-Cookie] reçue par le client ;
- lignes 32-34 : si cette variable n’existe pas c’est que la session n’a pas encore démarré. On envoie la commande [GET] sans l’entête [Cookie] ;
- lignes 35-38 : sinon la session a démarré et on envoie la commande [GET] avec l’entête [Cookie]. La valeur de cet entête sera [$sessionCookie] ;
- ligne 50 : si l’entête [Set-Cookie] fait partie des entêtes HTTP reçus, alors on cherche le cookie de session ;
- ligne 52 : le serveur web peut envoyer plusieurs entêtes [Set-Cookie]. Le cookie de session n’est que l’un d’entre-eux. Dans notre exemple, il a la particularité d’être de la forme [PHPSESSID=xxx;] ;
- lignes 53-57 : on utilise une expression régulière pour trouver le cookie de session ;
- ligne 62 : une fois les 10 requêtes faites, on affiche la dernière réponse jSON du serveur ;
Résultats
L’exécution du script client provoque l’affichage suivant dans la console Netbeans :
1 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 | "C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.exe" "C:\Data\st-2019\dev\php7\poly\scripts-console\clients web\06\session-client.php"
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=1cerjgsgdlc35e1mkenvtltmh8; path=/
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
…………………………………………………………
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse du serveur : {"N1":9,"N2":19,"N3":109}
|
- ligne 8 : dans sa première réponse, le serveur envoie l’identifiant de session. Dans les réponses suivantes, il ne l’envoie plus ;
- ligne 41 : les trois compteurs [N1, N2, N3] ont bien été incrémentés 9 fois. Lors de la requête n° 1, ils ont été mis à zéro ;
L’exemple suivant montre qu’on peut aussi sauvegarder les valeurs d’un tableau ou d’un objet dans la session.
Exemple 2
Le serveur
Nous allons mettre un objet [Personne] dans la session. La définition de cette classe est la suivante :
1 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 | <?php
namespace Modèles;
class Personne implements \JsonSerializable {
// attributs
private $nom;
private $prénom;
private $âge;
// conversion d'un tableau associatif vers un objet [Personne]
public function setFromArray(array $assoc): Personne {
// on initialise l'objet courant avec le tableau associatif
foreach ($assoc as $attribute => $value) {
$this->$attribute = $value;
}
// résultat
return $this;
}
// getters et setters
public function getNom() {
return $this->nom;
}
public function getPrénom() {
return $this->prénom;
}
public function setNom($nom) {
$this->nom = $nom;
return $this;
}
public function setPrénom($prénom) {
$this->prénom = $prénom;
return $this;
}
public function getÂge() {
return $this->âge;
}
public function setÂge($âge) {
$this->âge = $âge;
return $this;
}
// toString
public function __toString(): string {
return "Personne [$this->prénom, $this->nom, $this->âge]";
}
// implémente l'interface JsonSerializable
public function jsonSerialize(): array {
// on rend un tableau associatif avec pour clés les attributs de l'objet
// ce tableau pourra ensuite être encodé en jSON
return get_object_vars($this);
}
// conversion d'un jSON vers un objet [Personne]
public static function jsonUnserialize(string $json): Personne {
// on crée une personne à partir de la chaîne jSON
return (new Personne())->setFromArray(json_decode($json, true));
}
}
|
Le script serveur sera le suivant :
1 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 | <?php
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
//
// on récupère la requête courante
$request = Request::createFromGlobals();
// session
$session = new Session();
$session->start();
// on récupère diverses données dans la session
// tableau
if ($session->has("tableau")) {
// le tableau est dans la session - on incrémente toutes ses valeurs
$tableau = $session->get("tableau");
for ($i = 0; $i < count($tableau); $i++) {
$tableau[$i] += 1;
}
// on remet le tableau dans la session
$session->set("tableau", $tableau);
} else {
// le tableau n'est pas dans la session - on le crée
$tableau = [0, 10, 100];
// on le met dans la session
$session->set("tableau", $tableau);
}
// dictionnaire
if ($session->has("assoc")) {
// [assoc] est dans la session - on incrémente tous ses éléments
$assoc = $session->get("assoc");
foreach ($assoc as $key => $value) {
$assoc[$key] = $value + 1;
}
// on met $assoc dans la session
$session->set("assoc", $assoc);
} else {
// [assoc] n'est pas dans la session - on le crée
$assoc = ["un" => 0, "deux" => 10, "trois" => 100];
// on met $assoc dans la session
$session->set("assoc", $assoc);
}
// objet Personne
if ($session->has("personne")) {
// [personne] est dans la session - on incrémente son âge
$personne = $session->get("personne");
$personne->setÂge($personne->getÂge() + 1);
} else {
// [personne] n'est pas dans la session - on le crée
$personne = (new Personne())->setFromArray(
["prénom" => "Léonard", "nom" => "Hûche", "âge" => 0]);
// on met $personne dans la session
$session->set("personne", $personne);
}
// on élabore la réponse
$response = new Response();
// le contenu de la réponse est du jSON utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
$response->setContent(json_encode([
"tableau" => $tableau,
"assoc" => $assoc,
"personne" => $personne], JSON_UNESCAPED_UNICODE));
// envoi de la réponse
$response->send();
|
Commentaires
- lignes 16-17 : on récupère la session en cours et on l’active ;
- lignes 21-34 : on gère un tableau [tableau] mis en session. A chaque nouvelle requête, ses éléments sont incrémentés de 1 ;
- lignes 36-49 : on gère un tableau associatif [assoc] mis en session. A chaque nouvelle requête, ses éléments sont incrémentés de 1 ;
- lignes 51-61 : on gère un objet [Personne] mis en session. A chaque nouvelle requête, l’âge de cette personne est incrémentée de 1 ;
- lignes 62-73 : on envoie une réponse jSON au client : la chaîne jSON d’un tableau associatif ;
Exécutons ce script à partir de Netbeans. Les deux premières requêtes donnent les résultats suivants (F5 dans le navigateur pour la deuxième) :
on voit qu’en [6-8], tous les compteurs ont été incrémentés ;
Le client
Le client est le même que dans l’exemple 1 (paragraphe lien). On ne modifie que son fichier de configuration [config-session-client] :
1 2 3 | {
"url": "http://localhost/php7/scripts-web/07/session-server.php"
}
|
L’exécution produit les résultats suivants :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=qbfrj8clr20mod3eriur71mao6; path=/
content-length: 119
connection: close
content-type: application/json
---Réponse avec statut : 200
………….……………………………………………………….
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 119
connection: close
content-type: application/json
---Réponse du serveur : {"tableau":[9,19,109],"assoc":{"un":9,"deux":19,"trois":109},"personne":{"nom":"Hûche","prénom":"Léonard","âge":9}}
|
- en ligne [22], on voit que tous les compteurs ont été incrémentés ;
Authentification¶
Nous nous intéressons maintenant aux services web destinés à certains utilisateurs seulement. Le client doit alors s’identifier auprès du service web avant d’avoir sa réponse.
Le client¶
Le code du client [auth-client.php] est le suivant :
1 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 | <?php
// gestion d'une session
//
// gestion des erreurs
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
// la configuration du client
const CONFIG_FILE_NAME = "config-auth-client.json";
// on récupère la configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
exit;
}
// on crée un client HTTP
$httpClient = HttpClient::create([
'auth_basic' => ['admin', 'admin'],
// "verify_peer" => false,
// "verify_host" => false
]);
try {
// on fait la requête au serveur
$response = $httpClient->request('GET', $config['url']);
// statut de la réponse
$statusCode = $response->getStatusCode();
print "---Réponse avec statut : $statusCode\n";
// on récupère les entêtes
print "---Entêtes de la réponse\n";
$headers = $response->getHeaders();
foreach ($headers as $type => $value) {
print "$type: " . $value[0] . "\n";
}
// on affiche la réponse jSON du serveur
print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
// on affiche l'erreur
print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
|
Commentaires
- lignes 27-31 : on a passé un paramètre à la méthode statique [HttpClient::create], un tableau associatif ;
- ligne 28 : la clé [auth_basic] a pour valeur un tableau à deux éléments [user, password]. C’est avec ces éléments que le client va s’authentifier auprès du service web. La clé [auth_basic] désigne un type d’authentification appelé [Autorization Basic] du nom de l’entête HTTP que va émettre le client. Il existe d’autres types d’authentification ;
- en-dehors de ce code, le client est identique aux précédents ;
Pour voir les entêtes HTTP envoyés par le client, nous allons le connecter au serveur TCP générique [RawTcpServer] comme nous l’avons fait déjà de nombreuses fois :
Nous lançons le client avec la configuration [config-auth-client.json] suivante :
1 2 3 | {
"url": "http://localhost:100/php7/scripts-web/08/auth-server.php"
}
|
Le serveur [RawTcpServer] reçoit alors les lignes suivantes :
- en [5], on voit l’entête [Autorization : Basic XXX] envoyé par le client. La chaîne XXX est la chaîne [user:password] codé en Base64 ;
Pour vous en assurer, vous pouvez décoder la chaîne reçue sur le site [https://www.base64decode.org/] :
Le serveur¶
Le serveur [auth-server.php] est le suivant :
1 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 | <?php
// dépendances
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// utilisateurs autorisés
$users = ["admin" => "admin"];
//
// 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 ?
$trouvé = array_key_exists($requestUser, $users) && $users[$requestUser] === $requestPassword;
// préparation de la réponse
$response = new Response();
// 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("\"PHP7 par l'exemple\"")]);
} else {
// trouvé - code 200
$response->setStatusCode(Response::HTTP_OK);
}
// la réponse n'a pas de contenu, seulement des entêtes HTTP
$response->send();
|
Commentaires
- ligne 9 : les utilisateurs autorisés, ici un seul de login [admin] avec le mot de passe [admin] ;
- ligne 14 : l’identifiant de l’utilisateur est récupéré dans l’entête [PHP-AUTH-USER]. Ce n’est pas un entête que le client a envoyé, mais un entête que le PHP du serveur a construit ;
- ligne 15 : le mot de passe de l’utilisateur est récupéré dans l’entête [PHP-AUTH-PW], un entête construit par PHP ;
- ligne 17 : on cherche l’utilisateur qui veut se connecter dans la liste des utilisateurs autorisés ;
- lignes 23-24 : si l’utilisateur n’a pas été reconnu, on envoie au
client
- ligne 23 : le code [401 Unauthorized] ;
- ligne 24 : un entête [WWW-Authenticate: Basic realm=”quelque chose”]. La plupart des navigateurs reconnaissent cet entête et vont faire apparaître une fenêtre d’authentification invitant l’utilisateur à s’authentifier. Les entêtes HTTP doivent être codés en ISO 8859-1. Les textes Netbeans sont eux codés en UTF-8. La fonction [utf8_decode] assure la conversion UTF-8 vers ISO 8859-1. Ici, elle n’était pas nécessaire car les caractères de la chaîne [PHP7 par l’exemple] sont les mêmes en UTF-8 et ISO 8859-1. La fonction n’est là que pour un rappel du codage utilisé par les entêtes HTTP ;
- ligne 25 : si l’utilisateur a été reconnu, on envoie au client le code [200 OK] ;
Demandons l’URL [auth-server.php] avec un navigateur :
On voit que le navigateur fait afficher une fenêtre d’authentification. En [2], on voit la valeur de l’entête [WWW-Authenticate] envoyé par le serveur. Si on regarde les entêtes HTTP reçus par le navigateur, on trouve la chose suivante :
1 2 3 4 5 6 7 8 9 | HTTP/1.0 401 Unauthorized
Date: Fri, 07 Jun 2019 09:11:23 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
X-Powered-By: PHP/7.2.11
Cache-Control: no-cache, private
WWW-Authenticate: Basic realm="PHP7 par l'exemple"
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
|
- ligne 1 : le code [401 Unauthorized] de la réponse ;
- ligne 6 : l’entête HTTP [WWW-Authenticate] ;
- ligne 7 : le corps de la réponse est vide ;
Si en [3-4], on tape [admin] deux fois, la réponse du serveur est alors la suivante :
1 2 3 4 5 6 7 8 | HTTP/1.0 200 OK
Date: Fri, 07 Jun 2019 09:21:00 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
X-Powered-By: PHP/7.2.11
Cache-Control: no-cache, private
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
|
- ligne 1 : le code 200 OK de la réponse ;
- ligne 6 : le corps de la réponse est vide ;
Si en [3-4], on tape des identifiants erronés, le navigateur [Firefox] utilisé pour les tests, représente indéfiniment le fenêtre d’authentification jusqu’à ce que les identifiants corrects soient tapés. A chaque fois un aller-retour avec le serveur a lieu avec toujours la même réponse qui déclenche la fenêtre d’authentification du navigateur.
Exécutons le client [auth-client.php] avec un utilisateur non autorisé. La réponse du serveur est la suivante :
1 2 3 | ---Réponse avec statut : 401
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 401 Unauthorized returned for "https://localhost/php7/scripts-web/08/auth-server.php".
|
- En [1], le client a bien reçu un code 401 ;
- en [3], une exception a été lancée dans le client. C’est le client Symfony [HttpClient] qui l’a lancée : il lance une exception lorsque que le code de statut de la réponse HTTP indique qu’il y a eu erreur côté serveur, et que le client essaie de lire les entêtes ou le contenu de la réponse du serveur. Le message de la ligne 3 nous permet de voir que le serveur a répondu par [HTTP/1.0 401 Unauthorized] pour indiquer que l’utilisateur n’avait pas été reconnu ;
Exécutons maintenant le client [auth-client.php] avec l’utilisateur autorisé [‘admin’,’admin’]. La réponse du serveur est alors la suivante :
1 2 3 4 5 6 7 8 9 10 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:11:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :
|
ligne 1 : le serveur a répondu [HTTP/1. 200 OK] ;
ligne 7 : la réponse n’a pas de contenu (0 octet) ;
Sécuriser la connexion client / serveur
Nous avons vu que pour s’authentifier auprès du serveur, le client envoyait l’entête :
1 | authorization: Basic YWRtaW46YWRtaW4=
|
Si cette ligne est interceptée par un programme espion, celui-ci pourra aisément retrouver les identifiants [login, mot de passe] encodés en base 64 dans la chaîne [YWRtaW46YWRtaW4=]. Pour cette raison, il faut que l’authentification ait lieu dans une connexion sécurisée entre le client et le serveur. Les URL sécurisées utilisent le protocole [HTTPS] au lieu du protocole HTTP. Le protocole [HTTPS] est le protocole HTTP au sein d’une connexion client / serveur sécurisée. Les URL sécurisées sont de la forme [https://chemin_document].
Tous les serveurs web n’acceptent pas les URL de cette forme. Il faut les modifier pour qu’ils soient sécurisés. Le serveur Apache de Laragon est un serveur sécurisé mais le protocole HTTPS n’est pas actif par défaut. Il faut l’activer dans le menu de Laragon :
- en [4], il faut activer le chiffrement SSL du serveur Apache ;
Ceci fait, le serveur Apache est automatiquement redémarré :
- en [1], un cadenas vert apparaît : c’est le signe que le protocole HTTPS a été activé ;
- en [2], un nouveau port de service apparaît, le port 443 ici. C’est le port de service du protocole sécurisé HTTPS ;
Maintenant que nous avons un serveur sécurisé, modifions le fichier de configuration [config-auth-client.json] du client de la façon suivante :
1 2 3 | {
"url": "https://localhost:443/php7/scripts-web/08/auth-server.php"
}
|
En [2], le protocole est devenu [https] et le port [443].
Maintenant exécutons le client [auth-client.php] avec l’utilisateur autorisé [admin, admin]. Les résultats console sont les suivants :
1 | Erreur de communication avec le serveur : Peer certificate cannot be authenticated with given CA certificates for"https://localhost/php7/scripts-web/08/auth-server.php".
|
Le client Symfony [HttpClient] a lancé une exception car le serveur lui a envoyé un certificat de confiance que [HttpClient] n’a pas accepté. La communication SSL se fait avec des certificats de confiance certifiés par des organismes officiels. Lorsqu’on a activé le protocole HTTPS sur le serveur Apache de Laragon, un certificat autosigné a été généré pour le serveur Apache. Un certificat autosigné est un certificat non validé par un organisme officiel. Le client Symfony [HttpClient] a refusé ce certificat autosigné.
Il est possible de demander à [HttpClient] de ne pas vérifier la validité du certificat envoyé par le serveur. Cela se fait avec des options dans la méthode [HttpClient::create] :
1 2 3 4 5 | // on crée un client HTTP
$httpClient = HttpClient::create([
'auth_basic' => ['admin', 'admin'],
"verify_peer" => false
]);
|
La ligne 4 demande à ce que le certificat du serveur ne soit pas vérifié. Nous avions déjà rencontré ce problème dans le script [http-02.php] du paragraphe lien. Ce script utilisait la bibliothèque [libcurl] pour se connecter à des sites HTTP et HTTPS. On avait alors utilisé la configuration suivante pour cette bibliothèque :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Initialisation d'une session cURL
$curl = curl_init($url);
if ($curl === FALSE) {
// il y a eu une erreur
return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
}
// options de curl
$options = [
// mode verbose
CURLOPT_VERBOSE => true,
// nouvelle connexion - pas de cache
CURLOPT_FRESH_CONNECT => true,
// timeout de la requête (en secondes)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// ne pas vérifier la validité des certificats SSL
CURLOPT_SSL_VERIFYPEER => false,
// suivre les redirections
CURLOPT_FOLLOWLOCATION => true,
// récupération du document demandé sous la forme d'une chaîne de caractères
CURLOPT_RETURNTRANSFER => true
];
// paramétrage de curl
curl_setopt_array($curl, $options);
|
Ligne 17, la constante [CURLOPT_SSL_VERIFYPEER] contrôle la vérification ou non du certificat envoyé par le serveur. Le client [HttpClient] est en fait un client [curl] lorsque l’extension [curl] est activée dans la configuration de PHP comme c’est le cas ici. La classe instanciée par [HttpClient::create] est alors la classe [CurlHttpClient]. Les constantes de [curl] sont disponibles dans cette classe mais sous d’autres noms :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $curlopts = [
CURLOPT_URL => $url,
CURLOPT_USERAGENT => 'Symfony HttpClient/Curl',
CURLOPT_TCP_NODELAY => true,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
CURLOPT_CONNECTTIMEOUT_MS => 1000 * $options['timeout'],
CURLOPT_PROXY => $options['proxy'],
CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
CURLOPT_CAINFO => $options['cafile'],
CURLOPT_CAPATH => $options['capath'],
CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
CURLOPT_SSLCERT => $options['local_cert'],
CURLOPT_SSLKEY => $options['local_pk'],
CURLOPT_KEYPASSWD => $options['passphrase'],
CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
];
|
Nous avons surligné en jaune, les constantes utilisées par [CurlHttpClient].
Si maintenant, nous exécutons le client [auth-client] avec l’utilisateur [admin, admin] nous obtenons le résultat suivant :
1 2 3 4 5 6 7 8 9 10 | ---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:44:37 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :
|
L’utilisateur a bien été reconnu. Si nous exécutons le client [auth-client] avec un utilisateur autre que [admin, admin], nous obtenons le résultat suivant :
1 2 3 | ---Réponse avec statut : 403
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 403 Forbidden returned for "https://localhost/php7/scripts-web/08/auth-server.php".
|
Désormais, nous savons nous authentifier auprès d’un serveur sécurisé.
Exercice d’application – version 8¶
Nous allons reprendre l’application exemple – version 5 (paragraphe lien) et allons en faire une application client / serveur.
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 ;
Le serveur¶
Nous nous intéressons à la partie serveur de l’application.
Cette architecture sera implémentée par les scripts suivants :
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 lien.
La couche [dao]¶
La couche [dao] implémente l’interface [InterfaceServerDao] suivante :
1 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 :
1 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 lien.
La couche [métier]¶
La couche [métier] implémente l’interface [InterfaceServerMetier] suivante :
1 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 :
1 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 lien. Sa version objet avec une base de données a été présentée au paragraphe lien.
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 :
1 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 lien.
Le script [impots-server] implémente la couche [web] de la façon suivante :
1 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] :
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 lien et lien).
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 lien. 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].
Tests de la couche [dao]¶
Le test [ServerDaoTest] sera le suivant :
1 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 lien ;
Les résultats du test sont les suivants :
Tests de la couche [métier]¶
Le test [ServerMetierTest] sera le suivant :
1 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 lien ;
Les résultats du test sont les suivants :
Le client¶
Nous nous intéressons à la partie cliente de l’application.
Cette architecture sera implémentée par les scripts suivants :
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 lien ;
[ExceptionImpots] au paragraphe lien ;
[TaxPayerData] au paragraphe lien ;
La couche [dao]
La couche [dao] implémente l’interface [InterfaceClientDao] suivante :
1 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 :
1 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 lien) 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] ;
La couche [métier]
La couche [métier] [8] implémente l’interface [InterfaceClientMetier] suivante :
1 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 :
1 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 lien ;
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 :
1 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 :
1 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 :
1 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 :
1 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 :
1 2 3 4 | "user": {
"login": "x",
"passwd": "x"
},
|
Les résultats dans la console du client sont alors les suivants :
1 2 | L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
|
Tests [Codeception]¶
Comme il a été fait pour les version précédentes, nous allons écrire des tests [Codeception] pour la version 08.
Test de la couche [métier]¶
Le test [ClientMetierTest.php] est le suivant :
1 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 lien ;
- 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 lien ;
Les résultats du test sont les suivants :
Exercice d’application – version 9¶
Dans cette version, nous allons améliorer le serveur de la façon suivante :
- actuellement, à chaque requête les données de l’administration
fiscale sont cherchées dans la base de données. Nous allons utiliser
une session :
- lors de la 1re requête d’un utilisateur, de l’administration fiscale sont cherchées dans la base de données et mises en session ;
- lors des requêtes suivantes du même utilisateur, de l’administration fiscale sont cherchées dans la session. On peut espérer un léger gain de temps d’exécution car les requêtes en base de données coûtent cher ;
- le serveur va loguer dans un fichier texte, les moments importants :
- l’authentification réussie ou ratée ;
- la validité ou non des paramètres envoyés par le client ;
- le résultat du calcul d’impôt ;
- les différents cas d’erreur ;
- en cas d’erreur fatale, un mail sera envoyé à l’administrateur de l’application ;
Le client devra lui aussi être modifié pour gérer le cookie de session qu’on va lui envoyer.
Le serveur¶
Nous nous intéressons à la partie serveur de l’application.
Cette architecture sera implémentée par les scripts suivants :
Utilitaires¶
La classe [Logger]¶
La classe [Logger] sera utilisée pour écrire des logs dans un fichier texte :
1 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 | <?php
namespace Application;
class Logger {
// attribut
private $resource;
// constructeur
public function __construct(string $logsFilename) {
// ouverture du fichier
$this->resource = fopen($logsFilename, "a");
if (!$this->resource) {
throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
}
}
// écriture d'un message dans les logs
public function write(string $message) {
fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
}
// fermeture du fichier des logs
public function close() {
fclose($this->resource);
}
}
|
Commentaires
- ligne 7 : la ressource du fichier de logs ;
- ligne 10 : le constructeur de la classe reçoit comme paramètre le nom du fichier de logs ;
- ligne 12 : on ouvre le fichier texte en mode ajout (a+) : le fichier sera ouvert, son contenu préservé. Les écritures se feront derrière le contenu actuel ;
- lignes 13-15 : si l’ouverture n’a pu se faire, une exception est lancée ;
- lignes 19-21 : la méthode [write] permet d’écrire le message [$message] dans le fichier de logs, précédé de la date et de l’heure ;
- lignes 24-16 : la méthode [close] permet permet de fermer le fichier de logs ;
Note : l’application serveur est susceptible de servir plusieurs clients simultanément. Or on a un seul fichier de logs pour tous. Il y a donc un risque d’accès concurrents pour écrire dans le fichier. Il faudrait donc synchroniser les écritures pour éviter que celles-ci ne se mélangent. Pour cela PHP dispose de sémaphores [https://www.php.net/manual/fr/book.sem.php]. Nous ignorerons la synchronisation des écritures ici mais il faut rester conscient du problème.
La classe [SendAdminMail]¶
La classe [SendAdminMail] permet d’envoyer un mail à l’administrateur de l’application en cas de plantage de celle-ci :
1 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 | <?php
namespace Application;
class SendAdminMail {
// attributs
private $config;
private $logger;
// constructeur
public function __construct(array $config, Logger $logger = NULL) {
$this->config = $config;
$this->logger = $logger;
}
public function send() {
// envoie $this->config['message'] au serveur smtp $this->config['smtp-server'] sur le port $infos[smt-port]
// si $this->config['tls'] est vrai, le support TLS sera utilisé
// le mail est envoyé de la part de $this->config['from']
// pour le destinataire $this->config['to']
// le message a le sujet $this->config['subject']
// on attache au mail les attachements de $this->config['attachments']
// le résultat de la méthode
try {
// création du message
$message = (new \Swift_Message())
// sujet du message
->setSubject($this->config["subject"])
// expéditeur
->setFrom($this->config["from"])
// destinataires avec un dictionnaire (setTo/setCc/setBcc)
->setTo($this->config["to"])
// texte du message
->setBody($this->config["message"])
;
// attachements
foreach ($this->config["attachments"] as $attachment) {
// chemin de l'attachement
$fileName = __DIR__ . $attachment;
// on vérifie que le fichier existe
if (file_exists($fileName)) {
// on attache le document au message
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
if ($this->logger !== NULL) {
// erreur
$this->logger->write("L'attachement [$fileName] n'existe pas\n");
}
}
}
// protocole 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 {
// pas de TLS
$transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
}
// le gestionnaire de l'envoi
$mailer = new \Swift_Mailer($transport);
// envoi du message
$mailer->send($message);
// fin
if ($this->logger !== NULL) {
$this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
}
} catch (\Throwable $ex) {
// erreur
if ($this->logger !== NULL) {
$this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
}
}
}
}
|
Commentaires
- ligne 11 : le constructeur reçoit deux paramètres :
- [$config] : un tableau associatif contenant toutes les informations nécessaires à l’envoi du mail ;
- [$logger] : un logger permettant de loguer les moments importants de l’envoi du mail ;
Le tableau associatif aura la forme suivante :
1 2 3 4 5 6 7 8 9 | {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "plantage du serveur de calcul d'impôts",
"tls": "FALSE",
"attachments": []
}
|
lignes 16-76 : la méthode [send] permet d’envoyer le mail. Ce code a été présenté et décrit au paragraphe lien ;
La couche [dao]
Le script [ServeurDaoWithSession.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php
// espace de noms
namespace Application;
// définition d'une classe ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {
// constructeur
public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
// cas le + simple
if ($taxAdminData !== NULL) {
$this->taxAdminData = $taxAdminData;
} else {
// on passe la main à la classe parent
parent::__construct($databaseFilename);
}
}
}
|
Commentaires
- ligne 7 : la classe [ServerDaoWithSession] de la version 09 étend la classe [ServerDao] de la version 08. En effet, la classe [ServerDao] sait utiliser la base de données. Il ne nous reste plus qu’à prévoir le cas où les données de l’administration fiscale ont déjà été acquises :
- ligne 10 : le constructeur reçoit maintenant deux paramètres :
- [string $databaseFilename] : nom du fichier contenant les informations permettant de se connecter à la base de données si les données de l’administration fiscale n’ont pas encore été acquises, NULL sinon ;
- [TaxAdminData $taxAdminData] : les données de l’administration fiscale si déjà acquises, NULL sinon ;
Lors du démarrage d’une session web, la couche [dao] sera construite avec un objet [$databaseFilename] non NULL et un objet [taxAdminData] NULL. Les données de l’administration fiscale seront alors recherchées en base et mémorisées dans la session. Lors des requêtes ultérieures de la même session, la couche [dao] sera construite avec un objet [databaseFilename] NULL et un objet [taxAdminData] provenant de la session et non NULL. Il n’y aura donc pas de recherche en base.
Le script serveur¶
Le script serveur [impots-server.php] est configuré par le fichier jSON [config-server.json] suivant :
1 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 | {
"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"
}
|
Le script serveur [impots-server.php] évolue de la façon suivante :
1 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 152 153 154 155 156 157 158 159 160 161 162 163 164 | <?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épendances Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// session
$session = new Session();
$session->start();
// préparation de la réponse JSON du serveur
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// création du fichier des logs
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// internal server error
doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
// terminé
exit;
}
// 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")) {
// log
$logger->write("Autentification en cours…\n");
// authentification
…
}
// a-t-on trouvé l'utilisateur ?
if (!$trouvé) {
// pas trouvé - 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
);
// terminé
exit;
} else {
// on note dans la session qu'on a authentifié l'utilisateur
$session->set("user", TRUE);
// log
$logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
}
} 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
…
// 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
// création de la couche [dao]
if (!$session->has("taxAdminData")) {
// les données 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 ServerDaoWithSession($config["databaseFilename"], NULL);
// on met les données en session
$session->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// on note l'erreur
doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
// terminé
exit;
}
} else {
// les données sont prises dans la session
$dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
// logs
$logger->write("données fiscales prises en session\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);
// fin
exit;
function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
// 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);
}
// fonction d'envoi de la réponse HTTP au client
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
// $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
//
// 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();
}
}
|
Commentaires
lignes 34-35 : on démarre une session ;
lignes 38-40 : on prépare une réponse jSON ;
lignes 42-50 : on essaie de créer le fichier de logs. Si exception il y a, la méthode [doInternalServer] (lignes 132-140) est appelée ;
ligne 132 : la méthode [doInternalServer] accepte quatre paramètres :
- [$message] : le message à loguer. Doit être codé en UTF-8 ;
- [$response] : l’objet [Response] qui encapsule la réponse du serveur à son client ;
- [$logger] : l’objet [Logger] permettant de faire les logs ;
- [$infos] : les informations permettant d’envoyer un mail à l’administrateur de l’application ;
lignes 135-137 : on envoie un mail à l’administrateur de l’application ;
ligne 139 : on envoie la réponse au client :
- $response : réponse HTTP ;
- $result : le serveur envoie la chaîne jSON du tableau [‘réponse’=>[« erreur » => $message]] ;
- $statusCode : [Response::HTTP_INTERNAL_SERVER_ERROR], code 500 ;
- $headers : [], pas d’entêtes HTTP à ajouter à la réponse ;
- $logger : le logueur de l’application ;
ligne 58 : grâce à la session mise en place on ne fera l’authentification du client qu’une seule fois :
- une fois le client authentifié, on mettra une clé [user] dans la session (ligne 78) ;
- lors de la requête suivante du même client, la ligne 58 évite une authentification devenue inutile ;
ligne 103 : grâce à la session mise en place on ne cherchera les données en base qu’une seule fois :
- lors de la première requête, la recherche en base se fera (ligne 108). Les données récupérées sont ensuite mises en session (ligne 110) associées à la clé [taxAdminData] ;
- lors des requêtes suivantes, la clé [taxAdminData] sera trouvée en session (ligne 103) et alors les données discales seront directement communiquées à la couche [dao] (ligne 119) ;
lignes 111-116 : la recherche des données fiscales en base peut échouer. Dans ce cas, on envoie au client un code [500 Internal Server Error] ;
ligne 113 : le message d’erreur de l’exception du pilote MySQL est codé en ISO 8859-1. On le convertit en UTF-8 pour être correctement logué ;
le reste du code est quasi identique à celui de la version précédente ;
lignes 143-164 : la fonction [sendResponse] envoie toutes les réponses au client ;
lignes 144-148 : signification des paramètres ;
ligne 153 : la réponse est toujours la chaîne jSON d’un tableau [‘résultat’=>qqChose] ;
ligne 156 : parfois il y a des entêtes HTTP à ajouter à la réponse. C’est le cas en ligne 71 ;
ligne 158 : la réponse est envoyée ;
lignes 160-163 : la réponse est loguée et le logueur fermé ;
Tests [Codeception]
Nous n’allons tester que la couche [dao] qui est la seule à avoir changé.
Le code du test [ServerDaoTest] est le suivant :
1 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-09");
// 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 ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
|
- lignes 9-24 : on crée un environnement d’exécution identique à celui du script serveur [impots-server] ;
- ligne 38 : pour construire la couche [dao], on instancie la classe [ServerDaoWithSession] ;
Le résultat des tests est le suivant :
Le client¶
Nous nous intéressons à la partie cliente de l’application.
Cette architecture sera implémentée par les scripts suivants :
Dans la nouvelle version, seuls changent :
le fichier de configuration [config-client.json] ;
la couche [dao] du client ;
La couche [dao]
La couche [Dao] évolue de la façon suivante :
1 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 | <?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;
private $sessionCookie;
// 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 {
// cookie de session ?
if (!$this->sessionCookie) {
// 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 sans cookie de session
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]
]);
} else {
// on fait la requête au serveur avec le cookie de session
// on crée un client HTTP
$httpClient = HttpClient::create([
"verify_peer" => false
]);
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"headers" => ["Cookie" => $this->sessionCookie]
]);
}
// 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);
}
if (!$this->sessionCookie) {
// on récupère le cookie de session
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// cookie de session ?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// on rend la réponse
return $réponse;
}
}
|
Commentaires
La modification de la couche [dao] consiste à maintenant gérer une session :
- ligne 14 : le cookie de la session ;
- lignes 25-39 : lors de la 1re requête ce cookie n’existe pas : on fait alors la requête auprès du serveur en envoyant les informations d’authentification (ligne 28) ;
- lignes 40-53 : lors des requêtes suivantes, on a normalement le cookie de session. On n’envoie pas alors les informations d’authentification (lignes 42-44) ;
- lignes 69-82 : la réponse du serveur à la 1re requête va comporter un cookie de session. On le récupère. Ce code a déjà été utilisé et expliqué au paragraphe lien ;
- ligne 78 : le cookie de session récupéré est mémorisé dans l’attribut de classe [$sessionCookie] ;
Note : on aurait pu garder l’ancienne version de la couche [dao] et faire l’authentification à chaque requête car celle-ci a un coût négligeable. Par souci pédagogique, on a voulu rappeler comment un client HTTP pouvait gérer une session.
Le fichier de configuration¶
Le fichier de configuration jSON évolue de la façon suivante :
1 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-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"
}
|
Seule l’URL de la ligne 24 change.
Quelques tests¶
Test 1¶
Tout d’abord nous exécutons le client dans un environnement sans erreurs. Les résultats sont toujours ceux des versions précédentes. Mais maintenant côté serveur, on a un fichier de logs [logs.txt] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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
…
|
lignes 3-7 : lors de la 1re requête, il y a authentification et recherche des données en base ;
lignes 9-14 : lors de la requête suivante, il n’y a plus d’authentification et les données sont prises en session. Cela se répète lors des requêtes suivantes (lignes 15 et au-delà) ;
Test 2
Maintenant coupons la base de données MySQL. Côté client, on a le résultat console suivant :
1 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é
|
Côté serveur, on a les logs [logs.txt] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | 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
…
|
Pour avoir le mail reçu par l’administrateur de l’application, on utilise le script [imap-03.php] du paragraphe lien avec le fichier de configuration [config-imap-01.json] suivant :
1 2 3 4 5 6 7 8 9 10 | {
"{localhost:110/pop3}": {
"imap-server": "localhost",
"imap-port": "110",
"user": "guest@localhost",
"password": "guest",
"pop3": "TRUE",
"output-dir": "output/localhost-pop3"
}
}
|
On obtient le résultat suivant :
Le fichier [message_1.txt] contient le texte suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | 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.
|
Test 3¶
Maintenant faison en sorte que le fichier [logs.txt] ne puisse être créé. Pour cela, il suffit de créer un dossier [logs.txt] :
Ceci fait, exécutons le client.
Côté client, on a les résultats console suivants :
1 2 | L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé
|
Côté serveur, il n’y a pas de logs mais l’administrateur reçoit le mail suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | 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]
|
Test 4¶
Cette fois-ci, donnons, dans le fichier de configuration du client, des identifiants erronés au client qui se connecte.
Le client affiche les résultats console suivants :
1 2 | L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
|
Côté serveur, les logs suivants apparaissent :
1 2 3 | ---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]"}}
|
Test 5¶
Remettons le bon utilisateur [admin, admin] dans le fichier de configuration du client.
Maintenant demandons l’URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] du serveur directement dans un navigateur sans passer de paramètres :
Dans le fichier de logs [logs.txt] du serveur, on a les lignes suivantes :
1 2 3 4 | ---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"]}}
|
Tests [Codeception]¶
Comme il a été fait pour les version précédentes, nous allons écrire des tests [Codeception] pour la version 09.
Le test [ClientMetierTest.php] est le suivant :
1 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 | <?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-09");
// 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";
}
//
// uses
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;
// classe de test
class ClientMetierTest extends Unit {
…
}
|
Commentaires
- par rapport à la classe de test de la version 08, seule change la ligne 10 qui spécifie le dossier racine du client à tester ;
Les résultats du test sont les suivants :
Il est intéressant d’aller voir les logs du serveur [logs.txt] :
1 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 | 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}}
|
On constate que les données de l’administration fiscale sont toujours prises dans la base de données et jamais dans la session. Revenons au code du test exécuté :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
…
// classe de test
class ClientMetierTest extends 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 test2() {
…
}
public function test3() {
…
}
…
}
|
Dans une classe de test [Codeception] le constucteur est exécuté pour chaque test.
- ligne 21 : un nouveau [ClientDao] est donc créé pour chaque test avec un cookie de session NULL. Ceci explique que ce client ne profite d’aucune session ;
Cet exemple nous montre que la session n’est pas le bon endroit pour stocker les données de l’administration fiscale. En effet, celles-ci sont communes à tous les utilisateurs de l’application. Or ici, elles sont dupliquées dans chacune des sessions de ceux-ci.
En programmation web, on distingue trois types de visibilité pour les données partagées :
- des données partagées par tous les utilisateurs de l’application web. Ce sont en général des données en lecture seule. PHP ne dispose pas nativement de cette mémoire ;
- des données partagées par les requêtes d”un même client. Ces
- données sont mémorisées dans la session. On parle alors de session client pour désigner la mémoire du client. Toutes les requêtes d’un client ont accès à cette session. Elles peuvent y stocker et y lire des informations. Dans les scripts précédents, cette session est implémentée par l’objet Symfony [HttpFoundationSessionSession] ;
- la mémoire de requête, ou contexte de requête. La requête d’un
- utilisateur peut être traitée par plusieurs actions successives. Le contexte de la requête permet à une action 1 de transmettre de l’information à une action 2. Dans les scripts précédents, la requête est implémentée par l’objet Symfony [HttpFoundationRequest] et sa mémoire par l’attribut [HttpFoundationRequest::attributes] ;
Des bibliothèques tierces existent pour donner à PHP une mémoire d’application. La nouvelle version de l’exercices d’application montre l’usage de l’une d’elles.
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.
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 :
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 :
Le client Redis en mode commande¶
Le serveur Redis peut être interrogé en mode commande. On ouvre un terminal Laragon (cf paragraphe lien) :
- 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].
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 :
Code du serveur¶
Le fichier de configuration [config-server.json] évolue de la façon suivante :
1 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 :
1 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 [ApplicationServerDaoWithSession] 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 [PredisClient] ;
- 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 ;
Code du client¶
La version 10 du client est identique à la version 9. Seul change le fichier de configuration [config-client.json] :
1 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 :
1 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é
|
Tests [Codeception] du client¶
La classe de test [ClientMetierTest] de la version 10 est identique à celle de la version 09 à une exception près :
1 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 :
1 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 ;
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’aministration ;
- 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 ;
Traitement de documents XML¶
Nous considérons le fichier XML [data.xml] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?xml version="1.0" encoding="UTF-8"?>
<tribu>
<enseignant>
<personne sexe="M">
<nom>dupont</nom>
<prenom>jean</prenom>
<age>28</age>
ceci est un commentaire
</personne>
<section>27</section>
</enseignant>
<etudiant>
<personne sexe="F">
<nom>martin</nom>
<prenom>charline</prenom>
<age>22</age>
</personne>
<formation>dess IAIE</formation>
</etudiant>
</tribu>
|
Nous analysons ce document avec le script suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php
// fichier XML à exploiter
$FILE_NAME = "data.xml";
// exploitation
$xml = simplexml_load_file($FILE_NAME);
print_r($xml);
print_r($xml->enseignant->personne['sexe']);
$nom=$xml->enseignant->personne->nom;
print "nom=$nom\n";
$sexe=$xml->enseignant->personne['sexe'];
print "sexe=$sexe\n";
$formation=$xml->etudiant->formation;
print "formation=$formation\n";
print "isset=".isset($xml->enseignant->personne->nom)."\n";
print "isset=".isset($xml->enseignant->personne->xx)."\n";
|
Nous utilisons ici un module PHP appelé [simpleXML] qui permet d’exploiter des documents XML.
- ligne 6 : chargement du fichier XML ;
- ligne 7 : affichage du document XML ;
- ligne 8 : affichage de la valeur de l’attribut “sexe” d’une personne enseignante : <enseignant><personne sexe=”…”> ;
- ligne 9 : affichage de la valeur de la 1re balise <enseignant><personne><nom> ;
On notera que la balise racine <tribu> n’intervient pas dans le code. Elle pourrait être n’importe quoi ;
Résultats console
1 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 | SimpleXMLElement Object
(
[enseignant] => SimpleXMLElement Object
(
[personne] => SimpleXMLElement Object
(
[@attributes] => Array
(
[sexe] => M
)
[nom] => dupont
[prenom] => jean
[age] => 28
)
[section] => 27
)
[etudiant] => SimpleXMLElement Object
(
[personne] => SimpleXMLElement Object
(
[@attributes] => Array
(
[sexe] => F
)
[nom] => martin
[prenom] => charline
[age] => 22
)
[formation] => dess IAIE
)
)
SimpleXMLElement Object
(
[0] => M
)
nom=dupont
sexe=M
formation=dess IAIE
isset=1
isset=
|
- lignes 1-37 : le document XML sous la forme d’un objet de type [simpleXML].
Le script précédent ne nous montre pas toutes les possibilités du module [simpleXML] mais il nous suffit pour écrire une nouvelle version de l’exercice d’application.
Exercice d’application – version 11¶
Il est encore fréquent que des services web envoient leur réponse sous la forme d’un flux XML plutôt que d’un flux jSON :
- le flux jSON est plus léger mais il faut un mode d’emploi pour le comprendre ;
- le flux XML est plus verbeux mais il est autodocumenté. Sa compréhension est immédiate ;
Nous modifions la version 11 client / serveur pour que le serveur envoie désormais un flux XML comme réponse à ses clients :
Le serveur¶
Cette architecture sera implémentée par les scripts suivants :
La classe [Utilitaires]¶
Nous reprenons la classe [Utilitaires] utilisée dès la version 03 (cf paragraphe lien) :
1 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 | <?php
// espace de noms
namespace Application;
// une classe de fonctions utilitaires
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
…
}
// from https://stackoverflow.com/questions/1397036/how-to-convert-array-to-simplexml
public static function getXmlForArrayOfAttributes(array $arrayOfAttributes,
\SimpleXmlElement &$node): void {
// on scanne les attributs du tableau
foreach ($arrayOfAttributes as $attribute => $value) {
// l'attribut est-il numérique ?
if (is_numeric($attribute)) {
// cas de l'index de tableau (mais aussi autres cas)
$attribute = 'i' . $attribute;
}
// $value est-il un tableau ?
if (is_array($value)) {
// on va explorer le tableau [$value] à son tour
// on ajoute un nœud au graphe XML
$subnode = $node->addChild($attribute);
// appel récursif pour explorer le tableau [$value]
Utilitaires::getXmlForArrayOfAttributes($value, $subnode);
} else {
// on ajoute le nœud au graphe XML
$node->addChild("$attribute", htmlspecialchars("$value"));
}
}
}
}
|
Commentaires
- lignes 14-36 : nous introduisons la méthode statique [getXmlForArrayOfAttributes] qui rend la chaîne XML d’un tableau [arrayOfAttributes] passé en paramètre. Le second paramètre est la référence d’un nœud d’un graphe XML, de type [SimpleXmlElement]. Après exécution, ce nœud contient le graphe XML du tableau [arrayOfAttributes] ;
Nous écrivons le test [testXml.php] suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php
// dépendance
require __DIR__ . "/Utilitaires.php";
// tableau associatif
$array = ["nom" => "amédée", "prénom" => "sylvain", "âge" => 40,
"enfants" => [["nom" => "amédée", "prénom" => "béatrice", "âge" => 6],
["nom" => "amédée", "prénom" => "bertrand", "âge" => 4]]];
// xml
header("Content-Type: application/xml");
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><root></root>");
\Application\Utilitaires::getXmlForArrayOfAttributes($array, $node);
print $node->asXML();
|
Lorsqu’on exécute ce script [2], nous obtenons la chose suivante dans un navigateur Chrome :
Le script serveur¶
Le script serveur [impots-server.php] doit être modifié ainsi que son fichier de configuration [config-server.json] :
1 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 | {
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-11",
"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",
"/Utilities/Utilitaires.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
- la racine du projet est désormais le dossier de la version 11 ;
- ligne 16 : on inclut la nouvelle classe [Utilitaires] ;
Les modifications du script serveur sont les suivantes :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
…
// préparation de la réponse JSON du serveur
$response = new Response();
$response->headers->set("content-type", "application/xml");
$response->setCharset("utf-8");
…
// 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,
…
}
// 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 XML
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><réponse></réponse>");
Utilitaires::getXmlForArrayOfAttributes($result, $node);
$response->setContent($node->asXML());
// headers
$response->headers->add($headers);
// envoi
$response->send();
// log
if ($logger != NULL) {
// log en jSON
$log = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$logger->write("$log\n");
$logger->close();
}
// fermeture de la connexion [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
|
Commentaires
- ligne 12 : on indique que la réponse est de type [application/xml] ;
- lignes 29-59 : la réponse du serveur est désormais du XML ;
- ligne 41 : création du nœud racine [<réponse></réponse>] du graphe XML ;
- ligne 42 : ce graphe est complété avec le graphe XML du tableau [$result] des résultats à envoyer au client ;
- ligne 43 : le graphe XML est converti en chaîne XML pour envoi au client ;
Test
Directement dans un navigateur Chrome, on tape l’URL [http://localhost/php7/scripts-web/impots/version-11/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=60000]. On obtient le résultat suivant [1] dans un navigateur Chrome :
Le client¶
Nous nous intéressons maintenant à la partie client de l’application.
Cette architecture sera implémentée par les scripts suivants :
Dans la nouvelle version, seuls changent :
- le fichier de configuration [config-client.json] ;
- la couche [dao] du client ;
Le fichier de configuration [config-client.json] devient le suivant :
1 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-11",
"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-11/impots-server.php"
}
|
La couche [dao]¶
Le client [ClientDao.php] (ligne 13 ci-dessus) est modifié pour tenir compte du nouveau format de la réponse. On utilise [simpleXML] pour traiter celle-ci :
1 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 | <?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;
private $sessionCookie;
// 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 récupère la réponse XML
$réponse = $response->getContent(false);
$xml = new \SimpleXMLElement($réponse);
// logs
// print "$réponse\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
$message = \json_encode(["statut HTTP" => $statusCode, "réponse" => $xml], JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// on récupère le cookie de session
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// cookie de session ?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// on rend la réponse sous forme d'un tableau
return \json_decode(\json_encode($xml, JSON_UNESCAPED_UNICODE), true);
}
}
|
Commentaires
- lignes 26-27 : la réponse du serveur est lue. C’est un document XML [<réponse>…</réponse>]. Un objet [SimpleXMLElement] est construit à partir du document XML reçu ;
- lignes 33-37 : en cas d’erreur, le message de l’exception sera la chaîne jSON de la réponse du serveur plutôt que la chaîne XML recue. En effet, la chaîne jSON est plus concise ;
- ligne 53 : on rend le tableau des résultats en deux étapes :
- l’objet [$xml] de type [SimpleXMLElement] est passé en jSON ;
- on transforme la chaîne jSON obtenue en tableau associatif. C’est le résultat à rendre ;
Test
Si on lance le client avec un environnement correct (base de données, authentification, logs), on obtient les résultats habituels (vérifiez les fichiers [taxpayersdata.json, results.txt, errors.json]. Côté serveur, les logs sont eux les suivants :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 06/07/19 07:41:32:877 :
---nouvelle requête
06/07/19 07:41:32:882 : Autentification en cours…
06/07/19 07:41:32:883 : Authentification réussie [admin, admin]
06/07/19 07:41:32:883 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
06/07/19 07:41:32:908 : données fiscales prises en base de données
06/07/19 07:41:32:959 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
06/07/19 07:41:33:070 :
---nouvelle requête
06/07/19 07:41:33:077 : Authentification prise en session…
06/07/19 07:41:33:077 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
06/07/19 07:41:33:099 : données fiscales prises dans redis
06/07/19 07:41:33:100 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
06/07/19 07:41:33:189 :
---nouvelle requête
06/07/19 07:41:33:202 : Authentification prise en session…
06/07/19 07:41:33:202 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
06/07/19 07:41:33:233 : données fiscales prises dans redis
06/07/19 07:41:33:233 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
06/07/19 07:41:33:318 :
…
|
Tests [Codeception]¶
Le test [ClientMetierTest] est le suivant :
1 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 | <?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-11");
// 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);
…
// classe de test
class ClientMetierTest extends 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
…
}
|
Les résultats du test sont les suivants :
Exercice d’application – version 12¶
Nous allons dans ce chapitre écrire une application web respectant l’architecture MVC (Modèle-Vue-Contrôleur). L’application pourra délivrer ses réponses dans trois formats : jSON, XML, HTML. Il y a un saut de complexité entre ce que nous allons faire maintenant et ce qui a été fait précédemment. Nous allons réutiliser la plupart des concepts vus jusqu’à maintenant et nous allons détailler toutes les étapes menant à l’application finale.
Architecture MVC¶
Nous allons implémenter le modèle d’architecture dit MVC (Modèle – Vue – Contrôleur) de la façon suivante :
Le traitement d’une demande d’un client se déroulera de la façon suivante :
- 1 - demande
Les URL demandées seront de la forme http://machine:port/contexte/….?action=uneAction¶m1=v1¶m2=v2&… Le [Contrôleur principal] utilisera un fichier de configuration pour » router » la demande vers le bon contrôleur et la bonne action au sein de ce contrôleur. Pour cela, il utilisera le champ [action] de l’URL. Le reste de l’URL [param1=v1¶m2=v2&…] est formé de paramètres facultatifs qui seront transmis à l’action. Le C de MVC est ici la chaîne [Contrôleur principal, Contrôleur / Action]. Si aucun contrôleur ne peut traiter l’action demandée, le serveur web répondra que l’URL demandée n’a pas été trouvée.
- 2 - traitement
- l’action choisie [2a] peut exploiter les paramètres parami que le [Contrôleur principal] lui a transmis. Ceux-ci peuvent provenir de plusieurs sources :
- du chemin [/param1/param2/…] de l’URL,
- des paramètres [param1=v1¶m2=v2] de l’URL,
- de paramètres postés par le navigateur avec sa demande ;
- dans le traitement de la demande de l’utilisateur, l’action peut avoir besoin de la couche [métier] [2b]. Une fois la demande du client traitée, celle-ci peut appeler diverses réponses. Un exemple classique est :
- une réponse d’erreur si la demande n’a pu être traitée correctement ;
- une réponse de confirmation sinon ;
- le [Contrôleur / Action] rendra sa réponse [2c] au contrôleur principal ainsi qu’un code d’état. Ces codes d’état représenteront de façon unique l’état dans lequel se trouve l’application. Ce seront soit des codes de réussite, soit des codes d’erreur ;
- 3 - réponse
- selon que le client a demandé une réponse jSON, XML ou HTML, le [Contrôleur principal] instanciera [3a] le type de réponse appropriée et demandera à celle-ci d’envoyer la réponse au client. Le [Contrôleur principal] lui transmettra et la réponse et le code d’état fournis par le [Contrôleur / Action] qui a été exécuté ;
- si la réponse souhaitée est de type jSON ou XML, la réponse sélectionnée mettra en forme la réponse du [Contrôleur / Action] qu’on lui a donnée et l’enverra [3c]. Le client capable d’exploiter cette réponse peut être un script console PHP ou un script Javascript logé dans une page HTML ;
- si la réponse souhaitée est de type HTML, la réponse sélectionnée sélectionnera [3b] une des vues HTML [Vuei] à l’aide du code d’état qu’on lui a donné. C’est le V de MVC. A un code d’état correspond une unique vue. Cette vue V va afficher la réponse du [Contrôleur / Action] qui a été exécuté. Elle habille avec du HTML, CSS, Javascript les données de cette réponse. On appelle ces données le modèle de la vue. C’est le M de MVC. Le client est alors le plus souvent un navigateur ;
Maintenant, précisons le lien entre architecture web MVC et architecture en couches. Selon la définition qu’on donne au modèle, ces deux concepts sont liés ou non. Prenons une application web MVC à une couche :
Ci-dessus, les [Contrôleur / Action] intègrent chacun une partie des couches [métier] et [dao]. Dans la couche [web] on a bien une architecture MVC mais l’ensemble de l’application n’a pas une architecture en couches. Ici il n’y a qu’une couche qui fait tout.
Maintenant, considérons une architecture web multicouche :
La couche [web] peut être implémentée sans suivre le modèle MVC. On a bien alors une architecture multicouche mais la couche web n’implémente pas le modèle MVC.
Par exemple, dans le monde .NET la couche [web] ci-dessus peut être implémentée avec ASP.NET MVC et on a alors une architecture en couches avec une couche [web] de type MVC. Ceci fait, on peut remplacer cette couche ASP.NET MVC par une couche ASP.NET classique (WebForms) tout en gardant le reste (métier, DAO, Pilote) à l’identique. On a alors une architecture en couches avec une couche [web] qui n’est plus de type MVC.
Dans MVC, nous avons dit que le modèle M était celui de la vue V, c.a.d. l’ensemble des données affichées par la vue V. Une autre définition du modèle M de MVC est donnée :
Beaucoup d’auteurs considèrent que ce qui est à droite de la couche [web] forme le modèle M du MVC. Pour éviter les ambigüités on peut parler :
- du modèle du domaine lorsqu’on désigne tout ce qui est à droite de la couche [web] ;
- du modèle de la vue lorsqu’on désigne les données affichées par une vue V ;
Arborescence du projet Netbeans¶
Nous adopterons pour le projet Netbeans une architecture reflétant le modèle MVC :
- [3] : [main.php] est le contrôleur principal de notre modèle MVC. C’est le C de MVC ;
- [4] : le dossier [Controllers] contiendra les contrôleurs secondaires. Chacun traite une action particulière. Cette action est indiquée dans l’URL, par exemple […/main.php?action=authentifier-utilisateur]. Avec cette action, le [Contrôleur principal] [main.php] va sélectionner un [Contrôleur secondaire], ici [AuthentifierUtilisateurController] pour traiter l’action demandée. Ces contrôleurs font aussi partie du C de MVC ;
- [5] : le dossier [Model] contiendra les couches [métier] et [dao] de l’application. Selon les termes adoptés précédemment, ces éléments représentent le modèle du domaine et selon la terminologie adoptée pour le M peuvent représenter le M de MVC ;
- [6] : le dossier [Responses] contient les classes chargées
d’envoyer la réponse au client. Il y a une classe par type de réponse
souhaitée :
- [JsonResponse] : pour une réponse jSON ;
- [XmlResponse] : pour une réponse XML ;
- [HtmlResponse] : pour une réponse HTML ;
- [7] : le dossier [Views] contient les vues HTML lorsqu’une réponse HTML est souhaitée. C’est le V de MVC. Elles sont activées par la classe [HtmlResponse] qui leur transmet les données à afficher. Ces données sont le modèle de la vue. Selon la terminologie adoptée pour le M, ces données peuvent être le M de MVC ;
- [8] : le dossier [Utilities] contient des utilitaires :
- [Logger] : la classe qui permet de faire des logs dans un fichier texte ;
- [Sendmail] : la classe qui permet d’envoyer des mails ;
- [9] : le dossier [Logs] contient le fichier de logs [logs.txt] ;
- [10] : le dossier [Entities] contient des classes utilisées par les différents contrôleurs ;
A l’aide de cette arborescence, on peut décrire le cheminement du traitement d’une action demandée par un client :
- [main.php] [3] reçoit la demande ;
- après avoir fait quelques vérifications préliminaires (l’action fait-elle partie des actions acceptées ?), il transmet la demande au contrôleur secondaire [4] chargé de traiter cette action ;
- le contrôleur secondaire fait ce qu’il a à faire. Dans son travail, il peut avoir besoin des couches [métier] et [dao] [5] ainsi que des entités du dossier [10]. Il rend sa réponse au contrôleur principal [main.php] qui l’a activé ;
- selon le type de réponse [jSON, XML, HTML] souhaité par le client, le contrôleur principal [main.php] active l’une des réponses du dossier [Responses] [6] ;
- les réponses [JsonResponse, XmlResponse] envoient respectivement la réponse jSON ou XML au client ;
- la réponse [HtmlResponse] utilise l’une des vues du dossier [Views] [7] pour envoyer une réponse HTML au client ;
- les différents contrôleurs ont accès à la classe [Logger] du
dossier [8] pour écrire des logs dans le fichier des logs du
dossier [9]. Sont logués :
- l’action demandée ;
- la réponse de son contrôleur. Celle-ci est enregistrée au format jSON quelque soit le type [jSON, XML, HTML] demandé ;
- lors d’une erreur fatale (HTTP_INTERNAL_SERVER_ERROR), le contrôleur principal [main.php] envoie un mail à l’administrateur à l’aide de la classe [SendMail] du dossier [8] ;
Les actions de l’application¶
Le client transmet au serveur web l’action à exécuter sous la forme d’un paramètre [action] dans l’URL [/main.php?action=xxx]. Les actions autorisées sont listées dans le fichier [config.json] qui configure le contrôleur principal [main.php] :
1 2 3 4 5 6 7 8 9 10 | "actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
|
- ligne 1 : la clé [actions] du dictionnaire jSON ;
- lignes 3-9 : un dictionnaire [action:contrôleur]. A chaque action est associé le contrôleur secondaire chargé de la traiter ;
- ligne 3 : [init-session] : démarre une session de simulations de calculs d’impôts. Cette action indique le type de réponses souhaitées [jSON, XML, HTML] ;
- ligne 4 : une fois le type de session fixé, le client devra s’authentifier avec l’action [authentifier-utilisateur]. Tant qu’il n’est pas identifié, toutes les autres actions sont interdites à l’exception de [init-session] ;
- ligne 5 : une fois identifié, le client pourra faire une série de calculs d’impôt avec l’action [calculer-impot] ;
- ligne 6 : à tout moment, le client peut demander à voir la liste des simulations qu’il a faites avec l’action [lister-simulations] ;
- ligne 7 : il pourra en supprimer certaines avec l’action [supprimer-simulation] ;
- ligne 8 : le client termine sa session de simulations avec l’action [fin-session]. A partir de ce moment, il devra s’authentifier de nouveau s’il veut utiliser l’application ;
- ligne 9 : dans l’application HTML, l’action [afficher-calcul-impot] demande l’affichage du formulaire permettant le calcul de l’impôt ;
Configuration de l’application web¶
L’application est configurée par le fichier jSON [config.json] suivant :
1 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 | {
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/Simulation.php",
"/Entities/Database.php",
"/Entities/TaxAdminData.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Logger.php",
"/Utilities/SendAdminMail.php",
"/Model/InterfaceServerDao.php",
"/Model/ServerDao.php",
"/Model/ServerDaoWithSession.php",
"/Model/InterfaceServerMetier.php",
"/Model/ServerMetier.php",
"/Responses/InterfaceResponse.php",
"/Responses/ParentResponse.php",
"/Responses/JsonResponse.php",
"/Responses/XmlResponse.php",
"/Responses/HtmlResponse.php",
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.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": "Logs/logs.txt",
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
|
Commentaires
- ligne 2 : nom du fichier jSON contenant la configuration de l’accès à la base de données ;
- lignes 3-39 : configuration des dépendances du projet. On liste ici la totalité des scripts PHP de l’arborescence du projet ;
- lignes 40-44 : l’utilisateur autorisé à utiliser l’application ;
- lignes 46-54 : les coordonnées mail de l’administrateur de l’application ;
- ligne 55 : le chemin du fichier des logs ;
- lignes 56-65 : associations [action => contrôleur secondaire chargé de la traiter] ;
- lignes 66-70 : associations [type de réponse => classe Response chargée d’envoyer la réponse au client] ;
- lignes 71-75 : associations [vue HTML => tableau des codes d’état menant à cette vue] ;
- ligne 76 : la vue [vue-erreurs] est affichée dans une session
HTML à chaque fois qu’il se produit une erreur anormale :
- une application jSON ou XML est habituellement interrogée avec un client programmé. Celui-ci passe au serveur des paramètres qui peuvent absents ou erronés. L’ensemble des contrôleurs traitent ces cas et renvoient au client des codes d’erreur. Tous les cas d’erreur possibles doivent être traités ;
- avec une application HTML, c’est un peu différent. Utilisée
normalement, l’application web n’utilise qu’une partie des cas
d’utilisation possibles des clients jSON et XML. Prenons un
exemple : l’action [calculer-impot] attend trois paramètres
postés (envoyés par un POST) : [marié, enfants, salaire].
- si on a un client jSON permettant de taper des URL à la main, on peut demander l’action [calculer-impot] avec un GET plutôt qu’un POST, ou avec un POST sans aucun paramètre posté alors qu’il en faut trois, etc… Le serveur jSON doit traiter tous ces cas ;
- avec une application web, l’action [calculer-impot] sera demandée à partir d’un formulaire web où aucun des deux cas précédents ne sera possible : l’action [calculer-impot] sera demandée avec un POST et les trois paramètres [marié, enfants, salaire]. Certains de ces paramètres pourront avoir une valeur incorrecte mais ils seront présents. Cependant, l’utilisateur peut reproduire certaines erreurs en tapant lui-même des URL dans le navigateur. Par sécurité, on doit gérer ce cas ;
- la vue [vue-erreurs] sera affichée à chaque fois qu’un contrôleur secondaire rendra un code d’état incompatible avec l’application web, ç-à-d un code d’état non présent aux lignes 72-74 du fichier de configuration. Nous optons pour cette solution dans un souci pédagogique. Une autre option possible serait de ne rien faire et de se contenter de réafficher la vue actuellement affichée dans le navigateur du client pour que l’utilisateur ait l’impression que le serveur ne répond pas à ses URL fabriquées à la main ;
Installation d’outils et de bibliothèques¶
Postman¶
[Postman] est l’outil qui va nous permettre d’interroger les différentes URL de notre application web. Il nous permet :
- d’utiliser n’importe quelle URL : celles-ci sont fabriquées à la main ;
- de requêter le serveur web par un GET, POST, PUT, OPTIONS… ;
- de préciser les paramètres du GET ou du POST ;
- de fixer les entêtes HTTP de la requête ;
- de recevoir une réponse au format jSON, XML, HTML,
- d’avoir accès aux entêtes HTTP de la réponse. On a donc ainsi accès à la réponse HTTP complète du serveur ;
Puisque nous fabriquons à la main les URL interrogées, nous allons pouvoir tester tous les cas d’erreur possibles et voir comment le serveur réagit.
[Postman] est disponible à l’URL [https://www.getpostman.com/downloads/]. La version disponible en juin 2019 est la 7.2. Cette version présente une anomalie : lorsqu’on fait des requêtes successives au serveur web interrogé, le client [Postman 7.2] ne renvoie pas automatiquement les cookies que le serveur lui envoie, notamment le cookie de session. Pour maintenir la session, il faut alors recopier à la main le cookie de session dans les entêtes HTTP des requêtes successives. Ce n’est pas bien compliqué mais ce n’est pas pratique. C’est un bug qui n’existait pas dans les versions précédentes. Conscient du bug, l’équipe de [Postman] l’a corrigé dans une version alpha (peut être instable) appelée [Postman Canary] disponible à l’URL [https://www.getpostman.com/downloads/canary]. C’est cette version qui est utilisée ici. Nous allons décrire son installation. Si une version stable [Postman 7.3] ou ultérieure est disponible, vous pouvez la télécharger : le bug aura probablement été corrigé.
Procédez à l’installation de votre version de [Postman]. Au cours de l’installation, on vous demandera de créer un compte : celui-ci sera inutile ici. Le compte [Postman] sert à synchroniser différents appareils afin que la configuration de l’un soit répliqué sur un autre. Rien de tout ceci n’est utile ici.
Une fois installé, [Postman] présente l’interface suivante :
- en [2-3], on a accès au paramétrage du produit ;
en [6], la version utilisée dans ce document ;
si vous avez créé un compte, une synchronisation se fait entre votre poste et un serveur [Postman] distant. Cela est symbolisé par la roue [7] qui tourne à chaque fois que vous faites des modifications dans le projet [Postman]. Pour arrêter cette synchronisation inutile, déconnectez-vous en [8-9] ;
La bibliothèque Symfony / Serializer
Pour sérialiser des objets en jSON et XML, nous allons utiliser la bibliothèque [Symfony / Serializer]. Elle présente ici deux avantages :
- elle est homogène dans son utilisation pour sérialiser en jSON ou XML : cela évite d’apprendre deux bibliothèques aux API (Application Programming Interface) différentes ;
- nativement, elle sait sérialiser en jSON ou XML des objets, même si les attributs de ceux-ci sont privés. On se rappelle qu’en jSON, pour sérialiser un objet, il fallait que la classe de celui-ci implémente l’interface [JsonSerializable]. Le résultat obtenu alors était la chaîne jSON d’un tableau associatif ayant les attributs de la classe pour clés. Lorsqu’on désérialisait cette chaîne jSON, on retrouvait le tableau associatif primitif, qu’il fallait alors transformer en un objet de la classe qui avait été sérialisée. Avec [Symfony / Serializer], la désérialisation produit tout de suite un objet de la classe sérialisée. C’est plus simple ;
La documentation de la bibliothèque [Symfony / Serializer] est disponible à l’URL : [https://symfony.com/doc/current/components/serializer.html] (juin 2019).
Pour installer cette bibliothèque, ouvrez un terminal Laragon (cf paragraphe lien) et tapez la commande suivante :
- en [1], la commande d’installation de la bibliothèque [symfony/serializer] ;
- en [2], une autre bibliothèque nécessaire à notre projet : permet la sérialisation des objets ;
Les entités de l’application¶
Les entités [BaseEntity, Database, ExceptionImpots, TaxAdminData] ont été utilisées dès la version 08 du service web (cf paragraphe lien).
La classe [Simulation] va servir à encapsuler les éléments d’une simulation de calcul d’impôt :
1 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 | <?php
namespace Application;
class Simulation extends BaseEntity {
// attributs d'une simulation de calcul d'impôt
protected $marié;
protected $enfants;
protected $salaire;
protected $impôt;
protected $surcôte;
protected $décôte;
protected $réduction;
protected $taux;
// getters
public function getMarié() {
return $this->marié;
}
public function getEnfants() {
return $this->enfants;
}
public function getSalaire() {
return $this->salaire;
}
public function getImpôt() {
return $this->impôt;
}
public function getSurcôte() {
return $this->surcôte;
}
public function getDécôte() {
return $this->décôte;
}
public function getRéduction() {
return $this->réduction;
}
public function getTaux() {
return $this->taux;
}
}
|
Commentaires
- ligne 5 : la classe [Simulation] étend la classe [BaseEntity]
et hérite donc des méthodes :
- [setFromArrayOfAttributes($arrayOfAttributes)] : qui permet d’initialiser des attributs de la classe ;
- [__toString] : qui rend la chaîne jSON de l’objet ;
- lignes 7-14 : les attributs de la simulation ;
- lignes 16-47 : les getters de la classe ;
Les Utilitaires de l’application¶
La classe [Logger] permet de loguer des événements dans un fichier texte. Cette classe a été décrite au paragraphe lien.
La classe [SendAdminMail] permet d’envoyer un mail à l’administrateur de l’application. Cette classe a été décrite au paragraphe lien.
Les couches [métier] et [dao]¶
Les classes et interfaces des couches [métier] et [dao] sont rassemblées dans le dossier [Model]. Elles ont toutes été définies et utilisées dans des versions précédentes :
ExceptionImpots | La classe des exceptions lancées par la couche [dao]. Définie au paragraphe lien. |
---|---|
InterfaceServerDao | Interface implémentée par la couche [dao] du serveur. Définie au paragraphe lien. |
ServerDao | Implémentation de l’interface [InterfaceServerDao]. Implémente la couche [dao] du serveur. Définie au paragraphe lien. |
ServerDaoWithSession | Implémentation de l’interface [InterfaceServerDao]. Implémente la couche [dao] du serveur. Définie au paragraphe lien. |
InterfaceServerMetier | Interface implémentée par la couche [métier] du serveur. Définie au paragraphe lien. |
ServerMetier | Implémentation de l’interface [InterfaceMetier]. Implémente la couche [metier] du serveur. Définie au paragraphe lien. |
L’application en cours d’écriture utilise beaucoup d’éléments déjà présentés et utilisés :
- les couches [métier] et [dao] ;
- les utilitaires [Logger] et [SendAdminMail] ;
- les entités [ExceptionImpots, TaxAdminData, Database] ;
Nous allons nous concentrer sur la couche [web] de l’application :
Le contrôleur principal [main.php]¶
Introduction¶
- [1-2] : le contrôleur principal [main.php] [1] est configuré par le fichier [config.json] [2] ;
Rappelons la position du contrôleur principal dans notre architecture MVC :
En [1], le contrôleur principal [main.php] est le 1er élément de l’architecture MVC à traiter la requête du client. Il a plusieurs rôles :
il fait d’abord les vérifications de base :
est-ce que son fichier de configuration existe et est valide ;
chargement de toutes les dépendances du projet. Cela revient à charger tous les éléments de l’architecture MVC ;
est-ce que l’action demandée a été précisée ? Si oui, est-elle valide ?
si l’action demandée est valide, sélectionner [2a] le contrôleur secondaire qui va la traiter et lui passer les informations dont il a besoin : la requête HTTP, la session, la configuration de l’application ;
récupérer [2c] la réponse du contrôleur secondaire. Selon le type (jSON, XML, HTML) d’application demandé par le client, sélectionner [3a] la réponse (JsonResponse, XmlResponse, HtmlResponse) chargée d’envoyer la réponse au client et lui passer toutes les informations dont elle a besoin (la requête HTTP, la session, la configuration de l’application, la réponse du contrôleur secondaire) ;
une fois cette réponse envoyée [3c], procéder à la libération des ressources qui ont pu être mobilisées pour le traitement de la requête ;
[main.php] - 1
Le code du contrôleur principal [main.php] est le suivant :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// gestion des erreurs par PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// on récupère la configuration
$configFilename = "config.json";
$fileContents = \file_get_contents($configFilename);
$erreur = FALSE;
// erreur ?
if (!$fileContents) {
// on note l'erreur
$état = 131;
$erreur = TRUE;
$message = "Le fichier de configuration [$configFilename] n'existe pas";
}
if (!$erreur) {
// on récupère le code JSON du fichier de configuration dans un tableau associatif
$config = \json_decode($fileContents, true);
// erreur ?
if (!$config) {
// on note l'erreur
$erreur = TRUE;
$état = 132;
$message = "Le fichier de configuration [$configFilename] n'a pu être exploité correctement";
}
}
// erreur ?
if ($erreur) {
// préparation de la réponse JSON du serveur
// on ne peut pas s'aider du fichier de configuration
// dépendances symfony
require_once "C:/myprograms/laragon-lite/www/vendor/autoload.php";
// préparation réponse
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// code de statut
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
// contenu
$response->setContent(json_encode(["action" => "", "état" => $état, "réponse" => $message], JSON_UNESCAPED_UNICODE));
// envoi
$response->send();
// fin
exit;
}
…
|
Commentaires
lignes 10-12 : le contrôleur principal utilise les objets suivants de Symfony :
- [Request] : la requête HTTP en cours de traitement ;
- [Session] : la session de l’application web ;
- [Response] : la réponse HTTP au client ;
ligne 15 : pendant tout le développement on gardera cette ligne en commentaires : les erreurs PHP sont alors intégrées dans le flux texte envoyé au client. Si ce client est un navigateur, cela permet de voir les erreurs rencontrées par le serveur. C’est une aide au débogage ;
ligne 16 : toutes les erreurs sont signalées (E_ALL) sauf les avertissements (! E_WARNING) et les informations non fatales (! E_NOTICE). Par exemple, si un fichier ne peut être ouvert, PHP émet une erreur de type [E_NOTICE]. Si la ligne 15 permet l’affichage des erreurs, l’erreur d’ouverture du fichier apparaît dans le navigateur client. C’est bien si vous avez oublié de tester le résultat de l’ouverture du fichier, moins bien si vous avez prévu le test : une ligne de [notice] vient alors polluer la réponse du serveur au client. En phase de développement, la ligne 16 devrait elle-aussi être commentée : vous ne voulez rater aucune erreur ;
ligne 19 : le fichier de configuration est lu ;
lignes 22-27 : si cette lecture s’est mal passée, on note l’erreur (ligne 25), on met l’application dans l’état [131] et on prépare un message d’erreur ;
ligne 30 : on décode la chaîne jSON du fichier de configuration ;
lignes 32-37 : si ce décodage se passe mal, on note l’erreur (ligne 34), on met l’application dans l’état [132] et on prépare un message d’erreur ;
lignes 40-57 : en cas d’erreur de lecture du fichier de configuration, on ne peut plus avancer. On prépare alors une réponse jSON au client :
ligne 44 : comme le fichier de configuration n’a pas été lu, il faut importer à la main le fichier [autoload] nécessaire à [Symfony] ;
lignes 46-47 : on prépare une réponse jSON ;
ligne 50 : le code HTTP de la réponse sera 500 INTERNAL_SERVER_ERROR ;
ligne 52 : on fixe le contenu jSON de la réponse. Toutes les réponses faites par l’application web étudiée auront trois clés :
- [action] : l’action demandée par le client ;
- [état] : l’état de l’application après exécution de cette action ;
- [réponse] : la réponse du serveur web ;
ligne 54 : la réponse jSON est envoyée au client ;
Tests [Postman] - 1
Nous allons vérifier le comportement du serveur lorsque fichier de configuration est absent ou incorrect :
Nous allons rassembler les différentes requêtes que notre client [Postman] va émettre vers le serveur d’impôts dans des collections.
- en [1], créez une nouvelle collection ;
- en [2], donnez-lui un nom ;
- en [3], la description est facultative ;
- dans les collections [4], apparaît maintenant une collection nommée [impots-server-tests-version12] [5] ;
- en [6], on peut ajouter une nouvelle requête à la collection ;
- en [7], on donne un nom à la requête ;
- en [8], la description est facultative ;
- en [9-11], la requête ajoutée à la collection ;
- en [12], choix du type de la requête, ici une requête [GET]. En [19], les différents types de requête disponibles ;
- en [13], on tape ici l’URL du serveur ;
- en [14], on met ici les paramètres ajoutés à l’URL et qui seront donc des paramètres du GET. L’intérêt de les mettre ici plutôt que directement dans l’URL est qu’ils seront URL-encodés par [Postman]. Si vous les mettez vous-mêmes dans l’URL ce sera à vous de les URL-encoder ;
- en [15], [Authorization] sert à définir l’utilisateur qui va se connecter. Nous n’aurons pas à utiliser cette possibilité ;
- en [16], les entêtes HTTP qui accompagneront la requête. Un certain nombre d’entêtes sont automatiquement inclus dans la requête. Vous pouvez ici en ajouter de nouveaux ;
- en [17], [Body] désigne les paramètres d’une opération [POST]. Nous aurons à utiliser cette option ;
Nous allons faire le test suivant :
- dans [main.php], on indique que le fichier de configuration est [config2.json] qui n’existe pas :
- la ligne 16 du code doit être décommentée ;
- ligne 18 : l’erreur sur le nom du fichier de configuration ;
Entrons dans [Postman] [13, 20], l’URL du serveur web de calcul d’impôt et exécutons-la [21] :
La réponse renvoyée par le serveur (il faut bien sûr que Laragon soit actif) est la suivante :
- en [22], le serveur a renvoyé un code HTTP [500 Internal Server Error] ;
- en [23], [Body] désigne le corps de la réponse, ç-à-d le document envoyé par le serveur derrière les entêtes HTTP [28] ;
- en [26], on voit que [Postman] a reçu une réponse jSON ;
- en [27], la réponse jSON mise en forme ;
- en [28], la réponse jSON brute sans mise en forme ;
- en [29], le mode [Preview] est utilisé lorsque la réponse est du HTML. Le mode [Preview] affiche alors la page reçue ;
- en [30], la réponse jSON du serveur. C’est bien celle que nous attendions ;
En [25], les entêtes HTTP envoyés dans la réponse du serveur sont les suivants :
- en [32], le type jSON de la réponse ;
Ce premier test nous a permis de voir qu’on :
- peut envoyer tout type de requête au serveur testé ;
- peut fixer les paramètres du GET ou du POST ;
- a la totalité de la réponse : entêtes HTTP et le document qui suit ces entêtes [Body] ;
Maintenant, faisons un second test :
- en [1-3], le fichier [config3.json] est un fichier jSON syntaxiquement incorrect ;
- en [4], [main.php] est configuré pour utiliser [config3.json] ;
Nous ajoutons une nouvelle requête dans [Postman] :
- [1-3], on clique droit sur [2] et on prend l’option [duplicate] pour dupliquer la requête [2] ;
- en [4], la nouvelle requête a un nom prédéfini qu’on change en [5] ;
- en [6], la requête renommée ;
- en [9-10], on envoie la même requête GET que précédemment ;
- en [11], la réponse jSON du serveur ;
Nous avons montré ici comment allaient être testées les différentes actions du service web du calcul de l’impôt.
[main.php] – 2¶
Nous reprenons l’étude du code du contrôleur principal [main.php] :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// gestion des erreurs par PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// on récupère la configuration
$configFilename = "config.json";
…
// on inclut les dépendances nécessaires au script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require_once "$rootDirectory$dependency";
}
// dépendances absolues (bibliothèques tierces)
foreach ($config["absoluteDependencies"] as $dependency) {
require_once "$dependency";
}
// création du fichier des logs
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// on n'a pas pu créer le fichier de logs - internal server error
$état = 133;
(new JsonResponse())->send(
NULL, NULL, $config,
Response::HTTP_INTERNAL_SERVER_ERROR,
["action" => "non déterminée", "état" => $état, "réponse" => "Le fichier de logs [{$config['logsFilename']}] n'a pu être créé"],
[]);
// terminé
exit;
}
|
Commentaires
- ligne 18 : on a un fichier de configuration [config.json] désormais existant et syntaxiquement correct. Il faudrait de plus tester que les clés attendues dans ce fichier sont bien présentes. Nous considèrerons que cela fait partie du travail normal de débogage du développeur. Nous aurions pu faire ce même raisonnement pour les deux précédentes erreurs ;
- lignes 20-28 : on inclut toutes les dépendances nécessaires au projet web. Nous avons déjà rencontré ce code plusieurs fois ;
- ligne 31-43 : on essaie de créer l’objet [Logger] qui va nous permettre de loguer des événements dans le fichier [$config[“logsFilename”]]. Cette création peut échouer ;
- lignes 33-43 : gestion de l’erreur de création de l’objet [Logger] ;
- ligne 35 : on fixe un n° d’état ;
- lignes 36-40 : on envoie une réponse jSON ;
- ligne 42 : on arrête le script ;
Toutes les réponses envoyées au client implémentent l’interface [InterfaceResponse] suivante :
Le code de l’interface [InterfaceResponse] est le suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session : la session de l'application web
// array $config : la configuration de l'application
// int statusCode : le code HTTP de statut de la réponse
// array $content : la réponse du serveur
// array $headers : les entêtes HTTP à ajouter à la réponse
// Logger $logger : le logueur pour écrire des logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
|
- lignes 19-27 : l’interface [InterfaceResponse] a une unique méthode [send] pour envoyer la réponse au client ;
- lignes 11-17 : la signification des différents paramètres de la méthode [send] ;
- lignes 23-25 : les paramètres [$statusCode, $content, $headers] sont dans le résultat standard des contrôleurs secondaires de l’application. Cependant la réponse peut avoir besoin d’autres informations. Aussi lui donne-t-on les trois premiers paramètres (lignes 20-22) qui lui donnent accès à la totalité des informations concernant la requête, la session, la configuration ;
- ligne 26 : la réponse a besoin du [Logger] car elle va loguer la réponse envoyée au client ;
La classe [JsonResponse] implémente l’interface [InterfaceResponse] de la façon suivante :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session : la session de l'application web
// array $config : la configuration de l'application
// int statusCode : le code HTTP de statut de la réponse
// array $content : la réponse du serveur
// array $headers : les entêtes HTTP à ajouter à la réponse
// Logger $logger : le logueur pour écrire des logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// préparation sérialiseur symfony
$serializer = new Serializer(
[
// nécessaire pour la sérialisation d'objets
new ObjectNormalizer()],
// encodeur jSON
// pour les options, faire des OU entre les différentes options
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// sérialisation jSON
$json = $serializer->serialize($content, 'json');
// headers
$headers = array_merge($headers, ["content-type" => "application/json"]);
// envoi réponse
parent::sendResponse($statusCode, $json, $headers);
// log
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
|
Commentaires
- ligne 13 : la classe implémente l’interface [InterfaceResponse] ;
- ligne 13 : la classe étend la classe [ParentResponse]. Tous les types de [Response] étendent cette classe. C’est cette classe parent qui envoie la réponse au client (ligne 46). C’est parce que cet code était commun à tous les types de [Response] qu’il a été factorisé dans une classe parent ;
- lignes 33-40 : instanciation du sérialiseur [Symfony] qui va traduire la réponse du serveur [$content] en chaîne jSON (ligne 42) ;
- lignes 34-36 : le 1er paramètre du constructeur de [Serializer] est un tableau. Dans celui-ci, on met une instance de la classe [ObjectNormalizer] nécessaire à la sérialisation d’objets. Ce cas se présente dans cette application avec une liste de simulations où chaque simulation est une instance de la classe [Simulation] ;
- ligne 39 : le second paramètre du constructeur de [Serializer] est également un tableau : on y met tous les encodeurs utilisés dans une sérialisation (XML, jSON, CSV…) ;
- ligne 39 : il n’y aura qu’un encodeur ici, de type [JsonEncoder]. Le constructeur sans paramètres aurait pu être suffisant. Ici, nous avons passé un paramètre [JsonEncode] au constructeur, uniquement pour passer des options d’encodage jSON ;
- ligne 39 : le paramètre du constructeur [JsonEncode] est un tableau d’options. Ici on utilise l’option [JSON_UNESCAPED_UNICODE] pour demander que les caractères UTF-8 de la chaîne jSON soient rendus nativement et non pas « échappés » ;
- ligne 42 : le corps de la réponse HTTP est sérialisé en jSON grâce au sérialiseur précédent ;
- ligne 44 : on ajoute l’entête HTTP qui dit au client qu’on va lui envoyer du jSON ;
- ligne 46 : on demande à la classe parent d’envoyer la réponse au client ;
- lignes 48-50 : on logue la réponse jSON ;
Le code de la classe parent [ParentResponse] est le suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode : le code HTTP de statut de la réponse
// string $content : le corps de la réponse à envoyer
// selon les cas, c'est une chaîne jSON, XML, HTML
// array $headers : les entêtes HTTP à ajouter à la réponse
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// préparation de la réponse texte du serveur
$response = new Response();
$response->setCharset("utf-8");
// code de statut
$response->setStatusCode($statusCode);
// headers
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// on envoie la réponse
$response->setContent($content);
$response->send();
}
}
|
Commentaires
- lignes 10-13 : la signification des trois paramètres de la méthode [send] ;
- ligne 17 : on notera que le corps de la réponse est de type [string] et donc prêt à être envoyé (ligne 30) ;
- ligne 22 : la réponse aura des caractères UTF-8 ;
- ligne 24 : code de statut HTTP de la réponse ;
- lignes 26-28 : ajout des entêtes HTTP donnés par le code appelant ;
- lignes 30-31 : envoi de la réponse au client ;
Nous avons détaillé tout le cycle d’une réponse jSON. Nous ne reviendrons pas dessus dans la suite. Il faut simplement se rappeler la signature de l’interface [InterfaceResponse] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | interface InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session : la session de l'application web
// array $config : la configuration de l'application
// int statusCode : le code HTTP de statut de la réponse
// array $content : la réponse du serveur
// array $headers : les entêtes HTTP à ajouter à la réponse
// Logger $logger : le logueur pour écrire des logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
|
Le contrôleur principal [main.php] devra respecter cette signature à chaque fois qu’il demandera l’envoi de la réponse au client.
Tests [Postman] – 2¶
Nous modifions le fichier [config.json] de la façon suivante :
- en [1], nous indiquons que le fichier de logs est [Logs] qui est un dossier [2]. La création du fichier [Logs] devrait donc échouer ;
Nous créons une nouvelle requête [Postman] [3], appelée [erreur-133] :
[2-4] : nous définissons la même requête que dans les deux tests précédents ;
[5-7] : nous récupérons bien la réponse jSON attendue ;
[main.php] – 3
Continuons l’étude du contrôleur principal [main.php] :
1 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 | <?php
// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);
// espace de noms
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// gestion des erreurs par PHP
…
// création du fichier des logs
…
// 1er log
$logger->write("\n---nouvelle requête\n");
// requête courante
$request = Request::createFromGlobals();
// session
$session = new Session();
$session->start();
// liste d'erreurs
$erreurs = [];
$erreur = FALSE;
// on gère l'action demandée
if (!$request->query->has("action")) {
$erreurs[] = "paramètre [action] manquant";
$erreur = TRUE;
$état = 101;
$action = "";
} else {
// on mémorise l'action
$action = strtolower($request->query->get("action"));
}
// on logue l'action
$logger->write("action [$action] demandée\n");
// l'action existe-t-elle ?
if (!$erreur && !array_key_exists($action, $config["actions"])) {
$erreurs[] = "action [$action] invalide";
$erreur = TRUE;
$état = 102;
}
// le type de session doit être connu avant de faire certaines actions
if (!$erreur && !$session->has("type") && $action !== "init-session") {
$erreurs[] = "pas de session en cours. Commencer par action [init-session]";
$erreur = TRUE;
$état = 103;
}
// pour certaines actions on doit être authentifié
if (!$erreur && !$session->has("user") && $action !== "authentifier-utilisateur" && $action !== "init-session") {
$erreurs[] = "action demandée par utilisateur non authentifié";
$erreur = TRUE;
$état = 104;
}
// erreurs ?
if ($erreurs) {
// on prépare la réponse sans l'envoyer
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// on exécute l'action à l'aide de son contrôleur
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- on envoie la réponse
// cas de l'erreur fatale HTTP_INTERNAL_SERVER_ERROR
// on envoie un mail à l'administrateur si on peut
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// la réponse dépend du type de la session
if ($session->has("type")) {
// le type de session est dans la session
$type = $session->get("type");
} else {
// si pas de type dans session, alors par défaut ce sera une réponse en jSON
$type = "json";
}
// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
|
Commentaires
- une fois que les premières vérifications ont été faites et qu’il sait qu’il peut travailler, le contrôleur principal s’intéresse à l’action qu’on lui a demandée : elle doit remplir certaines conditions ;
- ligne 21 : on logue le fait qu’on a une nouvelle requête. On ne pouvait pas le faire avant car on n’était pas sûr d’avoir un fichier de logs valide ;
- ligne 23 : on encapsule toutes les informations de la requête du client dans l’objet Symfony [Request] ;
- ligne 26 : on démarre une nouvelle session où on récupère la session existante si elle existe ;
- ligne 27 : la session est activée ;
- ligne 29 : un tableau de messages d’erreur ;
- ligne 30 : un booléen qui au fil des tests nous dit si on a rencontré ou pas une erreur ;
- ligne 32 : le paramètre [action] doit faire partie de l’URL sous la forme [main.php?action=uneAction]. Le paramètre [action] fait alors partie des paramètres [$request→query] ;
- lignes 33-36 : cas de l’absence du paramètre [action] dans l’URL. L’erreur est notée et un état [101] lui est attribué ;
- ligne 39 : si le paramètre [action] est présent dans l’URL, il est mémorisé ;
- ligne 42 : le type de l’action est logué ;
- lignes 45-49 : si le paramètre [action] est présent, il doit alors être valide. Toutes les actions autorisées sont définies dans le tableau associatif [$config[« actions »]] ;
- lignes 46-48 : si l’action est invalide, l’erreur est notée et l’état [102] lui est attribué ;
- lignes 52-56 : on a une action valide. Elle doit encore remplir d’autres conditions. L’application web fournit trois types de réponse (jSON, XML, HTML). Ce type est fixé par l’action [init-session]. Cette action place le type de la session dans la clé [type] ;
- ligne 52 : en-dehors de l’action [init-session], tout autre action doit se dérouler avec une clé [type] dans la session ;
- lignes 53-55 : si ce n’est pas le cas, l’erreur est notée et l’état [103] lui est attribué ;
- lignes 58-63 : en-dehors des actions [init-session] et [authentifier-utilisateur], toutes les autres actions doivent se faire après authentification. Celle-ci se fait à l’aide de l’action [authentifier-utilisateur], qui si l’authentification réussit met une clé [user] dans la session ;
- ligne 59 : si l’action n’est ni [init-session] ni [authentifier-utilisateur] et que la clé [user] n’est pas dans la session, alors on a une erreur ;
- lignes 60-62 : on note l’erreur et lui attribue l’état [104] ;
- lignes 66-71 : on teste si le tableau [$erreurs] est non vide. Si c’est le cas, alors l’action demandée ou son contexte d’exécution sont erronés ;
- lignes 68-70 : on prépare la réponse à envoyer au client mais on ne l’envoie pas encore ;
- ligne 68 : code de statut HTTP ;
- ligne 69 : corps de la réponse ;
- ligne 70 : entêtes à ajouter à la réponse, aucun ici ;
- ligne 73 : on a une action valide. On va demander à son contrôleur (secondaire) de la traiter ;
- ligne 74 : on construit le nom de la classe du contrôleur à exécuter. [__NAMESPACE__] est l’espace de noms dans lequel on se trouve, ici [Application] (ligne 7) ;
- les noms des classes de contrôleur secondaire sont dans le fichier [config.json] :
1 2 3 4 5 6 7 8 9 10 | "actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
|
A chaque action correspond un contrôleur secondaire. Si l’action est [authentifier-utilisateur], la variable [$controller] de la ligne 74 aura donc la valeur [Application/AuthentifierUtilisateurController] ;
- ligne 75 : on logue le nom du contrôleur secondaire, pour vérification en cours de développement ;
- ligne 76 : le contrôleur secondaire est exécuté. Nous reviendrons sur les contrôleurs secondaires un peu plus loin ;
- ligne 76 : tous les contrôleurs secondaires rendent le même type de
résultat qui est un tableau :
- le 1er élement du tableau [$statusCode] est le code de statut HTTP de la réponse à envoyer ;
- le second élément [$état] est l’état de l’application après exécution du contrôleur ;
- le troisième élément [$content] est un tableau associatif avec l’unique clé [réponse] qui est le corps de la réponse à envoyer au client ;
- le quatrième élément [$headers] est un tableau d’entêtes HTTP à ajouter à la réponse envoyée au client ;
- ligne 79 : on arrive ici :
- soit parce qu’il y a eu erreur (lignes 68-70) ;
- soit après exécution d’un contrôleur (lignes 72-76) ;
- dans les deux cas, les éléments [$statusCode, $état, $content, $headers] nécessaires à l’élaboration de la réponse au client sont connus ;
- lignes 82-87 : traitent le cas particulier du code de statut [500 Internal Server Error]. Si un contrôleur a mis ce code de statut, c’est que l’application ne peut pas fonctionner. C’est par exemple le cas du calcul de l’impôt si le SGBD utilisé n’a pas été lancé ou ne répond plus. On envoie alors un mail à l’administrateur de l’application pour l’avertir. Nous ne commenterons pas particulièrement ce code. L’utilisation de la classe [SendAdminMail] a déjà été présentée (paragraphe lien) ;
- lignes 89-95 : on détermine le type [jSON, XML, HTML] de l’application web. Si l’action [init-session] a été exécutée avec succès, ce type est dans la session associé à la clé [type] (ligne 91). Si ce n’est pas le cas, alors on fixe arbitrairement un type pour la réponse, le type jSON (ligne 94) ;
- ligne 97 : [$content] est un tableau avec une unique clé
[réponse] et une unique valeur, le corps de la réponse à envoyer
au client. On lui ajoute les clés [action] et [état]. La clé
[action] permettra de mieux suivre les logs du fichier
[logs.txt]. La clé [état] aura deux rôles :
- elle permettra aux clients jSON et XML de connaître l’état dans lequel l’action exécutée a mis l’application web ;
- dans le cas d’une réponse HTML, elle permettra de choisir la vue HTML qu’il faut envoyer au navigateur client ;
- ligne 99 : on choisit le type de classe [Response] à exécuter pour envoyer la réponse au client ;
Nous avons déjà présenté la classe [JsonResponse] au paragraphe lien. Elle implémente l’interface [InterfaceResponse] et étend la classe [ParentResponse]. C’est le cas des deux autres classes [XmlResponse] et [HtmlResponse].
Les réponses sont rassemblées dans le dossier [Responses] :
Toutes ces classes implémentent l’interface [InterfaceResponse] présentée également au paragraphe lien :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session : la session de l'application web
// array $config : la configuration de l'application
// int statusCode : le code HTTP de statut de la réponse
// array $content : la réponse du serveur
// array $headers : les entêtes HTTP à ajouter à la réponse
// Logger $logger : le logueur pour écrire des logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
|
Cette interface a une unique méthode [send] chargée d’envoyer la réponse au client. Cette méthode a les 7 paramètres décrits aux lignes 11-17. Toutes les classes et interfaces du dossier [Responses] sont dans l’espace de noms [Application] (ligne 3).
Revenons au code de [main.php] :
1 2 3 4 5 6 7 8 9 10 | …
// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
|
- ligne 5 : on instancie la classe [Response] qui convient au type de l’application. Ces classes sont définies dans le fichier [config.json] de la façon suivante :
- « types »: {
- « json »: « \JsonResponse »,
- « html »: « \HtmlResponse »,
- « xml »: « \XmlResponse »
- },
ligne 5 : le nom de la classe est préfixée par son espace de noms ;
ligne 6 : la classe [Response] est instanciée et sa méthode [send] appelée avec les 7 paramètres qu’elle attend. Ces paramètres sont ceux de l’interface [InterfaceResponse] que tous les classes de réponse implémentent. Cela envoie la réponse au client ;
ligne 9 : on ferme le fichier de logs ;
ligne 10 : le contrôleur principal a terminé son travail ;
Tests [Postman] – 3
On va tester divers cas d’erreur du paramètre [action] de l’URL.
- en [1] :
- [erreur-101] : cas du paramètre [action] manquant dans l’URL ;
- [erreur-102] : cas du paramètre [action] présent dans l’URL mais non reconnu ;
- [erreur-103] : cas du paramètre [action] présent dans l’URL, reconnu mais sans que le type de réponse attendue [json, xml, html] ait été défini ;
Chaque requête est exécutée. Nous présentons directement les résultats obtenus :
Ci-dessus :
- en [2-4], une requête sans le paramètre [action] dans l’URL [4] ;
- en [5-7], le résultat jSON ;
Ci-dessus :
- en [5-9], une requête avec un paramètre [action] invalide ;
- en [10-13], la réponse jSON ;
Ci-dessus :
- en [14-19], une action reconnue mais le type (json, xml, html) n’a pas encore été précisé ;
- en [20-23], la réponse jSON du serveur ;
Les contrôleurs secondaires¶
Chaque action est exécutée par un des contrôleurs du dossier [Controllers] :
Dans l’architecture générale de l’application ci-dessus, les contrôleurs secondaires sont en [2a].
Chaque contrôleur implémente l’interface [InterfaceController] suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceController {
// $config est la configuration de l'application
// traitement d'une requête Request
// utile la session Session et peut la modifier
// $infos sont des informations supplémentaires propres à chaque contrôleur
// rend un tableau [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos=NULL): array;
}
|
Commentaires
- tous les contrôleurs secondaires sont exécutés via la méthode
[execute] de la ligne 17. On passe à cette méthode les
informations connues du contrôleur principa :
- ligne 18 : [array $config] qui encapsule la configuration de l’application ;
- ligne 19 : [Request $request] qui est la requête HTTP en cours de traitement ;
- ligne 20 : [Session $session] qui est la session courante de l’application web ;
- ligne 21 : [array $infos=NULL] qui est un tableau supplémentaire d’informations pour le contrôleur au cas où les trois premiers paramètres de la méthode ne suffiraient pas. Dans cette application, ce paramètre n’a jamais été utilisé. Il est là par prudence ;
- ligne 21 : la méthode [execute] rend le tableau [$statusCode,
$état, $content, $headers]
- [int $statusCode] : le code de statut de la réponse HTTP ;
- [int $état] : l’état dans lequel se trouve l’application à la fin de l’exécution ;
- [array $content] : un tableau associatif [réponse=>résultat] où [résultat] est de type quelconque : c’est le résultat produit par le contrôleur et qui sera envoyé au client, une fois ce résultat sérialisé sous la forme d’une chaîne de caractères ;
- [array $headers] : la liste des entêtes HTTP à incorporer à la réponse HTTP du serveur ;
Chaque contrôleur secondaire est appelé par le code suivant du contrôleur principal :
1 2 3 | // on exécute l'action à l'aide de son contrôleur
$controller = __NAMESPACE__ . $config["actions"][$action];
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
|
Ligne 3, on voit que le 4e paramètre [array $infos=NULL] de la méthode [execute] n’est pas utilisé.
Les actions¶
Nous passons maintenant en revue les différentes actions possibles du service web :
Action | Rôle | Contexte d’exécution |
---|---|---|
init-session | Sert à fixer le type (json, xml, html) des réponses souhaitées | Requête GET main.php?action=i nit-session&type=x peut être émise à tout moment |
auth entifier-utilisateur | Autorise ou non un utilisateur à se connecter | Requête POST ma in.php?action=authen tifier-utilisateur La requête doit avoir deux paramètres postés [user, password] Ne peut être émise que si le type de la session (json, xml, html) est connu |
calculer-impot | Fait une simulation de calcul d’impôt | Requête POST main.php?act ion=calculer-impot La requête doit avoir trois paramètres postés [marié, enfants, salaire] Ne peut être émise que si le type de la session (json, xml, html) est connu et l’utilisateur authentifié |
lister-simulations | Demande à voir la liste des simulations opérées depuis le début de la session | Requête GET main.php?action= lister-simulations La requête n’accepte aucun autre paramètre Ne peut être émise que si le type de la session (json, xml, html) est connu et l’utilisateur authentifié |
supprimer-simulation | Supprime une simulation de la liste des simulations | Requête GET main. php?action=lister-si mulations&numéro=x La requête n’accepte aucun autre paramètre Ne peut être émise que si le type de la session (json, xml, html) est connu et l’utilisateur authentifié |
fin-session | Termine la session de simulations. | Techniquement l’ancienne session web est supprimée et une nouvelle session est créée Ne peut être émise que si le type de la session (json, xml, html) est connu et l’utilisateur authentifié |
Tous les contrôleurs secondaires procèdent de la même façon :
- ils vérifient leurs paramètres. Ceux-ci sont trouvés dans l’objet [Request→query] pour les paramètres présents dans l’URL et dans l’objet [Request→request] pour ceux qui sont postés (requête POST) ;
- un contrôleur s’apparente à une foncion ou méthode qui vérifie la
validité de ses paramètres. Pour le contrôleur c’est cependant un peu
plus compliqué :
- les paramètres attendus peuvent être absents ;
- les paramètres attendus sont tous des chaînes de caractères, alors qu’une fonction peut fixer le type de ses paramètres. Si le paramètre attendu est un nombre, alors il faut vérifier que la chaîne du paramètre est bien celui d’un nombre ;
- une fois vérifié, que les paramètres attendus sont présents et syntaxiquement corrects, il faut vérifier qu’ils sont valides dans le contexte d’exécution du moment. Ce contexte est présent dans la session. L’exemple de l’authentification est un exemple de contexte d’exécution. Certaines actions ne doivent être traitées qu’une fois le client authentifié. Généralement, une clé dans la session indique si cette authentification a eu lieu ou pas ;
- une fois, les vérifications précédentes faites, le contrôleur secondaire peut travailler. Ce travail de vérification des paramètres est très important. On ne peut pas accepter qu’un client nous envoie n’importe quoi à n’importe quel moment de la vie de l’application. On doit contrôler totalement la vie de celle-ci ;
- une fois son travail fait, le contrôleur secondaire rend le tableau [$statusCode, $état, $content, $headers] attendu par le contrôleur principal qui l’a appelé ;
Nous allons maintenant passer en revue les différents contrôleurs ou ce qui revient au même les différentes actions qui rythment la vie de l’application web.
L’action [init-session]¶
L’action [init-session] est traitée par le contrôleur [InitSessionController] suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
class InitSessionController implements InterfaceController {
// $config est la configuration de l'application
// traitement d'une requête Request
// utile la session Session et peut la modifier
// $infos sont des informations supplémentaires propres à chaque contrôleur
// rend un tableau [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// on doit avoir un GET et un unique paramètre autre que [action]
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
if ($erreur) {
$état = 701;
$message = "méthode GET exigée avec paramètres [action, type] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// on récupère le paramètres du GET
$erreur = FALSE;
// type
if (!$request->query->has("type")) {
$erreur = TRUE;
$état = 702;
$message = "paramètre [type] manquant";
} else {
$type = strtolower($request->query->get("type"));
}
// vérification du type
if (!$erreur && !array_key_exists($type, $config["types"])) {
$erreur = TRUE;
$état = 703;
$message = "paramètre type [$type] invalide";
}
// erreur ?
if ($erreur) {
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// on met le type de session dans la session
$session->set("type", $type);
// message de réussite
$message = "session démarrée avec type [$type]";
$état = 700;
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
|
Commentaires
- on attend une requête [GET main.php?action=init-session&type=xxx]
- lignes 25-26 : on vérifie que la requête est une requête GET avec deux paramètres dans l’URL ;
- lignes 27-31 : si ce n’est pas le cas, on note l’erreur et on envoie un résultat [$statusCode, $état, $content, $headers] au contrôleur principal ;
- lignes 35-39 : on vérifie que le paramètre [type] est bien présent dans l’URL. Si ce n’est pas le cas, on note l’erreur ;
- ligne 40 : on note le type de la session ;
- lignes 43-47 : on vérifie que le type de la session est l’un des termes (json, xml, html). Si ce n’est pas le cas, on note l’erreur ;
- lignes 49-51 : s’il y a eu erreur, on envoie un résultat [$statusCode, $état, $content, $headers] au contrôleur principal ;
- ligne 53 : le type de la session est mis dans la session de l’application web ;
- lignes 55-57 : le contrôleur a fini son travail. On envoie un résultat [$statusCode, $état, $content, $headers] de succès au contrôleur principal ;
Rappelons ce que fait le contrôleur principal de la réponse des contrôleurs secondaires :
1 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 | // erreurs ?
if ($erreurs) {
// on prépare la réponse sans l'envoyer
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// on exécute l'action à l'aide de son contrôleur
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- on envoie la réponse
// cas de l'erreur fatale HTTP_INTERNAL_SERVER_ERROR
// on envoie un mail à l'administrateur si on peut
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// la réponse dépend du type de la session
if ($session->has("type")) {
// le type de session est dans la session
$type = $session->get("type");
} else {
// si pas de type dans session, alors par défaut ce sera une réponse en jSON
$type = "json";
}
// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
|
- ligne 12 : le contrôleur principal récupère le résultat du contrôleur secondaire ;
- lignes 35-36 : après quelques vérifications, il envoie la réponse en instanciant l’une des classes [JsonResponse, XmlResponse, HtmlResponse] selon le type (json, xml, html) de la session en cours ;
Dans la suite, nous ferons des tests [Postman] dans le cadre d’une session de simulations avec le type [json]. Le fonctionnement de la classe [JsonResponse] a été présenté au paragraphe lien.
Tests [Postman]¶
Ci-dessus :
- en [2], trois nouveaux tests ;
- en [3-7], l’action [init-session] avec le paramètre [type] manquant ;
- en [8-11], la réponse jSON du serveur ;
Ci-dessus :
- en [1-7], l’action [init-session] avec un paramètre [type] incorrect ;
- en [8-11], la réponse jSON du serveur ;
Ci-dessus :
en [1-8], l’action [init-session] avec le type jSON ;
en [9-12], la réponse jSON du serveur ;
L’action [authentifier-utilisateur]
L’action [authentifier-utilisateur] est exécutée par le contrôleur [AuthentifierUtilisateurController] suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class AuthentifierUtilisateurController implements InterfaceController {
// $config est la configuration de l'application
// traitement d'une requête Request
// utile la session Session et peut la modifier
// $infos sont des informations supplémentaires propres à chaque contrôleur
// rend un tableau [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// on doit avoir un POST et un unique paramètre GET
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
$état = 201;
$message = "méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user,password]";
// on rend le résultat au contrôleur principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// on récupère les paramètres du POST
$erreurs = [];
// user
$état = 210;
if (!$request->request->has("user")) {
$état += 2;
$erreurs[] = "paramètre [user] manquant";
} else {
$user = $request->request->get("user");
}
// password
if (!$request->request->has("password")) {
$état += 4;
$erreurs[] = "paramètre [password] manquant";
} else {
$password = trim($request->request->get("password"));
}
// erreur ?
if ($erreurs) {
// on rend le résultat au contrôleur principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// vérification des identifiants de l'utilisateur
// l'utilisateur existe-t-il ?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($user === $users[$i]["login"] && $users[$i]["passwd"] === $password);
$i++;
}
// trouvé ?
if (!$trouvé) {
// message d'erreur
$message = "Echec de l'authentification [$user, $password]";
$état = 221;
// on rend le résultat au contrôleur principal
return [Response::HTTP_UNAUTHORIZED, $état, ["réponse" => $message], []];
} else {
// on note dans la session qu'on a authentifié l'utilisateur
$session->set("user", TRUE);
// message de réussite
$message = "Authentification réussie [$user, $password]";
$état = 200;
// on rend le résultat au contrôleur principal
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
}
|
Commentaires
on attend une requête [POST main.php?action=authentifier-utilisateur] avec deux paramètres postés [user, password] ;
lignes 24-25 : on vérifie qu’on a une requête POST avec un unique paramètre dans l’URL ;
lignes 26-31 : si erreur il y a, on la note et on rend un résultat [$statusCode, $état, $content, $headers] au contrôleur principal ;
lignes 36-39 : on vérifie la présence du paramètre [user] dans les valeurs postées. S’il n’est pas présent, on note l’erreur ;
lignes 43-45 : on vérifie la présence du paramètre [password] dans les valeurs postées. S’il n’est pas présent, on note l’erreur ;
lignes 50-53 : si l’une des valeurs postées est manquante, un résultat [$statusCode, $état, $content, $headers] est rendu au contrôleur principal ;
lignes 56-62 : on vérifie que le couple [$user,$password] récupéré est présent dans le tableau [$config[‘users’]] du fichier de configuration ;
lignes 64-69 : si ce n’est pas le cas, l’erreur est notée. Le code de statut HTTP est mis à [Response::HTTP_UNAUTHORIZED] et le résultat [$statusCode, $état, $content, $headers] rendu au contrôleur principal ;
ligne 72 : l’authentification a réussi. On le note dans la session en plaçant dans celle-ci la clé [user]. C’est la présence de cette clé qui indique une authentification réussie ;
lignes 73-77 : on rend un résultat [$statusCode, $état, $content, $headers] de réussite au contrôleur principal ;
Tests [Postman]
Nous procédons aux tests [Postman] du contrôleur [AuthentifierUtilisateurController] en mode jSON ;
Ci-dessus :
- en [1-6], l’action [authentifier-utilisateur] avec un GET [2], alors qu’il faut un POST ;
- en [7-10], la réponse jSON du serveur ;
Remplaçons le GET par un POST [2] sans mettre de paramètres dans le corps de la réponse [7] :
Ci-dessus :
- en [1-7], le POST sans paramètres postés en [7] ;
- en [8-11], la réponse jSON du serveur ;
Ajoutons maintenant un paramètre [password] dans le corps (body) [4] de la requête :
Ci-dessus :
- en [1-6], un requête POST [2] avec un paramètre [password] posté [4-6]. Les paramètres postés doivent être ajoutés dans le corps (body) de la requête [4]. Il y a plusieurs façons de poster des valeurs au serveur. Nous choisissons la méthode [x-www-form-urlencoded] [5] ;
- en [8-10], la réponse jSON du serveur ;
Maintenant définissons le paramètre [user] sans le paramètre [password] :
Ci-dessus :
- en [1-7], une requête POST sans le paramètre [password] [4-7] ;
- en [8-11], la réponse jSON du serveur ;
Maintenant définissons les deux paramètres postés [user, password] mais avec des valeurs qui font que l’authentification échoue :
Ci-dessus :
- en [1-9], une requête POST avec des paramètres postés [user, password] incorrects ;
- en [10-13], la réponse jSON du serveur. On remarquera le code de statut [401 Unauthorized] [10] de la réponse ;
Maintenant une requête POST avec des identifiants valides :
Ci-dessus :
en [1-9], la requête POST [2] avec des identifiants valides [6-9] ;
en [10-13], la réponse jSON du serveur. On remarquera le code de statut HTTP [200 OK] en [10] ;
L’action [calculer-impot]
L’action [calculer-impot] est traitée par le contrôleur [CalculerImpotController] suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias de la couche [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class CalculerImpotController implements InterfaceController {
// $config est la configuration de l'application
// traitement d'une requête Request
// utile la session Session et peut la modifier
// $infos sont des informations supplémentaires propres à chaque contrôleur
// rend un tableau [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// on doit avoir un paramètre GET et trois paramètres POST
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
// on note l'erreur
$message = "il faut utiliser la méthode [post] avec [action] dans l'URL et les paramètres postés [marié, enfants, salaire]";
$état = 301;
// retour résultat au contrôleur principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// on récupère les paramètres du POST
$erreurs = [];
$état = 310;
// statut marital
if (!$request->request->has("marié")) {
$état += 2;
$erreurs[] = "paramètre [marié] manquant";
} else {
$marié = trim(strtolower($request->request->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
if ($erreur) {
$état += 4;
$erreurs[] = "valeur [$marié] invalide pour le paramètre [marié]";
}
}
// on récupère le nombre d'enfants
if (!$request->request->has("enfants")) {
$état += 8;
$erreurs[] = "paramètre [enfants] manquant";
} else {
$enfants = trim($request->request->get("enfants"));
$erreur = !preg_match("/^\d+$/", $enfants);
if ($erreur) {
$état += 9;
$erreurs[] = "valeur [$enfants] invalide pour le paramètre [enfants]";
}
}
// on récupère le salaire annuel
if (!$request->request->has("salaire")) {
$erreurs[] = "paramètre [salaire] manquant";
$état += 16;
} else {
$salaire = trim($request->request->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
if ($erreur) {
$état += 17;
$erreurs[] = "valeur [$salaire] invalide pour le paramètre [salaire]";
}
}
// erreur ?
if ($erreurs) {
// retour résultat au contrôleur principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// 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) {
// ça s'est mal passé
// retour résultat avec erreur au contrôleur principal
$état = 350;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// on a des paramètres valides
// création de la couche [dao]
if (!$redis->get("taxAdminData")) {
try {
// on va chercher les données fiscales en base
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// on met dans redis les données récupérées
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// ça s'est mal passé
// retour résultat avec erreur au contrôleur principal
$état = 340;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} 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);
}
// création de la couche [métier]
$métier = new ServerMetier($dao);
// on a tout ce qu'il faut pour travailler - calcul de l'impôt
$résultat = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// on ajoute dans la session la simulation qui vient d'être faite
$simulation = new Simulation();
$résultat = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $résultat;
$simulation->setFromArrayOfAttributes($résultat);
// existe-t-il une liste de simulations en session ?
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// ajout de la simulation à la liste de simulations
$simulations[] = $simulation;
// on remet les simulations en session
$session->set("simulations", $simulations);
// retour résultat au contrôleur principal
$état = 300;
return [Response::HTTP_OK, $état, ["réponse" => $résultat], []];
}
}
|
Commentaires
la requête attendue est [POST main.php?action=calculer-impot] avec trois paramètres postés [marié, enfants, salaire] :
- [marié] doit avoir sa valeur dans [oui, non] ;
- [enfants, salaire] doivent être des entiers positifs ou nuls ;
lignes 26-27 : on vérifie qu’on a bien un POST avec un unique paramètre dans l’URL ;
lignes 28-34 : si ce n’est pas le cas, un résultat d’erreur est envoyé au contrôleur principal ;
ligne 36 : on va cumuler les messages d’erreur dans le tableau [$erreurs] ;
lignes 39-41 : on vérifie la présence du paramètre [marié]. S’il n’est pas présent, l’erreur est notée ;
lignes 43-49 : on vérifie que [marié] a sa valeur dans [oui, non]. Si ce n’est pas le cas, l’erreur est notée ;
lignes 51-54 : on vérifie la présence du paramètre [enfants]. S’il n’est pas présent, l’erreur est notée ;
lignes 55-61 : on vérifie que la valeur du paramètre [enfants] est un nombre positif ou nul. Si ce n’est pas le cas, l’erreur est notée ;
lignes 63-66 : on vérifie la présence du paramètre [salaire]. S’il n’est pas présent, l’erreur est notée ;
lignes 67-72 : on vérifie que la valeur du paramètre [salaire] est un nombre positif ou nul. Si ce n’est pas le cas, l’erreur est notée ;
lignes 75-78 : si le tableau [$erreurs] n’est pas vide, c’est qu’il y a eu des erreurs. On met le tableau des erreurs dans la réponse et on rend le résultat au contrôleur principal ;
ligne 80 : on a des paramètres valides. On peut calculer l’impôt. Il faut pour cela construire les couches [dao] et [métier] qui savent faire ce calcul ;
lignes 82-94 : on crée un client [Redis] ;
lignes 88-94 : si on n’a pas pu se connecter au serveur [Redis], on envoie un code [500 Internal Server Error] au client ;
ligne 98 : on regarde si le serveur [Redis] a la clé [taxAdminData]. Cette clé représente les données de l’administration fiscale. Si la clé n’est pas présente, alors les données fiscales doivent être cherchées dans la base de données ;
ligne 101 : construction de la couche [dao] lorsque les données fiscales doivent être prises en base. La classe [ServerDaoWithRedis] a été décrite au paragraphe lien ;
ligne 103 : les données récupérées en base sont mises en mémoire [Redis] avec la clé [taxAdminData] ;
lignes 104-110 : si la recherche en base s’est mal passée, on note l’erreur renvoyée par la couche [dao] et on l’intègre au résultat renvoyé au contrôleur principal ;
ligne 109 : le message d’erreur renvoyé par la couche [PDO] est codé en [iso-8859-1]. On le code en [utf-8] ;
lignes 111-117 : si la clé [taxAdminData] existe dans la mémoire [Redis], alors les données fiscales sont passées directement au constructeur de la couche [dao] ;
ligne 119 : la couche [métier] est créée. La classe [ServerMetier] a été décrite au paragraphe lien ;
lignes 124-126 : avec le montant de l’impôt calculé, un objet [Simulation] est créé. La classe [Simulation] encapsule les données d’une simulation et a été décrite au paragraphe lien ;
lignes 128-132 : la simulation qui vient d’être construite doit être ajoutée à la liste des simulations déjà calculées. Cette liste se trouve en session sauf si aucune simulation n’a encore été faite ;
ligne 133-136 : la simulation est ajoutée à la liste des simulations et celle-ci est remise en session ;
lignes 137-139 : on rend le résultat au contrôleur principal ;
Tests [Postman]
Nous procédons aux tests [Postman] du contrôleur [CalculerImpotController] en mode jSON ;
Ci-dessus :
- en [1-7] on fait une requête [GET] au lieu de [POST] ;
- en [8-11], la réponse jSON du serveur ;
Maintenant, utilisons une méthode [POST], avec ou sans paramètres postés ainsi qu’avec des paramètres postés invalides :
Ci-dessus :
- on fait une requête [POST] [2] avec des paramètres postés [6-11] [marié, enfants, salaire] invalides. On peut ne pas poster l’un de ces paramètres en décochant sa case dans [16]. Cela vous permettra de tester différents cas de figure. Sur la copie d’écran ci-dessus, les trois paramètres sont présents et tous invalides ;
- en [12-15], la réponse jSON du serveur ;
Maintenant décochons deux des trois paramètres postés :
Ci-dessus,
- en [5-8], seul le paramètre [salaire] est posté et de plus il est invalide ;
- en [9-11], le résultat jSON du serveur ;
Maintenant faisons un calcul d’impôt avec des paramètres valides :
Ci-dessus :
en [1118], une demande avec des paramètres valides [6-8] ;
en [12-14], la réponse jSON du serveur ;
L’action [lister-simulations]
L’action [lister-simulations] est traitée par le contrôleur secondaire [ListerSimulationsController] suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class ListerSimulationsController {
// $config est la configuration de l'application
// traitement d'une requête Request
// utile la session Session et peut la modifier
// $infos sont des informations supplémentaires propres à chaque contrôleur
// rend un tableau [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// on doit avoir un unique paramètre GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
$état = 501;
$message = "GET requis, avec l'unique paramètre [action] dans l'URL";
// on rend un résultat avec erreur au contrôleur principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// on récupère la liste des simulations dans la session
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// on rend un résultat avec sucès au contrôleur principal
$état = 500;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
|
Commentaires
requête [GET main.php?action=lister-simulations] ;
lignes 24-25 : on vérifie qu’on a une requête GET avec un unique paramètre ;
lignes 26-31 : si ce n’est pas le cas, un résultat avec erreur est rendu au contrôleur principal ;
lignes 33-37 : on récupère la liste des simulations dans la session si elle s’y trouve (ligne 36), sinon cette liste est vide (ligne 34) ;
lignes 39-40 : on rend la liste des simulations au contrôleur principal ;
Tests [Postman]
Nous allons créer deux tests, un d’erreur et un réussi.
Ci-dessus :
- en [1-8], on fait une requête [GET] avec un paramètre [param1] en trop dans l’URL [3, 7-8] ;
- en [9-12], la réponse jSON du serveur ;
Maintenant faisons une requête valide :
Ci-dessus :
- en [1-5], une requête valide ;
Le résultat de la requête est le suivant :
en [3-6], la réponse jSON du serveur. Avant ce test, le test [Postman] [calculer-impot-300] avait été exécuté plusieurs fois pour créer des simulations dans la session web du serveur ;
L’action [supprimer-simulation]
L’action [supprimer-simulation] est traitée par le contrôleur secondaire [SupprimerSessionController] suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class SupprimerSimulationController {
/// $config est la configuration de l'application
// traitement d'une requête Request
// utile la session Session et peut la modifier
// $infos sont des informations supplémentaires propres à chaque contrôleur
// rend un tableau [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// on doit avoir deux paramètres GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
$état = 600;
if ($erreur) {
$état += 2;
$message = "GET requis, avec les paramètres [action, numéro]";
}
// le paramètre [numéro] doit exister
if (!$erreur) {
$état += 4;
$erreur = !$request->query->has("numéro");
if ($erreur) {
$message = "paramètre [numéro] manquant";
}
}
// le paramètre [numéro] doit être valide
if (!$erreur) {
$état += 8;
$numéro = $request->query->get("numéro");
$erreur = !preg_match("/^\d+$/", $numéro);
if ($erreur) {
$message = "paramètre [$numéro] invalide";
}
}
// le paramètre [numéro] doit être dans l'intervalle [0,n-1]
// si n est le nombre de simulations
if (!$erreur) {
$numéro = (int) $numéro;
$erreur = !$session->has("simulations");
if (!$erreur) {
$simulations = $session->get("simulations");
$erreur = $numéro < 0 || $numéro >= count($simulations);
}
if ($erreur) {
$état += 16;
$message = "la simulation n° [$numéro] n'existe pas";
}
}
// erreur ?
if ($erreur) {
// on rend le résultat au contrôleur principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// on supprime la simulation $numéro
unset($simulations[$numéro]);
$simulations = array_values($simulations);
// on remet les simulations dans la session
$session->set("simulations", $simulations);
// on rend la liste des simulations au client
$état = 600;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
|
Commentaires
requête [GET main.php?action=supprimer-simulation&numéro=x] ;
lignes 24-30 : on vérifie qu’on a une requête GET avec deux paramètres ;
lignes 32-38 : on vérifie que le paramètre [numéro] existe dans les paramètres de l’URL ;
lignes 40-47 : on vérifie que la valeur du paramètre [numéro] est syntaxiquement correcte ;
lignes 50-61 : on vérifie que la simulation n° [numéro] existe bien. Il y a deux cas d’erreur :
- la liste de simulations ne peut être trouvée dans la session (ligne 52) ;
- le n° [numéro] de la simulation à supprimer n’existe pas dans la liste des simulations ;
lignes 63-66 : en cas d’erreur, un résultat avec erreur est rendu au contrôleur principal ;
ligne 68 : la simulation n° [numéro] est supprimée ;
ligne 69 : l’opération [unset] ne change pas les index [0, n-1] de la liste. Pour les mettre à jour, on demande les valeurs du tableau [$simulations] pour éliminer la simulation manquante ;
ligne 71 : on remet le nouveau tableau des simulations dans la session ;
lignes 73-74 : on rend au contrôleur principal la nouvelle liste des simulations ;
Tests [Postman]
Nous allons faire des tests d’erreur et de succès :
Ci-dessus :
- en [1-6], une requête GET sans le paramètre [numéro] ;
- en [7-10], la réponse jSON du serveur ;
Maintenant une requête avec un n° syntaxiquement incorrect :
Ci-dessus :
- en [1-5], une requête GET avec un paramtre [numéro] invalide [3, 5] ;
- en [6-9], la réponse jSON du serveur ;
Maintenant une requête avec un n° de simulation qui n’existe pas :
Ci-dessus :
- en [1-5], une requête avec un n° de simulation égal à 100 qui n’existe pas dans la liste des simulations ;
- en [6-9], la réponse jSON du serveur ;
Maintenant, nous allons supprimer la simulation n° 0 de la liste, donc la première simulation. Tout d’abord redemandons cette liste avec la requête [lister-simulations-500] :
- en [1], il y a actuellement 2 simulations ;
On supprime la 1re simulation (numéro 0) :
Ci-dessus :
- en [1-5], on supprime la simulation n° 0 [5] ;
- en [6-9], la réponse jSON du serveur. On voit que la simulation n° 0 a été supprimée ;
Répétons cette opération :
Ci-dessus :
en [1], il ne reste plus de simulations dans la session web du serveur ;
L’action [fin-session]
L’action [fin-session] est traitée par le contrôleur secondaire [FinSessionController] suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class FinSessionController implements InterfaceController {
// $config est la configuration de l'application
// traitement d'une requête Request
// utile la session Session et peut la modifier
// $infos sont des informations supplémentaires propres à chaque contrôleur
// rend un tableau [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// on doit avoir un unique paramètre GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
// erreur ?
if ($erreur) {
$état = 401;
// résultat au contrôleur principal
$message = "GET requis avec le seul paramètre [action] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// on mémorise le type de session
$type = $session->get("type");
// on invalide la session courante
$session->invalidate();
// on remet le type dans la nouvelle session
$session->set("type", $type);
// envoi de la réponse
$état = 400;
// résultat au contrôleur principal
$content = ["réponse" => "session supprimée"];
return [Response::HTTP_OK, $état, $content, []];
}
}
|
Commentaires
requête [GET main.php?action=fin-session] ;
lignes 25-33 : on vérifie que l’action est un GET avec l’unique paramètre [fin-action] ;
ligne 38 : on invalide la session courante. Cela supprime les données enregistrées dans celle-ci et une nouvelle session est démarrée ;
ligne 36 : avant la fin de la session, on mémorise le type [json, xml, html] de celle-ci ;
ligne 40 : le type de la session précédente est replacé dans la nouvelle session. Finalement on repart avec une nouvelle session ayant l’uniqué clé [type] ;
lignes 44-45 : on rend le résultat au contrôleur principal ;
Tests [Postman]
Nous allons faire un test d’erreur et un test de réussite :
Ci-dessus :
- en [1-5], on demande la fin de session [5] avec un POST [2] au lieu du GET attendu ;
- en [6-9], la réponse jSON du serveur ;
Maintenant un exemple de réussite. Regardons tout d’abord le cookie de session échangé entre le client [Postman] et le serveur lors du dernier test réalisé :
Ci-dessus :
- en [3], le cookie de session envoyé par le client [Postman] au serveur ;
Regardons maintenant les entêtes HTTP envoyés par le serveur dans sa réponse :
Ci-dessus :
- en [3-4], le cookie de session n’est pas dans la réponse du serveur. C’est normal. Celui-ci ne l’envoie qu’une fois : au début d’une nouvelles session web ;
Maintenant exécutons une action [fin-session] valide :
Ci-dessus :
- en [1-3], une action [fin-session] valide ;
- en [4-7], la réponse jSON du serveur ;
Regardons les entêtes HTTP envoyés dans la réponse du serveur :
- en [3], le serveur envoie l’entête [Set-Cookie] montrant par là qu’une nouvelle session web démarre ;
Les types de réponse du serveur¶
Introduction¶
Revenons sur l’architecture générale de l’application :
Nous allons présenter les types de réponse possibles [3a]. Celles-ci sont rassemblées dans le dossier [Responses] du projet :
Nous avons déjà présenté la classe [JsonResponse] au paragraphe lien. Elle implémente l’interface [InterfaceResponse] et étend la classe [ParentResponse]. C’est le cas des deux autres classes [XmlResponse] et [HtmlResponse].
Rappelons la définition de l’interface [InterfaceResponse] :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session : la session de l'application web
// array $config : la configuration de l'application
// int statusCode : le code HTTP de statut de la réponse
// array $content : la réponse du serveur
// array $headers : les entêtes HTTP à ajouter à la réponse
// Logger $logger : le logueur pour écrire des logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
|
- lignes 19-27 : l’interface [InterfaceResponse] a une unique méthode [send] pour envoyer la réponse au client ;
- lignes 11-17 : la signification des différents paramètres de la méthode [send] ;
- lignes 23-25 : les paramètres [$statusCode, $content, $headers] sont la réponse standard des contrôleurs secondaires de l’application. Cependant la réponse peut avoir besoin d’autres informations. Aussi lui donne-t-on les trois premiers paramètres (lignes 20-22) qui lui donnent accès à la totalité des informations concernant la requête, la session, la configuration ;
- ligne 26 : la réponse a besoin du [Logger] car elle va loguer la réponse envoyée au client ;
Rappelons maintenant le code de la classe [ParentResponse], classe parent des trois types de réponse qui factorise ce qui leur est commun : l’envoi effectif d’une réponse texte au client :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode : le code HTTP de statut de la réponse
// string $content : le corps de la réponse à envoyer
// selon les cas, c'est une chaîne jSON, XML, HTML
// array $headers : les entêtes HTTP à ajouter à la réponse
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// préparation de la réponse texte du serveur
$response = new Response();
$response->setCharset("utf-8");
// code de statut
$response->setStatusCode($statusCode);
// headers
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// on envoie la réponse
$response->setContent($content);
$response->send();
}
}
|
Commentaires
- lignes 10-13 : la signification des trois paramètres de la méthode [send] ;
- ligne 17 : on notera que le corps de la réponse est de type [string] et donc prêt à être envoyé (ligne 30) ;
- ligne 22 : la réponse aura des caractères UTF-8 ;
- ligne 24 : code de statut HTTP de la réponse ;
- lignes 26-28 : ajout des entêtes HTTP donnés par le code appelant ;
- lignes 30-31 : envoi de la réponse au client ;
Rappelons enfin le code du contrôleur principal qui demande l’envoi de la réponse au client :
1 2 3 4 5 6 7 8 9 | // on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
|
ligne 4 : on fixe le nom de la classe [Response] à instancier ;
ligne 5 : on l’instancie et on envoie la réponse au client grâce à la méthode [send($request, $session, $config, $statusCode, $content, $headers, $logger)]. Parce qu’elles implémentent la même interface [InterfaceResponse], les méthodes [send] des différents types de réponse ont toutes la même signature ;
La classe [JsonResponse]
Elle a déjà présentée au paragraphe lien. Nous redonnons cependant son code pour mieux souligner l’homogénéité des trois classes de réponse :
La classe [JsonResponse] implémente l’interface [InterfaceResponse] de la façon suivante :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session : la session de l'application web
// array $config : la configuration de l'application
// int statusCode : le code HTTP de statut de la réponse
// array $content : la réponse du serveur
// array $headers : les entêtes HTTP à ajouter à la réponse
// Logger $logger : le logueur pour écrire des logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// préparation sérialiseur symfony
$serializer = new Serializer(
[
// nécessaire pour la sérialisation d'objets
new ObjectNormalizer()],
// encodeur jSON
// pour les options, faire des OU entre les différentes options
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// sérialisation jSON
$json = $serializer->serialize($content, 'json');
// headers
$headers = array_merge($headers, ["content-type" => "application/json"]);
// envoi réponse
parent::sendResponse($statusCode, $json, $headers);
// log
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
|
Commentaires
ligne 13 : la classe implémente l’interface [InterfaceResponse] ;
ligne 13 : la classe étend la classe [ParentResponse]. Tous les types de [Response] étendent cette classe. C’est cette classe parent qui envoie la réponse au client (ligne 46). C’est parce que ce code était commun à tous les types de [Response] qu’il a été factorisé dans une classe parent ;
lignes 33-40 : instanciation du sérialiseur [Symfony] qui va traduire la réponse du serveur [$content] en chaîne jSON (ligne 42) ;
lignes 34-36 : le 1er paramètre du constructeur de [Serializer] est un tableau. Dans celui-ci, on met une instance de la classe [ObjectNormalizer] nécessaire à la sérialisation d’objets. Ce cas se présente dans cette application avec une liste de simulations où chaque simulation est une instance de la classe [Simulation] ;
ligne 39 : le second paramètre du constructeur de [Serializer] est également un tableau : on y met tous les encodeurs utilisés dans une sérialisation (XML, jSON, CSV…) ;
ligne 39 : il n’y aura qu’un encodeur ici, de type [JsonEncoder]. Le constructeur sans paramètres aurait pu être suffisant. Ici, nous avons passé un paramètre [JsonEncode] au constructeur, uniquement pour passer des options d’encodage jSON ;
ligne 39 : le paramètre du constructeur [JsonEncode] est un tableau d’options. Ici on utilise l’option [JSON_UNESCAPED_UNICODE] pour demander que les caractères UTF-8 de la chaîne jSON soient rendus nativement et non pas « échappés » ;
ligne 42 : le corps de la réponse HHTP est sérialisé en jSON grâce au sérialiseur précédent ;
ligne 44 : on ajoute l’entête HTTP qui dit au client qu’on va lui envoyer du jSON ;
ligne 46 : on demande à la classe parent d’envoyer la réponse au client ;
lignes 48-50 : on logue la réponse jSON ;
La classe [XmlResponse]
La classe [XmlResponse] implémente l’interface [InterfaceResponse] de la façon suivante :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class XmlResponse extends ParentResponse implements InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session : la session de l'application web
// array $config : la configuration de l'application
// int statusCode : le code HTTP de statut de la réponse
// array $content : la réponse du serveur
// array $headers : les entêtes HTTP à ajouter à la réponse
// Logger $logger : le logueur pour écrire des logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// préparation sérialiseur symfony
$serializer = new Serializer(
// nécessaire pour la sérialisation d'objets
[new ObjectNormalizer()],
[
// sérialisation XML
new XmlEncoder(
[
XmlEncoder::ROOT_NODE_NAME => 'root',
XmlEncoder::ENCODING => 'utf-8'
]
),
// sérialisation jSON
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// sérialisation XML
$xml = $serializer->serialize($content, 'xml');
// headers
$headers = array_merge($headers, ["content-type" => "application/xml"]);
// envoi réponse
parent::sendResponse($statusCode, $xml, $headers);
// log
if ($logger !== NULL) {
// log en jSON
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
|
Commentaires
lignes 34-48 : instanciation d’un sérialiseur Symfony. Le constructeur admet deux paramètres de type tableau ;
ligne 36 : le 1er tableau comprend une instance de type [ObjectNormalizer] qui intervient dans la sérialisation d’objets ;
lignes 37-47 : le second tableau contient les encodeurs utilisés pour la sérialisation. On peut prévoir divers types de sérialisation avec le même sérialiseur ;
lignes 38-44 : l’encodeur XML ;
ligne 41 : on fixe la racine du code XML généré. Celui-ci aura la forme <root>[autres balises XML]</root> ;
ligne 42 : l’encodage utilisera des caractères UTF-8 ;
ligne 46 ; l’encodeur jSON. Celui-ci sera utilisé pour le log de la réponse dans le fichier [logs.txt] qui est fait en jSON ;
ligne 50 : le corps de la réponse envoyée au client est sérialisée en XML ;
ligne 52 : on ajoute aux entêtes reçus en paramètre (ligne 30) l’entête HTTP qui indique au client qu’on lui envoie un document XML ;
ligne 54 : envoi effectif de la réponse au client par la classe parent ;
lignes 56-60 : log en jSON de la réponse ;
Tests [Postman]
Nous avons déjà fait tous les tests d’erreurs possibles en jSON. Il n’y a rien à faire de plus en XML. Nous montrons deux exemples de réponse XML :
Ci-dessus :
- en [1-3], la requête de début de session XML ;
- en [4-7], la réponse XML du serveur ;
A partir de maintenant, toutes les réponses du serveur seront en XML. On peut reprendre toutes les requêtes déjà utilisées dans [Postman] sans les changer et on aura pour chacune d’elles une réponse XML. Faisons par exemple une authentification réussie :
Ci-dessus :
en [1-3], une demande d’authentification valide ;
en [4-7], la réponse XML du serveur ;
La réponse [HtmlResponse]
Lorsque le type de la session est [html] un objet de type [HtmlResponse] est instancié pour envoyer la réponse au client. Celle-ci va envoyer au client un flux HTML qui dépend du code d’état rendu par le contrôleur secondaire qui a traité l’action. Cette correspondance [état=>vue] est inscrite dans le fichier de configuration [config.json] de la façon suivante :
1 2 3 4 5 6 | "vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
|
Cette configuration se lit de la façon suivante : [‘nom de la vue’ => ‘états associés à cette vue’]
- ligne 2 : si le contrôleur secondaire a rendu un état du tableau [700, 221, 400], alors il faut afficher la vue [vue-authentification.php] ;
- ligne 3 : si le contrôleur secondaire a rendu un état du tableau [200, 300, 341, 350, 800], alors il faut afficher la vue [vue-calcul-impot.php] ;
- ligne 4 : si le contrôleur secondaire a rendu un état du tableau [500, 600], alors il faut afficher la vue [vue-liste-simulations.php] ;
- ligne 6 : si le contrôleur secondaire a rendu un état qui n’est dans aucun des tableaux précédents, alors il faut afficher la vue [vue-erreurs.php] ;
Les vues sont rassemblées dans le dossier [Views] du projet :
Le code de la classe [HtmlResponse] est le suivant :
1 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 | <?php
namespace Application;
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class HtmlResponse extends ParentResponse implements InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session : la session de l'application web
// array $config : la configuration de l'application
// int statusCode : le code HTTP de statut de la réponse
// array $content : la réponse du serveur
// array $headers : les entêtes HTTP à ajouter à la réponse
// Logger $logger : le logueur pour écrire des logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// préparation sérialiseur symfony
$serializer = new Serializer(
[
// pour la sérialisation des objets
new ObjectNormalizer()],
[
// pour la sérialisation jSON du log de la réponse
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// la réponse HTML dépend du code d'état rendu par le contrôleur
$état = $content["état"];
// à un état, correspond une vue - on cherche celle-ci dans la configuration de l'application
// la liste des vues
$vues = array_keys($config["vues"]);
$trouvé = false;
$i = 0;
// on parcourt la liste des vues
while (!$trouvé && $i < count($vues)) {
// états associés à la vue n° i
$états = $config["vues"][$vues[$i]];
// est-ce que l'état cherché se trouve dans les états associés à la vue n° I ?
if (in_array($état, $états)) {
// la vue affichée sera la vue n° i
$vueRéponse = $vues[$i];
$trouvé = true;
}
// vue suivante
$i++;
}
// trouvé ?
if (!$trouvé) {
// si aucune vue n'existe pour l'état actuel de l'application
// on rend la vue d'erreurs
$vueRéponse = $config["vue-erreurs"];
}
// on récupère la vue HTML à afficher dans une chaîne de caractères
ob_start();
require __DIR__ . "/../Views/$vueRéponse";
$html = ob_get_clean();
// on indique dans les entêtes qu'on va envoyer du HTML
$headers = array_merge($headers, ["content-type" => "text/html"]);
// la classe parent s'occupe de l'envoi effectif de la réponse
parent::sendResponse($statusCode, $html, $headers);
// log en jSON de la réponse sans le HTML
if ($logger !== NULL) {
// log en jSON de la réponse du contrôleur secondaire qui a traité l'action
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
|
Commentaires
lignes 32-41 : on instancie un sérialiseur Symfony. Celui-ci est nécessaire pour le log jSON de la réponse du contrôleur qui a traité l’action (lignes 72-82) ;
lignes 42-57 : on cherche dans la configuration de l’application, la vue qui doit être affichée. Celle-ci dépend du code d’état rendu par le contrôleur qui a traité l’action. Ce code est dans [$content[‘état’]] (ligne 43) ;
lignes 42-61 : on cherche la vue qui correspond à cet état ;
lignes 62-67 : si aucune vue n’a été trouvée alors on est dans une situation d’un code d’état anormal pour l’application HTML. On explicitera plus loin cette notion d’états anormaux. Dans ce cas, on affiche une vue d’erreur ;
lignes 68-70 : on interprète le code PHP de la vue sélectionnée et on met le résultat dans la variable [$html] (ligne 71) ;
ce code mérite quelques explications. Imaginons que la vue sélectionnée soit [vue-authentification.php] qui présente un formulaire web d’authentification :
- ligne 69 : la fonction [ob_start] démarre ce que la documentation appelle une temporisation de sortie. Tout ce qui est écrit par des opérations print, require… et qui normalement est immédiatement envoyé au client, va dans un tampon de sortie (ob=output buffer) sans être envoyé au client ;
- ligne 70 : on charge la vue [vue-authentification.php] qui est
une vue HTML dynamique contenant du code PHP. Se passent alors
deux choses :
- le code PHP de la vue [vue-authentification.php] est chargé et interprété. Le résultat est une vue qu’on appellera [vue-authentification.html] qui ne contient que du code HTML, voire CSS et Javascript mais plus de PHP ;
- ce code HTML est normalement envoyé au client. C’est en fait le cas de tout texte rencontré par l’interpréteur PHP et qui n’est pas du code PHP. A cause de la temporisation de sortie, ce code HTML est mis dans le tampon de sortie sans être envoyé au client ;
- ligne 71 : la fonction [ob_get_clean] fait deux choses :
- elle met dans la variable [$html] le contenu du buffer de sortie, donc la page [vue-authentification.html] qu’on y a mise ;
- elle vide le buffer de sortie. Pour celui-ci, tout se passe comme si rien ne s’était produit. Par ailleurs, le client n’a toujours rien reçu ;
ligne 70 : on est ici en cours d’exécution de la classe [HtmlResponse] qui se trouve dans le dossier [Responses]. Pour touver la vue, il faut donc remonter d’un niveau [..] puis passer dans le dossier [Views]. [__DIR__] est le nom absolu du dossier dans lequel se trouve le script en cours d’exécution, dans notre exemple le dossier [C:/myprograms/laragon-lite/www/php7/scripts-web/impots/13/Responses] ;
ligne 73 : on ajoute aux entêtes HTTP reçus en paramètre (ligne 29), l’entête qui dit au client qu’on va lui envoyer du HTML ;
ligne 75 : on demande à la classe parent de procéder à l’envoi effectif de la réponse au client ;
lignes 77-81 : on logue en jSON la réponse [$content] fournie par le contrôleur secondaire qui a traité l’action en cours ;
Tests [Postman]
Pour tester véritablement le mode HTML de la session, il nous faudrait passer en revue toutes les vues. Nous allons le faire ultérieurement. Nous allons faire le test suivant :
Regardons la liste des vues dans le fichier de configuration :
1 2 3 4 5 6 | "vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
|
On peut trouver le contexte produisant certains des codes d’état ci-dessus en regardant les tests [Postman] réalisés :
On voit que le code d’état [700] est celui d’une action [init-session] réussie [2]. Ci-dessus, on a une réponse jSON mais elle peut être de type XML ou HTML. C’est ce dernier cas qui va être testé. D’après le fichier de configuration, c’est la vue [vue-authentification.php] qui constitue la réponse HTML. Vérifions.
Ci-dessus :
- en [1-3], on initialise une session HTML. On attend donc une réponse HTML ;
- en [4-8], la réponse HTML du serveur ;
- l’onglet [8] permet d’avoir une prévisualisation du code HTML reçu ;
- en [8-9], un aperçu de la vue HTML ;
L’application web HTML¶
Présentation des vues¶
L’application web HTML utilisera quatre vues :
La vue d’authentification :
La vue de calcul de l’impôt :
La vue de la liste des simulations :
La vue des erreurs inattendues :
Nous allons décrire ces vues l’une après l’autre.
La vue d’authentification
Présentation de la vue
La vue d’authentification est la suivante :
La vue est composée de deux éléments qu’on appellera des fragments :
- le fragment [1] est généré par un script [v-bandeau.php] ;
- le fragment [2] est généré par un script [v-authentification.php] ;
La vue d’authentification est générée par la page [vue-authentification.php] suivante :
1 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
// données de test de la page
// on encapsule les données de la pagé dans $page
…
?>
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- bandeau sur 1 ligne et 12 colonnes -->
<?php require "v-bandeau.php"; ?>
<!-- formulaire d'authentification sur 9 colonnes -->
<div class="row">
<div class="col-md-9">
<?php require "v-authentification.php" ?>
</div>
</div>
<?php
// si erreur - on affiche une alerte d'erreur
if ($modèle->error) {
print <<<EOT
<div class="row">
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
Les erreurs suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT;
}
?>
</div>
</body>
</html>
|
Commentaires
- ligne 7 : un document HTML commence avec cette ligne ;
- lignes 8-44 : la page HTML est encapsulée dans les balises <html> </html> ;
- lignes 9-16 : entête (head) du document HTML ;
- ligne 11 : la balise <meta charset> indique que le document est codé en UTF-8 ;
- ligne 12 : la balise <meta name=’viewport’> fixe l’affichage initial de la vue : sur toute la largeur de l’écran qui l’affiche (width) à sa taille initiale (initial-scale) sans redimensionnement pour s’adapter à une taille plus petite d’écran (shrink-to-fit) ;
- ligne 14 : la balise <link rel=’stylesheet’> fixe le fichier CSS qui gouverne l’apparence de la vue. Nous utilisons ici le framework CSS Bootstrap 4.1.3 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
- ligne 15 : la balise <title> fixe le titre de la page :
- lignes 17-43 : le corps de la page web est encapsulé dans les balises <body></body> ;
- lignes 18-42 : la balise <div> délimite une section de la page affichée. Les attributs [class] utilisés dans la vue se réfèrent tous au framework CSS Bootstrap. La balise <div class=’container’> délimite un conteneur Bootstrap ;
- ligne 20 : on inclut le script [v-bandeau.php]. Ce script génère le bandeau [1] de la page. Nous le décrirons bientôt ;
- lignes 22-26 : la balise <div class=’row’> délimite une ligne Bootstrap. Ces lignes sont constituées de 12 colonnes ;
- ligne 23 : la balise <div class=’col-md-9’> délimite une section de 9 colonnes ;
- ligne 24 : on inclut le script [v-authentification.php] qui affiche le formulaire d’authentification [2] de la page. Nous le décrirons bientôt ;
- ligne 27 : la balise <?php introduit du code PHP à l’intérieur de la page HTML. Ce code est exécuté avant l’affichage de la page HTML et peut modifier celle-ci ;
- ligne 29 : la totalité des données dynamique de la vue affichée sera encapsulée dans un objet [$modèle] de type [stdClass]. C’est un choix arbitraire. On aurait pu choisir un tableau associatif à la place pour le même résultat ;
- ligne 29 : l’authentification échoue si l’utilisateur entre des identifiants incorrects. Dans ce cas, la vue d’authentification est réaffichée avec un message d’erreur. L’attribut [$modèle→error] indique s’il faut afficher ce message d’erreur ;
- lignes 30-39 : cette syntaxe écrit tout le texte placé entre les symbole PHP <<<EOT (ligne 30 – on peut mettre ce qu’on veut à la place de EOT=End Of Text) et le symbole EOT de la ligne 39 (doit être identique au symbole utilisé ligne 30). Le symbole doit être écrit dans la 1re colonne de la ligne 39. Les variables PHP situées dans le texte entre les deux symboles EOT sont interprétées ;
- lignes 33-36 : délimitent une zone à fond rose (class= »alert alert-danger ») (ligne 33) ;
- ligne 34 : un texte ;
- ligne 35 : la balise HTML <ul> (unordered list) affiche une liste à puces. Chaque élément de la liste doit avoir la syntaxe <li>élément</li> ;
Retenons de ce code les éléments dynamiques à définir :
[$modèle→error] : pour afficher un message d’erreur ;
[$modèle→erreurs] : une liste (au sens HTML du terme) de messages d’erreur ;
Le fragment [v-bandeau.php]
Le fragment [v-bandeau.php] affiche le bandeau supérieur de toutes les vues de l’application web :
Le code du fragment [v-bandeau.php] est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 | <!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
|
Commentaires
- lignes 2-13 : le bandeau est encapsulé dans une section Bootstrap de type Jumbotron [<div class= »jumbotron »>]. Cette classe Bootstrap stylise d’une façon particulière le contenu affiché pour le faire ressortir ;
- lignes 3-12 : une ligne Bootstrap ;
- lignes 4-6 : une image [img] est placée dans les quatre premières colonnes de la ligne ;
- ligne 5 : la syntaxe [<?= $logo ?>] est équivalente à la syntaxe [<?php print $logo ?>]. Dit autrement la valeur de l’attribut [src] sera la valeur de la variable PHP [$logo] ;
- lignes 7-11 : les 8 autres colonnes de la ligne (on rappelle qu’il y en a 12 au total) serviront à placer un texte (ligne 9) en gros caractères (<h1>, lignes 8-10) ;
Eléments dynamiques :
[$logo] : URL de l’image affichée dans le bandeau ;
Le fragment [v-authentification.php]
Le fragment [v-authentification .php] affiche le formulaire d’authentification de l’application web :
Le code du fragment [v-authentification.php] est le suivant :
1 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 | <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
<form method="post" action="main.php?action=authentifier-utilisateur">
<!-- titre -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
<!-- formulaire Bootstrap -->
<fieldset class="form-group">
<!-- 1re ligne -->
<div class="form-group row">
<!-- libellé -->
<label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
<div class="col-md-4">
<!-- zone de saisie texte -->
<input type="text" class="form-control" id="user" name="user"
placeholder="Nom d'utilisateur" value="<?= $modèle->login ?>">
</div>
</div>
<!-- 2e ligne -->
<div class="form-group row">
<!-- libellé -->
<label for="password" class="col-md-3 col-form-label">Mot de passe</label>
<!-- zone de saisie texte -->
<div class="col-md-4">
<input type="password" class="form-control" id="password" name="password"
placeholder="Mot de passe">
</div>
</div>
<!-- bouton de type [submit] sur une 3e ligne-->
<div class="form-group row">
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
|
Commentaires
- lignes 2-39 : la balise <form> délimite un formulaire HTML. Celui-ci
a en général les caractéristiques suivantes :
- il définit des zones de saisies (balises <input> des lignes 17 et 27 ;
- il a un bouton de type [submit] (ligne 34) qui envoie les valeurs saisies à l’URL indiqué dans l’attribut [action] de la balise [form] (ligne 2). La méthode HTTP utilisée pour requêter cette URL est précisée dans l’attribut [method] de la balise [form] (ligne 2) ;
- ici, lorsque l’utilisateur va cliquer sur le bouton [Valider] (ligne 34), le navigateur va poster (ligne 2) les valeurs saisies dans le formulaire à l’URL [main.php?action=authentifier-utilisateur] (ligne 2) ;
- les valeurs postées sont les valeurs saisies par l’utilisateur dans les zones de saisie des lignes 17 et 27. Elles seront postées sous la forme [user=xx&password=yy]. Les noms des paramètres [user, password] sont ceux des attributs [name] des zones de saisie des lignes 17 et 27 ;
- ligne 5-7 : une section Bootstrap pour afficher un titre dans un fond bleu :
- lignes 10-37 : un formulaire Bootstrap. Tous les éléments du formulaire vont alors être stylisés d’une certaine façon ;
- lignes 12-20 : définissent la 1re ligne du formulaire :
- la ligne 14 définit le libellé [1] sur trois colonnes. L’attribut [for] de la balise [label] relie le libellé à l’attribut [id] de la zone de saisie de la ligne 17 ;
- lignes 15-19 : met la zone de saisie dans un ensemble de quatre colonnes ;
- ligne 17 : la balise HTML [input] décrit une zone de saisie. Elle
a plusieurs paramètres :
- [type=’text’] : c’est une zone de saisie texte. On peut y taper n’importe quoi ;
- [class=’form-control’] : style Bootstrap pour la zone de saisie ;
- [id=’user’] : identifiant de la zone de saisie. Cet identifiant est en général utilisé par le CSS et le code Javascript ;
- [name=’user’] : nom de la zone de saisie. C’est sous ce nom que la valeur saisie par l’utilisateur sera postée par le navigateur [user=xx] ;
- [placeholder=’invite’] : le texte affiché dans la zone de saisie lorsque l’utilisateur n’a encore rien tapé ;
- [value=’valeur’] : le texte ‘valeur’ sera affiché dans la zone de saisie dès que celle-ci sera affichée, avant donc que l’utilisateur ne saisisse autre chose. Ce mécanisme est utilisé en cas d’erreur pour afficher la saisie qui a provoqué l’erreur. Ici cette valeur sera la valeur de la variable PHP [$modèle→login] ;
- lignes 21-30 : un code analogue pour la saisie du mot de passe ;
- ligne 27 : [type=’password’] fait qu’on a une zone de saisie texte (on peut taper n’importe quoi) mais les caractètres tapés sont cachés :
- lignes 32-36 : une 3e ligne pour le bouton [Valider] ;
- ligne 34 : parce qu’il a l’attribut [type=submit], un clic sur ce bouton déclenche l’envoi au serveur par le navigateur des valeurs saisies comme il a été expliqué précédemment. L’attribut CSS [class= »btn btn-primary »] affiche un bouton bleu :
Il nous reste à expliquer une dernière chose. Ligne 2, l’attribut [action= »main.php?action=authentifier-utilisateur »] définit une URL incomplète (elle ne commence pas par http://machine:port/chemin). Dans notre exemple, toutes les URL de l’application sont de la forme [http://localhost/php7/scripts-web/impots/version-12/main.php?action=xx]. La vue de l’authentification va être obtenue avec diverses URL :
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html];
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur]
Ces URL désignent un document [main.php] dans le chemin [http://localhost/php7/scripts-web/impots/version-12]. Ce sera le cas de toutes les URL de cette application. Le paramètre [action= »main.php?action=authentifier-utilisateur »] sera préfixé par ce chemin au moment de l’envoi des valeurs saisies. Celles-ci seront donc postées à l’URL [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur].
Tests visuels¶
On peut procéder aux tests des vues bien avant leur intégration dans l’application. Il s’agit ici de tester leur aspect visuel. Nous rassemblerons toutes les vues de test dans le dossier [Tests] du projet :
Pour tester la vue [vue-authentification.php], il nous faut créer le modèle de données qu’elle va afficher :
1 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 | <?php
// données de test de la page
//
// on calcule le modèle de la vue
$modèle = getModelForThisView();
function getModelForThisView(): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
// identifiant utilisateur
$modèle->login = "albert";
// liste d'erreurs
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// on construit une liste HTML des erreurs
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// image du bandeau
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// on rend le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
…
</head>
<body>
….
</body>
</html>
|
Commentaires
- lignes 1-5 : la vue d’authentification a des parties dynamiques contrôlées par l’objet [$modèle]. On appelle cet objet le modèle de la vue. Selon l’une des deux définitions données pour le sigle MVC, on a là le M du MVC ;
- ligne 5 : le modèle de la vue est calculé par la fonction [getModelForThisView] ;
- ligne 9 : le modèle de la vue sera encapsulé dans un type [stdClass] ;
- lignes 10-22 : on définit des valeurs de tests pour les éléments dynamiques de la vue d’authentification ;
Le test visuel peut se faire à partir de Netbeans :
On poursuit ces tests visuels jusqu’à être satisfait du résultat.
Calcul du modèle de la vue¶
Une fois l’aspect visuel de la vue déterminé, on peut procéder au calcul du modèle de la vue en conditions réelles. Rappelons les codes d’état qui mènent à cette vue. On les trouve dans le fichier de configuration :
1 2 3 4 5 6 | "vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
|
Ce sont donc les codes d’état [700, 221, 400] qui font afficher la vue d’authentifcation. Pour retrouver la signification de ces codes, on peut s’aider des tests [Postman] réalisés sur l’application jSON :
- [init-session-json-700] : 700 est le code d’état à l’issue d’une action [init-session] réussie : on présente alors le formulaire d’authentification vide ;
- [authentifier-utilisateur-221] : 221 est le code d’état à l’issue d’une action [authentifier-utilisateur] ayant échoué (identifiants non reconnus) : on présente alors le formulaire d’authentification pour qu’il soit corrigé ;
- [fin-session-400] : 400 est le code d’état à l’issue d’une action [fin-session] réussie : on présente alors le formulaire d’authentification vide ;
Maintenant que nous savons à quels moments doit être affiché le formulaire d’authentification, on peut calculer son modèle dans [vue-authentification.php] :
Le code de calcul du modèle de la vue [vue-authentification.php] est le suivant :
1 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 | <?php
// on hérite des variables suivantes
// Request $request : la requête en cours
// Session $session : la session de l'application
// array $config : la configuration de l'application
// array $content : la réponse du contrôleur
//
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// on calcule le modèle de la vue
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new stdClass();
// état de l'application
$état = $content["état"];
// le modèle dépend de l'état
switch ($état) {
case 700:
case 400:
// cas de l'affichage du formulaire vide
$modèle->login = "";
// il n'y pas d'erreur à afficher
$modèle->error = FALSE;
break;
case 221:
// authentification erronée
// on réaffiche l'utilisateur initialement saisi
$modèle->login = $request->request->get("user");
// il y a une erreur à afficher
$modèle->error = TRUE;
// liste HTML de msg d'erreur - ici un seul
$modèle->erreurs = "<li>Echec de l'authentification</li>";
}
// résultat
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
|
Commentaires
- lignes 3-6 : on rappelle les variables héritées de la classe [HtmlResponse] qui fait afficher par un [require] la vue [vue-authentification.php] ;
- lignes 9-10 : les classes Symfony utilisées dans le code de la vue ;
- lignes 15-40 : la fonction [getModelForThisView] est chargée de calculer le modèle de la vue ;
- ligne 19 : on récupère le code d’état rendu par le contrôleur qui a traité l’action en cours ;
- lignes 21-37 : le modèle dépend de ce code d’état ;
- lignes 22-28 : cas où on doit afficher un formulaire d’authentification vierge ;
- lignes 29-37 : cas de l’authentification erronée : on raffiche l’identifiant saisi pour l’utilisateur et on affiche un message d’erreur. L’utilisateur au clavier peut alors réessayer une autre authentification ;
Un modèle particulier a été écrit pour le bandeau [v-bandeau.php] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php
// logo
$scheme = $request->server->get('REQUEST_SCHEME'); // http
$host = $request->server->get('SERVER_NAME'); // localhost
$port = $request->server->get('SERVER_PORT'); // 80
$uri = $request->server->get('REQUEST_URI'); // /php7/scripts-web/impots/version-12/main.php?action=xxx
$champs = [];
preg_match("/(.+)\/.+?$/", $uri, $champs);
$root = $champs[1]; // /php7/scripts-web/impots/version-12
$modèle->logo = "$scheme://$host:$port$root/Views/logo.jpg"; // http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg
?>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $modèle->logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
|
Commentaires
la ligne 16 utilise la variable [$modèle→logo] qui est l’URL du logo du bandeau. Plutôt que de calculer cette variable quatre fois pour les quatre vues de l’application, ce calcul est factorisé dans le fragment [v-bandeau.php] ;
les lignes 1-11 montrent comment construire l’URL [http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg] à partir des informations trouvées dans l’environnement du serveur [$request→server] ;
Tests [Postman]
Nous avons déjà créé des requêtes produidant les codes [700, 221, 400] qui affichent la vue d’authentification. Rappelons-les :
- [init-session-html-700] : 700 est le code d’état à l’issue d’une action [init-session] réussie : on présente alors le formulaire d’authentification vide ;
- [authentifier-utilisateur-221] : 221 est le code d’état à l’issue d’une action [authentifier-utilisateur] ayant échoué (identifiants non reconnus) : on présente alors le formulaire d’authentification pour qu’il soit corrigé ;
- [fin-session-400] : 400 est le code d’état à l’issue d’une action [fin-session] réussie : on présente alors le formulaire d’authentification vide ;
Il suffit de les réutiliser et de voir s’ils affichent bien la vue d’authentification. On ne montrera ici que deux tests :
- [init-session-html-700] : début d’une session HTML ;
- [authentifier-utilisateur-221] : authentification de l’utilisateur [x, x] ;
Ci-dessus :
la requête avait posté la chaîne [user=x&password=x] ;
en [4], un message d’erreur est affiché ;
en [3], l’utilisateur erroné a été réaffiché ;
Conclusion
Nous avons pu tester la vue [vue-authentification.php] sans avoir écrit les autres vues. Cela a été possible parce que :
tous les contrôleurs sont écrits ;
[Postman] nous permet d’émettre des requêtes vers le serveur sans avoir besoin des vues. Lorsqu’on écrit les contrôleurs, il faut être conscient que n’importe qui peut faire cela. Il faut donc être prêt à traiter des requêtes qu’aucune vue ne permettrait. Elles sont fabriquées à la main dans [Postman]. On ne doit jamais se dire a priori « cette requête est impossible » . Il faut vérifier ;
La vue de calcul de l’impôt
Présentation de la vue
La vue de calcul de l’impôt est la suivante :
La vue a trois parties :
- 1 : le bandeau supérieur est généré par le fragment [v-bandeau.php] déjà présenté ;
- 2 : le formulaire de calcul de l’impôt généré par le fragment [v-calcul-impot.php] ;
- 3 : un menu présentant deux liens, généré par le fragment [v-menu.php] ;
La vue de calcul de l’impôt est générée par le script [vue-calcul-impot.php] suivant :
1 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 | <?php
// on hérite des variables suivantes
// Request $request : la requête en cours
// Session $session : la session de l'application
// array $config : la configuration de l'application
// array $content : la réponse du contrôleur qui a traité l'action
//
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// on calcule le modèle de la vue
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
…
// on rend le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- bandeau -->
<?php require "v-bandeau.php"; ?>
<!-- ligne à deux colonnes -->
<div class="row">
<!-- le menu -->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- le formulaire de calcul -->
<div class="col-md-9">
<?php require "v-calcul-impot.php" ?>
</div>
</div>
<!-- cas du succès -->
<?php
if ($modèle->success) {
// on affiche une alerte de réussite
print <<<EOT1
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-success" role="alert">
$modèle->impôt</br>
$modèle->décôte</br>\n
$modèle->réduction</br>\n
$modèle->surcôte</br>\n
$modèle->taux</br>\n
</div>
</div>
</div>
EOT1;
}
?>
<?php
if ($modèle->error) {
// liste des erreurs sur 9 colonnes
print <<<EOT2
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
L'erreur suivante s'est produite :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT2;
}
?>
</div>
</body>
</html>
|
Commentaires
nous ne commentons que des nouveautés encore non rencontrées ;
ligne 37 : inclusion du bandeau supérieur de la vue dans la première ligne Bootstrap de la vue ;
lignes 41-43 : inclusion du menu qui occupera trois colonnes de la seconde ligne Bootstrap de la vue ;
lignes 45-47 : inclusion du formulaire de calcul d’impôt qui occupera neuf colonnes de la seconde ligne Bootstrap de la vue ;
lignes 51-69 : si le calcul de l’impôt réussit [$modèle→success=TRUE], alors le résultat du calcul de l’impôt est affiché dans un cadre vert (lignes 59-65). Ce cadre est dans la troisième ligne Bootstrap de la vue (ligne 54) et occupe neuf colonnes (ligne 58) à droite de trois colonnes vides (lignes 55-57). Ce cadre sera donc immédiatement dessous le formulaire de calcul de l’impôt ;
lignes 71-87 : si le calcul de l’impôt échoue [$modèle→error=TRUE], alors un message d’erreur est affiché dans un cadre rose (lignes 80-83). Ce cadre est dans la troisième ligne Bootstrap de la vue (ligne 75) et occupe neuf colonnes (ligne 79) à droite de trois colonnes vides (lignes 76-78). Ce cadre sera donc immédiatement dessous le formulaire de calcul de l’impôt ;
Le fragment [v-calcul-impot.php]
Le fragment [v-calcul-impot.php] affiche le formulaire d’authentification de l’application web :
Le code du fragment [v-calcul-impot.php] est le suivant :
1 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 | <!-- formulaire HTML posté -->
<form method="post" action="main.php?action=calculer-impot">
<!-- message sur 12 colonnes sur fond bleu -->
<div class="col-md-12">
<div class="alert alert-primary" role="alert">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</div>
</div>
<!-- éléments du formulaire -->
<fieldset class="form-group">
<!-- première ligne sur 9 colonnes -->
<div class="row">
<!-- libellé sur 4 colonnes -->
<legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
<!-- boutons radio sur 5 colonnes-->
<div class="col-md-5">
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios1" value="oui" <?= $modèle->checkedOui ?>>
<label class="form-check-label" for="gridRadios1">
Oui
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios2" value="non" <?= $modèle->checkedNon ?>>
<label class="form-check-label" for="gridRadios2">
Non
</label>
</div>
</div>
</div>
<!-- deuxième ligne sur 9 colonnes -->
<div class="form-group row">
<!-- libellé sur 4 colonnes -->
<label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
<!-- zone de saisie numérique du nombre d'enfants sur 5 colonnes -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="<?= $modèle->enfants ?>">
</div>
</div>
<!-- troisème ligne sur 9 colonnes -->
<div class="form-group row">
<!-- libellé sur 4 colonnes -->
<label for="salaire" class="col-md-4 col-form-label">Salaire annuel</label>
<!-- zone de saisie numérique pour le salaire sur 5 colonnes -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="salaire" name="salaire" placeholder="Salaire annuel" aria-describedby="salaireHelp" value="<?= $modèle->salaire ?>">
<small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
</div>
</div>
<!-- quatrième ligne, bouton [submit] sur 5 colonnes -->
<div class="form-group row">
<div class="col-md-5">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
|
Commentaires
- ligne 2 : le formulaire HTML sera posté (attribut [method]) à
l’URL [main.php?action=calculer-impot] (attribut [action]).
Les valeurs postées seront les valeurs des zones de saisie :
- la valeur du bouton radio coché sous la forme :
- [marié=oui] si le bouton radio [Oui] est coché (lignes 16-22). [marié] est la valeur de l’attribut [name] de la ligne 18, [oui] la valeur de l’attribut [value] de la ligne 18 ;
- [marié=non] si le bouton radio [Non] est coché (lignes 23-28). [marié] est la valeur de l’attribut [name] de la ligne 24, [non] la valeur de l’attribut [value] de la ligne 24 ;
- la valeur de la zone de saisie numérique de la ligne 37 sous la forme [enfants=xx] où [enfants] est la valeur de l’attribut [name] de la ligne 37, et [xx] la valeur saisie par l’utilisateur au clavier ;
- la valeur de la zone de saisie numérique de la ligne 46 sous la forme [salaire=xx] où [salaire] est la valeur de l’attribut [name] de la ligne 46, et [xx] la valeur saisie par l’utilisateur au clavier ;
- la valeur du bouton radio coché sous la forme :
Finalement, la valeur postée aura la forme [marié=xx&enfants=yy&salaire=zz].
- les valeurs saisies seront postées lorsque l’utilisateur cliquera sur le bouton de type [submit] de la ligne 53 ;
- lignes 16-30 : les deux boutons radio :
Les deux boutons radio font partie du même groupe de boutons radio car ils ont le même attribut [name] (lignes 18, 24). Le navigateur s’assure que dans un groupe de boutons radio, un seul est coché à un moment donné. Donc cliquer sur l’un désactive celui qui était coché auparavant ;
- ce sont des boutons radio à cause de l’attribut [type= »radio »] (lignes 18, 24) ;
- à l’affichage du formulaire (avant saisie), l’un des boutons radio
devra être coché : il suffit pour cela d’ajouter l’attribut
[checked=’checked’] à la balise <input type= »radio »>
concernée. Cela est réalisé avec des variables dynamiques :
- [<?= $modèle->checkedOui ?>] à la ligne 18 ;
- [<?= $modèle->checkedNon ?>] à la ligne 24 ;
Ces variables feront partie du modèle de la vue.
- ligne 37 : une zone de saisie numérique [type= »number »] avec une valeur minimale de 0 [min= »0 »]. Dans les navigateurs récents, cela signifie que l’utilisateur ne pourra saisir qu’un nombre >=0. Sur ces mêmes navigateurs récents, la saisie peut être faite avec un variateur qu’on peut cliquer à la hausse ou à la baisse. L’attribut [step= »1 »] de la ligne 37 indique que le variateur opèrera avec des sauts de 1 unité. Cela a pour conséquence que le variateur ne prendra pour valeur que des entiers progressant de 0 à n avec un pas de 1. Pour la saisie manuelle, cela signifie que les nombres avec virgule ne seront pas acceptés ;
- ligne 37 : lors de certains affichages, la zone de saisie des enfants devra être préremplie avec la dernière saisie faite dans cette zone. On utilise pour cela l’attribut [value] qui fixe la valeur à afficher dans la zone de saisie. Cette valeur sera dynamique et générée par la variable [$modèle→enfants] ;
- ligne 46 : mêmes explications pour la saisie du salaire que pour celle des enfants ;
- ligne 53 : le bouton de type [submit] qui déclenche le POST des valeurs saisies à l’URL [main.php?action=calculer-impot] ;
Calcul du modèle de la vue¶
Une fois l’aspect visuel de la vue déterminé, on peut procéder au calcul du modèle de la vue en conditions réelles. Rappelons les codes d’état qui mènent à cette vue. On les trouve dans le fichier de configuration :
1 2 3 4 5 6 | "vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
|
Ce sont donc les codes d’état [200, 300, 341, 350, 800] qui font afficher la vue d’authentifcation. Pour retrouver la signification de ces codes, on peut s’aider des tests [Postman] réalisés sur l’application jSON :
- [authentifier-utilisateur-200] : 200 est le code d’état à l’issue d’une action [authentifier-itilisateur] réussie : on présente alors le formulaire de calcul d’impôt vide ;
- [calculer-impot-300] : 300 est le code d’état à l’issue d’une action [calculer-impot] réussie. On affiche alors le formulaire de calcul avec les données qui y ont été saisies et le montant de l’impôt. L’utilisateur peut alors refaire un autre calcul ;
- [fin-session-400] : 400 est le code d’état à l’issue d’une action [fin-session] réussie : on présente alors le formulaire d’authentification vide ;
- le code d’état [341] est celui obtenu pour un calcul d’impôt valide mais l’absence de connexion au SGBD provoque une erreur ;
- le code d’état [350] est celui obtenu pour un calcul d’impôt valide mais l’absence de connexion au serveur [Redis] provoque une erreur ;
- le codes d’état [800] sera présenté ultérieurement. Nous ne l’avons pas encore rencontré ;
- on a fait ici l’hypothèse que l’utilisateur utilise un navigateur récent. Ainsi avec le formulaire étudié, il n’est pas possible de taper des nombres négatifs, des chaines de caractères non numériques, des nombres à virgule dans les champs de saisie [enfants, salaire]. Avec des navigateurs plus anciens, ce serait possible. On traitera ces erreurs comme des erreurs inattendues et on affichera alors la vue [vue-erreurs] ;
Maintenant que nous savons à quels moments doit être affiché le formulaire de calcul de l’impôt, on peut calculer son modèle dans [vue-calcul-impot.php] :
1 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 | <?php
// on hérite des variables suivantes
// Request $request : la requête en cours
// Session $session : la session de l'application
// array $config : la configuration de l'application
// array $content : la réponse du contrôleur qui a traité l'action
//
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// on calcule le modèle de la vue
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
// état de l'application
$état = $content["état"];
// le modèle dépend de l'état
switch ($état) {
case 200 :
case 800:
// affichage initial d'un formulaire vide
$modèle->success = FALSE; $modèle->errror = FALSE;
$modèle->checkedNon = 'checked="checked"';
$modèle->checkedOui = "";
$modèle->enfants = "";
$modèle->salaire = "";
break;
case 300:
// réussite du calcul - affichage du résultat
$modèle->success = TRUE;
$modèle->error = FALSE;
$modèle->impôt = "Montant de l'impôt : {$content["réponse"]["impôt"]} euros";
$modèle->décôte = "Décôte : {$content["réponse"]["décôte"]} euros";
$modèle->réduction = "Réduction : {$content["réponse"]["réduction"]} euros";
$modèle->surcôte = "Surcôte : {$content["réponse"]["surcôte"]} euros";
$modèle->taux = "Taux d'imposition : " . ($content["réponse"]["taux"] * 100) . " %";
// formulaire rétabli avec les valeurs saisies
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
break;
case 341:
// base de données HS
case 350:
// serveur Redis HS
// formulaire rétabli avec les valeurs saisies
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
// erreur
$modèle->success = FALSE;
$modèle->error = TRUE;
$modèle->erreurs = "<li>{$content["réponse"]}</li>";
break;
}
//menu
$modèle->optionsMenu = [
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session"];
// on rend le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
<title>Application impots</title>
</head>
<body>
…
</body>
</html>
|
Commentaires
lignes 22-30 : affichage d’un formulaire vide ;
lignes 31-45 : cas du calcul d’impôt réussi. On réaffiche les valeurs saisies ainsi que le montant de l’impôt ;
lignes 46-59 : cas de l’échec du calcul d’impôt à cause d’une indisponibilité d’un des serveurs [Redis] ou [MySQL] ;
lignes 62-64 : calcul des deux options du menu ;
Tests [Postman]
Le test [calculer-impot-300] nous permet d’avoir le code d’état 300. Il correspond à un calcul d’impôt réussi :
- en [3], les valeurs ayant amené au résultat [2] ;
Essayons un cas d’erreur : l’erreur [350] dûe à une indisponibilité du serveur [Redis] :
La vue de la liste des simulations
Présentation de la vue
La vue qui présente la liste des simulations est la suivante :
La vue générée par le script [vue-liste-simulations] a trois parties :
- 1 : le bandeau supérieur est généré par le fragment [v-bandeau.php] déjà présenté ;
- 2 : le tableau des simulations généré par le fragment [v-liste-simulations.php] ;
- 3 : un menu présentant deux liens, généré par le fragment [v-menu.php] ;
La vue des simulations est générée par le script [vue-liste-simulations.php] suivant :
1 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 | <?php
// on calcule le modèle de la vue
$modèle = getModelForThisView();
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
…
// on rend le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- bandeau -->
<?php require "v-bandeau.php"; ?>
<!-- ligne à deux colonnes -->
<div class="row">
<!-- menu sur trois colonnes-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- liste des simulations sur 9 colonnes-->
<div class="col-md-9">
<?php require "v-liste-simulations.php" ?>
</div>
</div>
</div>
</body>
</html>
|
Commentaires
- ligne 28 : inclusion du bandeau de l’application [1] ;
- ligne 33 : inclusion du menu [2]. Il sera affiché sur trois colonnes sous le bandeau ;
- ligne 37 : inclusion du tableau des simulations [3]. Il sera affiché sur neuf colonnes sous le bandeau et à droite du menu ;
Nous avons déjà commenté deux des trois fragments de cette vue :
Le fragment [v-liste-simulations.php] est le suivant :
1 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 | <!-- message sur fond bleu -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- tableau des simulations -->
<table class="table table-sm table-hover table-striped">
<!-- entêtes des six colonnes du tableau -->
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Marié</th>
<th scope="col">Nombre d'enfants</th>
<th scope="col">Salaire annuel</th>
<th scope="col">Montant impôt</th>
<th scope="col">Surcôte</th>
<th scope="col">Décôte</th>
<th scope="col">Réduction</th>
<th scope="col">Taux</th>
<th scope="col"></th>
</tr>
</thead>
<!-- corps du tableau (données affichées) -->
<tbody>
<?php
$i = 0;
// on affiche chaque simulation en parcourant le tableau des simulations
foreach ($modèle->simulations as $simulation) {
// affichage d'une ligne du tableau avec 6 colonnes - balise <tr>
// colonne 1 : entête ligne (n° simulation) - balise <th scope='row'>
// colonne 2 : valeur paramètre [marié] - balise <td>
// colonne 3 : valeur paramètre [enfants] - balise <td>
// colonne 4 : valeur paramètre [salaire] - balise <td>
// colonne 5 : valeur paramètre [impôt] (de l'impôt) - balise <td>
// colonne 6 : valeur paramètre [surcôte] - balise <td>
// colonne 7 : valeur paramètre [décôte] - balise <td>
// colonne 8 : valeur paramètre [réduction] - balise <td>
// colonne 9 : valeur paramètre [taux] (de l'impôt) - balise <td>
// colonne 10 : lien de suppression de la simulation - balise <td>
print <<<EOT
<tr>
<th scope="row">$i</th>
<td>{$simulation["marié"]}</td>
<td>{$simulation["enfants"]}</td>
<td>{$simulation["salaire"]}</td>
<td>{$simulation["impôt"]}</td>
<td>{$simulation["surcôte"]}</td>
<td>{$simulation["décôte"]}</td>
<td>{$simulation["réduction"]}</td>
<td>{$simulation["taux"]}</td>
<td><a href="main.php?action=supprimer-simulation&numéro=$i">Supprimer</a></td>
</tr>
EOT;
$i++;
}
?>
</tr>
</tbody>
</table>
|
Commentaires
un tableau HTML est réalisé avec la balise <table> (lignes 6 et 58) ;
les entêtes des colonnes du tableau se font à l’intérieur d’une balise <thead> (table head, lignes 8, 21). La balise <tr> (table row, lignes 9 et 20) délimitent une ligne. Lignes 10-15, la balise <th> (table header) définit un entête de colonne. Il y en a donc dix. [scope= »col »] indique que l’entête s’applique à la colonne. [scope= »row »] indique que l’entête s’applique à la ligne ;
lignes 23-57 : la balise <tbody> encadre les données affichées par le tableau ;
lignes 40-51 : la balise <tr> encadre une ligne du tableau ;
ligne 41 : la balise <th scope=’row’> définit l’entête de la ligne ;
lignes 42-50 : chaque balise <td> définit une colonne de la ligne ;
ligne 27 : la liste des simulations sera trouvée dans le modèle [$modèle→simulations] qui est un tableau associatif ;
ligne 50 : un lien pour supprimer la simulation. L’URL reprend le n° affiché dans la 1re colonne du tableau (ligne 41) ;
Test visuel
Nous rassemblons ces différents éléments dans le dossier [Tests] et nous créons un modèle de test pour la vue [vue-liste-simulations.php] :
Le modèle de données de la vue [vue-liste-simulations] sera le suivant :
1 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
// on calcule le modèle de la vue
$modèle = getModelForThisView();
function getModelForThisView(): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
// on met les simulations au format attendu par la page
$modèle->simulations = [
[
"marié" => "oui",
"enfants" => 2,
"salaire" => 60000,
"impôt" => 448,
"décôte" => 100,
"réduction" => 20,
"surcôte" => 0,
"taux" => 0.14
],
[
"marié" => "non",
"enfants" => 2,
"salaire" => 200000,
"impôt" => 25600,
"décôte" => 0,
"réduction" => 0,
"surcôte" => 8400,
"taux" => 0.45
]
];
// les options de menu
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// image du bandeau
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// on rend le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
|
Commentaires
- lignes 9-30 : le tableau des simulations affichées par la table HTML ;
- lignes 32-34 : le tableau des options de menu ;
Affichons cette vue :
On obtient le résultat suivant :
On travaille sur cette vue jusqu’à ce que le résultat obtenu visuellement nous convienne. On peut ensuite passer à l’intégration de la vue dans l’application web en cours d’écriture.
Calcul du modèle de la vue¶
Une fois l’aspect visuel de la vue déterminé, on peut procéder au calcul du modèle de la vue en conditions réelles. Rappelons les codes d’état qui mènent à cette vue. On les trouve dans le fichier de configuration :
1 2 3 4 5 6 | "vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
|
Ce sont donc les codes d’état [500, 600] qui font afficher la vue des simulations. Pour retrouver la signification de ces codes, on peut s’aider des tests [Postman] réalisés sur l’application jSON :
- [lister-simulations-500] : 500 est le code d’état à l’issue d’une action [lister-simulations] réussie : on présente alors la liste des simulations réalisées par l’utilisateur ;
- [supprimer-simulation-600] : 600 est le code d’état à l’issue d’une action [supprimer-simulation] réussie. On présente alors la nouvelle liste des simulations obtenue après cette suppression ;
Maintenant que nous savons à quels moments doit être affichée la liste des simulations, on peut calculer son modèle dans [vue-liste-simulations.php] :
1 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
// on hérite des variables suivantes
// Request $request : la requête en cours
// Session $session : la session de l'application
// array $config : la configuration de l'application
// array $content : la réponse du contrôleur
// pas d'erreurs possibles
// array $content : la réponse du contrôleur
//
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// on calcule le modèle de la vue
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
// on met les simulations au format attendu par la page
// elles sont trouvées dans la réponse du contrôleur qui a exécuté l'action
// sous la forme d'un tableau d'objets de type [Simulation]
$objetsSimulation = $content["réponse"];
// chaque objet [Simulation] va être transformé en tableau associatif
$modèle->simulations = [];
foreach ($objetsSimulation as $objetSimulation) {
$modèle->simulations[] = [
"marié" => $objetSimulation->getMarié(),
"enfants" => $objetSimulation->getEnfants(),
"salaire" => $objetSimulation->getSalaire(),
"impôt" => $objetSimulation->getImpôt(),
"surcôte" => $objetSimulation->getSurcôte(),
"décôte" => $objetSimulation->getdécôte(),
"réduction" => $objetSimulation->getRéduction(),
"taux" => $objetSimulation->getTaux()
];
}
// les options de menu
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// on rend le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
|
Commentaires
lignes 26-36 : calcul du modèle [$modèle→simulations] utilisé par le fragment [v-liste-simulations.php] ;
lignes 39-41 : calcul du modèle [$modèle→optionsMenu] utilisé par le fragment [v-menu.php] ;
Tests [Postman]
Le test [lister-simulations-500] nous permet d’avoir le code d’état 500. Il correspond à une demande pour voir les simulations :
Le test [supprimer-simulation-600] nous permet d’avoir le code d’état 600. Il correspond à la suppression réussie de la simulation n° 0. Le résultat renvoyé est une liste de simulations avec une simulation en moins :
La vue des erreurs inattendues¶
On appelle ici, erreur inattendue, une erreur qui n’aurait pas dû se produire dans le cadre d’une utilisation normale de l’application web.
Prenons comme exemple, le test [Postman] [calculer-impot-3xx] défini comme suit :
- en [1-3], une requête POST avec l’action [calculer-impot] ;
- en [4-6] : ici on peut définir ce qu’on veut pour les trois
paramètres du POST :
- [4] : le paramètre [marié] est manquant ;
- [5-6] : les paramètres [enfants, salaire] sont présents mais invalides ;
- en [9], ces trois erreurs sont signalées avec le code d’état 338 ;
Or dans le formulaire HTML de l’application web, ce cas ne peut pas se produire :
- tous les paramètres sont présents ;
- le paramètre [marié] qui prend sa valeur dans les attributs [value] de deux boutons radio, a forcément l’une des valeurs [oui] ou [non] ;
- avec un navigateur récent, les attributs <input type=’number’ min=’0’ step=’1’ …> font que les saisies pour les enfants et le salaire sont forcément des nombres entiers >=0 ;
Cependant, rien n’empêche un utilisateur de prendre [Postman] et d’envoyer à notre serveur, le test [calcul-impot-3xx] ci-dessus. On a vu que notre application web savait répondre correctement à cette requête. On appellera, erreur inattendue, une erreur qui ne devrait pas se produire dans le cadre de l’application HTML. Si elle se produit, c’est que probablement quelqu’un essaie de ‘hacker’ l’application. Par souci de pédagogie, on a décidé d’afficher une vue d’erreurs pour ces cas. Dans la réalité, on pourrait réafficher la dernière page envoyée au client. Il suffit pour cela d’enregistrer en session, la dernière réponse HTML envoyée. En cas d’erreur inattendue, on renvoie cette réponse. Ainsi l’utilisateur aura l’impression que le serveur ne répond pas à ses erreurs puisque la page affichée ne change pas.
Présentation de la vue¶
La vue qui présente les erreurs inattendues est la suivante :
La vue générée par le script [vue-erreurs.php] a trois parties :
- 1 : le bandeau supérieur est généré par le fragment [v-bandeau.php] déjà présenté ;
- 2 : la ou les erreurs inattendues ;
- 3 : un menu présentant trois liens, généré par le fragment [v-menu.php] ;
La vue des erreurs inattendues est générée par le script [vue-erreurs.php] suivant :
1 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 | <?php
// on calcule le modèle de la vue
$modèle = getModelForThisView();
function getModelForThisView(): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
…
// on retourne le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- bandeau sur 12 colonnes -->
<?php require "v-bandeau.php"; ?>
<!-- ligne à deux colonnes -->
<div class="row">
<!-- menu sur 3 colonnes-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- liste des erreurs -->
<div class="col-md-9">
<?php
print <<<EOT
<div class="alert alert-danger" role="alert">
Les erreurs inattendues suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
EOT;
?>
</div>
</div>
</div>
</body>
</html>
|
Commentaires
- ligne 27 : inclusion du bandeau de l’application [1] ;
- ligne 32 : inclusion du menu [2]. Il sera affiché sur trois colonnes sous le bandeau ;
- lignes 34-44 : affichage de la zone d’erreurs sur neuf colonnes ;
- lignes 37-44 : l’opération [print] qui affiche les erreurs inattendues ;
- ligne 38 : cet affichage se fera dans un cadre Bootstrap à fond rose ;
- ligne 39 : un texte de présentation ;
- ligne 40 : la balise <ul> encadre une liste à puces. Cette liste à puces est fournie par le modèle [$modèle->erreurs] ;
Nous avons déjà commenté les deux fragments de cette vue :
Nous rassemblons ces différents éléments dans le dossier [Tests] et nous créons un modèle de test pour la vue [vue-erreurs.php] :
Le modèle de données de la vue [vue-erreurs.php] sera le suivant :
1 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 | <?php
// on calcule le modèle de la vue
$modèle = getModelForThisView();
function getModelForThisView(): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
// le tableau des erreurs inattendues
$erreurs = ["erreur1", "erreur2"];
// on construit la liste HTML des erreurs
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// options du menu
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// image du bandeau
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// on retourne le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
|
Commentaires
- lignes 9-15 : construction de la liste HTML des erreurs ;
- lignes 17-20 : le tableau des options de menu ;
Affichons cette vue :
On obtient le résultat suivant :
On travaille sur cette vue jusqu’à ce que le résultat obtenu visuellement nous convienne. On peut ensuite passer à l’intégration de la vue dans l’application web en cours d’écriture.
Calcul du modèle de la vue¶
Une fois l’aspect visuel de la vue déterminé, on peut procéder au calcul du modèle de la vue en conditions réelles. Rappelons les codes d’état qui mènent à cette vue. On les trouve dans le fichier de configuration :
1 2 3 4 5 6 | "vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
|
Ce sont donc les codes d’état qui ne sont pas dans ceux des lignes [2-4] qui font afficher la vue des erreurs inattendues.
Le code de calcul du modèle de la vue [vue-erreurs.php] est le suivant :
1 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 | <?php
// on hérite des variables suivantes
// Request $request : la requête en cours
// Session $session : la session de l'application
// array $config : la configuration de l'application
// array $content : la réponse du contrôleur
//
// dépendances Symfony
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// on calcule le modèle de la vue
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// on encapsule les données de la pagé dans $modèle
$modèle = new \stdClass();
// on récupère les erreurs dans la réponse du contrôleur
$réponse = $content["réponse"];
if (!is_array($réponse)) {
// un seul message d'erreur
$erreurs = [$réponse];
} else {
// plusieurs messages d'erreur
$erreurs = $réponse;
}
// on construit la liste HTML des erreurs
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// options du menu
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// on retourne le modèle
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
|
Commentaires
lignes 19-32 : calcul du modèle [$modèle→erreurs] utilisé par la vue [vue-erreurs.php] ;
lignes 34-37 : calcul du modèle [$modèle→optionsMenu] utilisé par le fragment [v-menu.php] ;
Tests [Postman]
Le test [calculer-impot-3xx] nous permet d’avoir le code d’état 338 qui n’est pas un code d’état attendu. La réponse HTML est alors la suivante :
Client du service web jSON¶
Architecture client / serveur¶
Nous nous intéressons maintenant au client jSON [A] du service web [B]. Le client [A], comme le service web [B] a une structure en couches :
Cette architecture est reflétée par l’organisation du code suivante :
La plupart des classes ont déjà été rencontrées et expliquées :
BaseEntity | paragraphe lien. |
---|---|
TaxPayerData | paragraphe lien. |
Simulation | paragraphe lien. |
ExceptionImpots | paragraphe lien. |
TraitDao | paragraphe lien. |
Utilitaires | paragraphe lien. |
La couche [dao]¶
Interface¶
L’interface de la couche [dao] sera la suivante [InterfaceClientDao.php] :
1 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 | <?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): Simulation;
// enregistrement des résultats
public function saveResults(string $resultsFilename, array $simulations): void;
// authentification
public function authentifierUtilisateur(String $user, string $password): void;
// liste des simulations
public function listerSimulations(): array;
// supprimer une simulation
public function supprimerSimulation(int $numéro): array;
// début de session
public function initSession(string $type = 'json'): void;
// fin de session
public function finSession(): void;
}
|
Commentaires
ligne 9 : la méthode [getTaxPayersData] permet d’exploiter le fichier jSON des données des contribuables. Cette méthode est implémentée par le trait [TraitDao] déjà commenté (paragraphe lien) ;
ligne 15 : la méthode [saveResults] permet de sauvegarder les résultats de plusieurs calculs d’impôt dans un fichier jSON. Là également, cette méthode est implémentée par le trait [TraitDao] déjà commenté (paragraphe lien) ;
lignes 12, 18, 21, 27, 30 : on a créé une méthode pour chacune des actions acceptées par le service web ;
Implémentation
L’interface [InterfaceClientDao] est implémentée par la classe [ClientDao] suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php
namespace Application;
// dépendances
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\CurlResponse;
class ClientDao implements InterfaceClientDao {
// utilisation d'un Trait
use TraitDao;
// attributs
private $urlServer;
private $sessionCookie;
private $verbose;
// constructeur
public function __construct(string $urlServer, bool $verbose = TRUE) {
$this->urlServer = $urlServer;
$this->verbose = $verbose;
}
…
}
|
Commentaires
lignes 18-21 : le consructeur reçoit deux paramètres :
- l’URL [$urlServer] du service web jSON ;
- un booléen [$verbose] qui à TRUE indique que la classe doit afficher les réponses du serveur sur la console ;
ligne 14 : le cookie de session. Le rôle de celui-ci a été décrit dans la version 09 du client (paragraphe lien) ;
ligne 11 : la classe utilise le trait [TraitDao] qui implémente deux méthodes de l’interface :
[getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array] ;
[function calculerImpot(string $marié, int $enfants, int $salaire): Simulation] ;
Méthode [initSession]
La méthode [initSession] est implémentée de la façon suivante :
1 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 | public function initSession(string $type = 'json'): void {
// on crée un client HTTP
$httpClient = HttpClient::create();
// on fait la requête au serveur sans authentification
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "init-session",
"type" => $type
],
"verify_peer" => false
]);
// on récupère la réponse
$this->getResponse($response);
// on récupère le cookie de session
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// cookie de session ?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
|
L’action [init-session] devant être la 1re action demandée au service web, la méthode [initSession] sera la 1re méthode de la couche [dao] à être appelée.
Commentaires
ligne 1 : le type de session désirée est passé en paramètre. En l’absence de paramètre, ce sera une session jSON qui sera démarrée ;
lignes 5-11 : une requête GET est faite au service web ;
lignes 7-8 : les deux paramètres du GET ;
ligne 10 : en cas d’échanges sécurisés (schéma https), le certificat de sécurité envoyé par le service web ne sera pas vérifié ;
ligne 13 : la méthode [getResponse] récupère la réponse du serveur. Elle la rend sous forme d’un tableau. Ici, le résultat de la méthode n’est pas exploité. La méthode [getResponse] lance une exception si le code HTTP de la réponse du service web est différent de 200 OK ;
lignes 14-25 : comme la méthode [initSession] est la 1re méthode de la couche [dao] à être exécutée, on récupère le cookie de session pour que les méthodes suivantes puissent le renvoyer au service web. Ce code a déjà été commenté dans la version 09 ;
La méthode [getResponse]
La méthode [getResponse] est chargée d’exploiter la réponse du service web :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | private function getResponse(CurlResponse $response) {
// on récupère la réponse
$json = $response->getContent(false);
// logs
if ($this->verbose) {
print "$json\n";
}
// on récupère le statut de la réponse
$statusCode = $response->getStatusCode();
// erreur ?
if ($statusCode !== 200) {
// on a une erreur
throw new ExceptionImpots($json);
}
// on rend sa réponse
$array = json_decode($json, true);
return $array["réponse"];
}
|
Commentaires
ligne 1 : la méthode est privée ;
ligne 1 : le paramètre de la méthode est la réponse du service web de type [SymfonyComponentHttpClientResponseCurlResponse], le type de réponse Symfony, lorsque [HttpClient] est implémenté par [CurlClient], ç-à-d par la bibliothèque [curl] ;
ligne 3 : on récupère la réponse jSON du serveur. On rappelle que le paramètre [false] est là pour empêcher Symfony de lancer une exception lorsque le statut de la réponse HTTP du serveur est dans le domaine [3xx, 4xx, 5xx] ;
lignes 5-7 : si on est en mode [$verbose] alors on affiche la réponse du serveur sur la console ;
lignes 9-14 : si le statut de la réponse HTTP du serveur est différent de 200, alors on lance une exception avec pour message d’erreur la réponse jSON du serveur ;
ligne 16 : la chaîne jSON est décodée dans un tableau ;
ligne 17 : les informations utiles sont dans [$array[« réponse »]] ;
La méthode [authentifierUtilisateur]
La méthode [authentifierUtilisateur] est la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public function authentifierUtilisateur(string $user, string $password): void {
// on crée un client HTTP
$httpClient = HttpClient::create();
// on fait la requête au serveur avec authentification
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "authentifier-utilisateur"
],
"body" => [
"user" => $user,
"password" => $password
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// on récupère la réponse
$this->getResponse($response);
}
|
Commentaires
ligne 5 : la requête du client est un POST ;
lignes 6-8 : paramètres dans l’URL ;
lignes 9-12 : paramètres du POST ;
ligne 14 : le cookie de session ;
ligne 17 : on lit la réponse. On sait qu’en cas d’erreur (code HTTP différent de 200), la méthode [getResponse] lance elle-même une exception ;
La méthode [calculerImpot]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
// on crée un client HTTP
$httpClient = HttpClient::create();
// on fait la requête au serveur sans authentification mais avec le cookie de session
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "calculer-impot"],
"body" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// on récupère la réponse
$array = $this->getResponse($response);
return (new Simulation())->setFromArrayOfAttributes($array);
}
|
Commentaires
ligne 6-7 : l’unique paramètre de l’URL ;
lignes 8-12 : les trois paramètres du POST (ligne 5) ;
ligne 17 : la réponse est exploitée ;
ligne 18 : si on arrive là c’est que la méthode [getResponse] n’a pas lancé d’exception. On rend un objet [Simulation] initialisé avec le tableau rendu par [getResponse] ;
La méthode [listerSimulations]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public function listerSimulations(): array {
// on crée un client HTTP
$httpClient = HttpClient::create();
// on fait la requête au serveur sans authentification mais avec le cookie de session
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "lister-simulations"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// on récupère la réponse
return $this->getSimulations($response);
}
|
Commentaires
ligne 5 : méthode GET ;
lignes 6-8 : l’unique paramètre du GET ;
ligne 13 : la récupération des simulations est confiée à la méthode privée [getSimulations] ;
La méthode [getSimulations]
1 2 3 4 5 6 7 8 9 10 11 12 | private function getSimulations(CurlResponse $response): array {
// on récupère la réponse JSON
$array = $this->getResponse($response);
// on a un tableau d'bjets associatifs
// on va en faire un tableau d'objets Simulation
$simulations = [];
foreach ($array as $simulation) {
$simulations [] = (new Simulation())->setFromArrayOfAttributes($simulation);
}
// on rend la liste d'objets Simulation
return $simulations;
}
|
Commentaires
ligne 3 : on récupère le tableau issu de la réponse. C’est un tableau de tableaux, chacun de ces derniers ayant tous les attributs d’un objet [Simulation] ;
ligne 6 : si on arrive là, c’est que la méthode [getResponse] n’a pas lancé d’exception ;
lignes 6-9 : on exploite la réponse pour construire un tableau d’objets [Simulation] ;
ligne 11 : on rend ce tableau ;
La méthode [SupprimerSimulation]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public function supprimerSimulation(int $numéro): array {
// on crée un client HTTP
$httpClient = HttpClient::create();
// on fait la requête au serveur sans authentification mais avec le cookie de session
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "supprimer-simulation",
"numéro" => $numéro
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// on récupère la réponse
return $this->getSimulations($response);
}
|
Commentaires
ligne 5 : on fait une requête GET ;
lignes 6-9 : les deux paramètres de l’URL ;
ligne 14 : après une suppression, le serveur rend le nouveau tableau des simulations. On rend ce tableau ;
La méthode [finSession]
Une session de travail avec le service web se termine normalement par l’appel à la méthode [finSession] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public function finSession(): void {
// on crée un client HTTP
$httpClient = HttpClient::create();
// on fait la requête au serveur sans authentification mais avec le cookie de session
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "fin-session"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// on récupère la réponse
$this->getResponse($response);
}
|
Commentaires
ligne 5 : on fait une requête GET ;
lignes 6-8 : l’unique paramètre de l’URL ;
ligne 13 : on lit la réponse. Une exception sera lancée si le code HTTP de la réponse est différent de 200 ;
La couche [métier]
L’interface¶
L’interface de la couche [métier] est la suivante [InterfaceClientMetier.php] :
1 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 | <?php
// espace de noms
namespace Application;
interface InterfaceClientMetier {
// calcul des impôts d'un contribuable
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
// calcul des impôts en mode batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFilename, string $errorsFileName): void;
// authentification
public function authentifierUtilisateur(String $user, string $password): void;
// liste des simulations
public function listerSimulations(): array;
// enregistrement des résultats
public function saveResults(string $resultsFilename, array $simulations): void;
// supprimer une simulation
public function supprimerSimulation(int $numéro): array;
// début de session
public function initSession(string $type = 'json'): void;
// fin de session
public function finSession(): void;
}
|
Commentaires
seule la méthode [executeBatchImpots] de la ligne 12 est spécifique à la couche [métier]. Toutes les autres appartiennent à la couche [dao] qui les implémente ;
La classe [ClientMetier]
La classe implémentant la couche [métier] est la suivante :
1 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
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// attribut
private $clientDao;
// constructeur
public function __construct(InterfaceClientDao $clientDao) {
$this->clientDao = $clientDao;
}
// calcul de l'impôt
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
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
$simulations = [];
// on les exploite
foreach ($taxPayersData as $taxPayerData) {
// on calcule l'impôt
$simulations [] = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
}
// enregistrement des résultats
if ($resultsFileName !== NULL) {
$this->clientDao->saveResults($resultsFileName, $simulations);
}
}
public function authentifierUtilisateur(String $user, string $password): void {
$this->clientDao->authentifierUtilisateur($user, $password);
}
public function listerSimulations(): array {
return $this->clientDao->listerSimulations();
}
public function saveResults(string $resultsFilename, array $simulations): void {
$this->clientDao->saveResults($resultsFilename, $simulations);
}
public function supprimerSimulation(int $numéro): array {
return $this->clientDao->supprimerSimulation($numéro);
}
public function finSession(): void {
$this->clientDao->finSession();
}
public function initSession(string $type = 'json'): void {
$this->clientDao->initSession($type);
}
}
|
Commentaires
- lignes 10-12 : pour se construire, la couche [métier] a besoin d’une référence sur la couche [dao] ;
- lignes 20-38 : seule la méthode [executeBatchImpots] est spécifique à la couche [métier]. L’implémentation des autres méthodes délègue le travail à faire aux méthodes de mêmes noms dans la couche [dao] ;
- ligne 23 : on s’adresse à la couche [dao] pour obtenir dans un tableau d’objets de type [TaxPayerData] les données des contribuables ;
- ligne 25 : on va cumuler dans le tableau [$simulations] les différentes simulations calculées ;
- lignes 27-33 : on calcule l’impôt de chacun des contribuables du tableau [$taxPayersData] ;
- lignes 35-37 : les résultats obtenus dans le tableau [$simulations] sont sauvegardés dans un fichier jSON ;
Note : La couche [métier] ne fait quasiment rien. On pourrait décider de la supprimer et de tout rassembler dans la couche [dao].
Le script principal¶
Le script principal est configuré par le fichier [config.json] suivant :
1 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 | {
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12",
"dependencies": [
"/Entities/BaseEntity.php",
"/Entities/TaxPayerData.php",
"/Entities/Simulation.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Utilitaires.php",
"/Model/InterfaceClientDao.php",
"/Model/TraitDao.php",
"/Model/ClientDao.php",
"/Model/InterfaceClientMetier.php",
"/Model/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-12/main.php"
}
|
Le script principal [main.php] est le suivant :
1 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 | <?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.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"]);
// création de la couche [métier]
$clientMetier = new ClientMetier($clientDao);
// calcul de l'impôts en mode batch
try {
// initialisation de la session
$clientMetier->initSession('json');
// authentification
$clientMetier->authentifierUtilisateur($config["user"]["login"], $config["user"]["passwd"]);
// calcul d'impots sans sauvegarde des résultats
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, NULL, ERRORS_FILENAME);
// liste des simulations
$clientMetier->listerSimulations();
// suppression d'une simulation
$simulations = $clientMetier->supprimerSimulation(1);
// sauvegarde des résultats
$clientMetier->saveResults(RESULTS_FILENAME, $simulations);
// fin de session
$clientMetier->finSession();
// action sans être authentifié - doit planter
$clientMetier->listerSimulations();
} catch (ExceptionImpots $ex) {
// on affiche l'erreur
print "Une erreur s'est produite : " . $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit();
|
Commentaires
- lignes 12-16 : exploitation du fichier de configuration [config.json] ;
- lignes 18-26 : chargement de toutes les dépendances ;
- lignes 28-34 : définition de constantes et d’alias ;
- lignes 36-39 : construction des couches [dao] et [métier] ;
- ligne 44 : initialisation d’une session jSON ;
- ligne 46 : on s’authentifie auprès du serveur ;
- ligne 48 : calcul de l’impôt d’une série de contribuables. On ne sauvegarde pas les résultats (2e paramètre NULL) ;
- ligne 50 : on demande les résultats de tous ces calculs ;
- ligne 52 : on supprime la simulation n° 1 (la 2e de la liste) ;
- ligne 54 : on sauvegarde les simulations qui restent ;
- ligne 56 : on termine la session. Cela signifie que le cookie de session est détruit ;
- ligne 58 : on demande la liste des simulations. Comme le cookie de session a été détruit, l’authentification doit être refaite. On doit donc avoir une exception disant qu’on n’est pas authentifié ;
Le fichier [taxpayersdata.json] est le suivant :
1 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 | [
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
|
Il y a 12 contribuables dont 1 est incorrect. Cela fait donc 11 simulations au total. L’une d’elles va être supprimée. Il doit en rester 10.
Après exécution du script principal, le fichier jSON [results.json] est le suivant :
1 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 | [
{
"marié": "oui",
"enfants": "2",
"salaire": "55555",
"impôt": 2814,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "oui",
"enfants": "3",
"salaire": "50000",
"impôt": 0,
"surcôte": 0,
"décôte": 720,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "2",
"salaire": "100000",
"impôt": 19884,
"surcôte": 4480,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "non",
"enfants": "3",
"salaire": "100000",
"impôt": 16782,
"surcôte": 7176,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "3",
"salaire": "100000",
"impôt": 9200,
"surcôte": 2180,
"décôte": 0,
"réduction": 0,
"taux": 0.3
},
{
"marié": "oui",
"enfants": "5",
"salaire": "100000",
"impôt": 4230,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "0",
"salaire": "100000",
"impôt": 22986,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "2",
"salaire": "30000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
},
{
"marié": "non",
"enfants": "0",
"salaire": "200000",
"impôt": 64210,
"surcôte": 7498,
"décôte": 0,
"réduction": 0,
"taux": 0.45
},
{
"marié": "oui",
"enfants": "3",
"salaire": "20000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
}
]
|
Il y a bien 10 simulations.
Le fichier jSON [errors.json] a le contenu suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
|
Les résultats console sont les suivants (en mode verbose, les réponses jSON du serveur sont affichées sur la console) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | {"action":"init-session","état":700,"réponse":"session démarrée avec type [json]"}
{"action":"authentifier-utilisateur","état":200,"réponse":"Authentification réussie [admin, admin]"}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"lister-simulations","état":500,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"supprimer-simulation","état":600,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"fin-session","état":400,"réponse":"session supprimée"}
{"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Une erreur s'est produite : {"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Terminé
|
Tests [Codeception]¶
Comme pour les précédents clients, le client de la version 12 peut faire l’objet de tests [Codeception] :
Le code de la classe de test de la couche [métier] du client est analogue à celui des classes de test des précédents clients :
1 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 | <?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-12");
// chemin du fichier de configuration
define("CONFIG_FILENAME", ROOT . "/Data/config.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épendances Symfony
use Symfony\Component\HttpClient\HttpClient;
// classe de test
class ClientDaoTest extends \Codeception\Test\Unit {
// couche dao
private $clientDao;
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"]);
// création de la couche [métier]
$this->métier = new ClientMetier($clientDao);
// initialisation session
$this->métier->initSession("json");
// authentification
$this->métier->authentifierUtilisateur("admin", "admin");
}
// tests
public function test1() {
$simulation = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $simulation->getImpôt(), 1);
$this->assertEqualsWithDelta(0, $simulation->getSurcôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getDécôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getRéduction(), 1);
$this->assertEquals(0.14, $simulation->getTaux());
}
public function test2() {
….
}
…
public function test11() {
…
}
}
|
Commentaires
- lignes 34-46 : on rappelle que le constructeur de la classe de test est exécuté avant chaque test ;
- lignes 38-41 : construction des couches [dao] et [métier] ;
- lignes 42-45 : les méthodes de test [test1…, test11] testent la méthode [calculerImpot]. Pour que cela soit possible, il faut auparavant initialiser une session jSON et s’authentifier ;
Les résultats du test sont les suivants :
Beaucoup d’autres tests devraient être faits :
- tester les différentes méthodes de la couche [dao] ;
- tester les états rendus par le serveur web. Ces états sont importants puisque leur valeur décide de la page HTML à afficher ;
Exercice d’application – version 13¶
La version 13 amène peu de changements : elle sécurise l’accès aux fichiers de l’application.
Avec la version 12, on peut demander l’URL suivante [http://localhost/php7/scripts-web/impots/version-12/config.json]. On obtient alors la page suivante (Firefox) :
Or ce fichier [config.json] contient des informations sensibles tels les identifiants des utilisateurs autorisés à utiliser l’application. Il ne faut pas qu’il soit accessible aux utilisateurs. Il en est de même pour tous les fichiers de l’application à l’exception des fichiers [main.php, index.php, Views/logo.jpg] qui eux doivent être accessibles de l’extérieur. En effet les vues ont besoin d’avoir un accès HTTP au logo de l’application. La version 13 amène une solution simple à ce problème.
Dans Netbeans, nous faisons un copier-coller du dossier [version-12] dans [version-13] :
- en [1], dans le dossier racine de l’application, nous ne laissons que les scripts [index.php, main.php] ;
- en [2], les fichiers de configuration sont placés dans un dossier [Config] ;
- en [3], l’image [logo.jpg] est placée dans un dossier [Resources] ;
Le serveur HTTP utilisé est ici un serveur Apache. Celui-ci permet de contrôler l’accès à un dossier via un fichier [.htaccess]. Dans tous les dossiers auxquels nous voulons interdire un accès direct par URL, nous créons le fichier [.htaccess] suivant :
Ces deux lignes font que tout accès au dossier est interdit à tous.
Nous plaçons ce fichier dans tous les dossiers de l’application sauf dans le dossier racine et le dossier [Resources]. Finalement seuls trois fichiers sont accessibles de l’extérieur : [index.php, main.php, Resources/logo.jpg].
Faisons quelques essais :
Quelques modifications doivent être faites dans le code :
Dans le fichier [Config/config.json] :
Dans le fichier [main.php] :
Dans le fichier [Views/v-bandeau.php] :