Skip to content

13. Exercice d'application – version 5

Nous avons déjà écrit plusieurs versions de cet exercice. La dernière version utilisait une architecture en couches :

Image

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 .

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.

13.1. Création de la base de données [dbimpots-2019]

En suivant l'exemple du chapitre précédent, nous construisons une base de données MySQL nommée [dbimpots-2019] dont le propriétaire sera l'utilisateur [admimpots] avec le mot de passe [mdpimpots]. Cette base aura deux tables :

  • la table [tbtranches] contiendra les tranches d'imposition ;
  • la table [tbconstantes] contiendra les constantes du calcul de l'impôt .

Le cours PHP 7 créait la base, l'utilisateur et les tables avec l'assistant de [phpMyAdmin], puis exportait la structure obtenue dans un fichier SQL. Nous faisons le chemin inverse : nous écrivons directement le script SQL [dbimpots-2019.sql], placé dans le dossier [Data] de la version 5 :

-- base de données [dbimpots-2019] de l'application de calcul de l'impôt - SGBD MySQL
-- à exécuter par l'administrateur [root] du SGBD :
--   mysql -u root < dbimpots-2019.sql
-- la base, ses tables et son utilisateur sont recréés s'ils existent déjà

-- la base de données
DROP DATABASE IF EXISTS `dbimpots-2019`;
CREATE DATABASE `dbimpots-2019` CHARACTER SET utf8mb4;

-- son propriétaire [admimpots] de mot de passe [mdpimpots]
DROP USER IF EXISTS 'admimpots'@'localhost';
CREATE USER 'admimpots'@'localhost' IDENTIFIED BY 'mdpimpots';
GRANT ALL PRIVILEGES ON `dbimpots-2019`.* TO 'admimpots'@'localhost';

-- les tables sont créées dans la base [dbimpots-2019]
USE `dbimpots-2019`;

-- la table des tranches d'impôt
CREATE TABLE tbtranches (
    id INT NOT NULL AUTO_INCREMENT,
    limites DECIMAL(10, 2) NOT NULL,
    coeffr DECIMAL(6, 2) NOT NULL,
    coeffn DECIMAL(10, 2) NOT NULL,
    PRIMARY KEY (id)
);

-- la table des constantes de calcul de l'impôt
CREATE TABLE tbconstantes (
    id INT NOT NULL AUTO_INCREMENT,
    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,
    PRIMARY KEY (id)
);

Commentaires

  • ligne 7 : on supprime la base [dbimpots-2019] si elle existe. Le script peut ainsi être exécuté de nouveau pour régénérer la base si elle a été détruite ou altérée. Le nom de la base contient un tiret : il doit être entouré d'apostrophes inversées ` (backquotes) pour que MySQL ne le prenne pas pour une soustraction ;
  • ligne 8 : création de la base avec le jeu de caractères [utf8mb4] ;
  • lignes 11-13 : l'utilisateur [admimpots] de mot de passe [mdpimpots] est (re)créé. Il a tous les droits sur la base [dbimpots-2019] ;
  • ligne 16 : les ordres suivants s'appliquent à la base [dbimpots-2019] ;
  • lignes 19-25 : la table [tbtranches] a quatre colonnes :
    • [id] est un entier, clé primaire de la table, et sera auto-incrémenté par le SGBD ([AUTO_INCREMENT]). 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 première insertion, puis 2 à la suivante, etc. ;
    • [limites] est un nombre décimal à 10 chiffres dont 2 décimales ([DECIMAL(10, 2)]). Elle contiendra les éléments de la colonne [limites] des tranches d'impôt ;
    • [coeffr] est un nombre décimal à 6 chiffres dont 2 décimales. Elle contiendra les éléments de la colonne [coeffR] des tranches d'impôt ;
    • [coeffn] est un nombre décimal à 10 chiffres dont 2 décimales. Elle contiendra les éléments de la colonne [coeffN] des tranches d'impôt ;
  • lignes 28-41 : la table [tbconstantes] a une clé primaire [id] et une colonne décimale par constante de calcul de l'impôt. Elle n'aura qu'une ligne .

Le type [DECIMAL] stocke les nombres de façon exacte, contrairement aux types réels [FLOAT] et [DOUBLE] qui stockent une valeur approchée en binaire : 0.14 est stocké exactement comme 0.14.

On exécute ce script dans le terminal de [Laragon] ouvert dans le dossier [version-05/Data] par la commande :

mysql -u root < dbimpots-2019.sql

Si tout se passe bien, rien n'est affiché. On vérifie le résultat en se connectant sous l'identité de [admimpots] :

1
2
3
4
5
6
7
8
> mysql -u admimpots -pmdpimpots dbimpots-2019 -e "show tables"
mysql: [Warning] Using a password on the command line interface can be insecure.
+-------------------------+
| Tables_in_dbimpots-2019 |
+-------------------------+
| tbconstantes            |
| tbtranches              |
+-------------------------+
> mysql -u admimpots -pmdpimpots dbimpots-2019 -e "describe tbtranches"
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------+---------------+------+-----+---------+----------------+
| Field   | Type          | Null | Key | Default | Extra          |
+---------+---------------+------+-----+---------+----------------+
| id      | int           | NO   | PRI | NULL    | auto_increment |
| limites | decimal(10,2) | NO   |     | NULL    |                |
| coeffr  | decimal(6,2)  | NO   |     | NULL    |                |
| coeffn  | decimal(10,2) | NO   |     | NULL    |                |
+---------+---------------+------+-----+---------+----------------+
  • ligne 6 : la colonne [id] est clé primaire (PRI) et ses valeurs sont gérées par MySQL (auto_increment) .

On peut aussi bien sûr ouvrir et exécuter le script [dbimpots-2019.sql] dans [HeidiSQL] ou [phpMyAdmin], et y examiner la structure des tables.

13.2. Organisation du code

Pour mieux montrer le rôle des différents scripts PHP que nous écrivons, nous allons organiser le code de la version 5 en dossiers :

version-05
├── Dao
│   ├── DaoImpotsWithTaxAdminDataInDatabase.php
│   ├── DaoTransferAdminDataFromJsonFile2Database.php
│   ├── InterfaceDao.php
│   ├── InterfaceDao4TransferAdminData2Database.php
│   └── TraitDao.php
├── Data
│   ├── database.json
│   ├── dbimpots-2019.sql
│   ├── errors.json
│   ├── resultats.json
│   ├── taxadmindata.json
│   └── taxpayersdata.json
├── Entities
│   ├── Database.php
│   ├── ExceptionImpots.php
│   ├── TaxAdminData.php
│   └── TaxPayerData.php
├── Main
│   ├── MainCalculateImpotsWithTaxAdminDataInMySQLDatabase.php
│   └── MainTransferAdminDataFromJsonFile2MySQLDatabase.php
├── Metier
│   ├── InterfaceMetier.php
│   └── Metier.php
├── Tests
│   ├── data
│   │   ├── database-incomplet.json
│   │   └── database-mdp-errone.json
│   ├── DaoTest.php
│   └── MetierTest.php
└── Utilities
    └── Utilitaires.php
  • [Entities] : les entités de l'application, objets échangés entre couches ;
  • [Utilities] : les utilitaires de l'application ;
  • [Data] : les données utilisées ou produites par l'application, et le script SQL de la base. 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 le signalent. Par exemple, une erreur difficile à détecter dans un fichier texte basique est l'utilisation de la lettre O majuscule à la place du chiffre zéro. Si dans le fichier JSON on écrit par inadvertance ["plafondRevenusCouplePourReduction": 42O74], [VS Code] souligne en rouge la valeur 42O74, qui n'est ni un nombre, ni une chaîne entre guillemets, et l'onglet [Problèmes] affiche l'erreur. L'attention du développeur est attirée par la faute et il peut la corriger ;
  • [Dao] : les interfaces, trait et classes de la couche [dao] ;
  • [Metier] : l'interface et la classe de la couche [métier]. Le cours PHP 7 nommait ce dossier [Métier]. Nous évitons les caractères accentués dans les noms de dossiers et de fichiers : ils sont source de problèmes lorsque les fichiers passent d'un système à un autre (archives zip, dépôts Git…) ;
  • [Main] : les scripts principaux de la version 5 ;
  • [Tests] : les tests [PHPUnit] de la version 5 .

La version 5 a deux objectifs distincts :

  • remplir la base MySQL [dbimpots-2019] avec le contenu du fichier JSON [Data/taxadmindata.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.

13.3. Remplissage de la base de données [dbimpots-2019]

13.3.1. Objectif

Le fichier [Data/taxadmindata.json] contient les données de l'administration fiscale. C'est celui de la version 4 :

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

13.3.2. Les entités

Les entités sont les objets échangés entre les couches de l'application. Trois d'entre elles viennent de la version 4 et sont reprises sans aucune modification :

  • l'exception [ExceptionImpots] ;
  • la classe [readonly] [TaxAdminData] des données de l'administration fiscale, construite par ses méthodes statiques [fromJsonFile] et [fromArray] ;
  • la classe [readonly] [TaxPayerData] des données d'un contribuable et des résultats du calcul de son impôt .

Nous en ajoutons une quatrième, l'entité [Database], qui décrit la base de données.

13.3.2.1. Pourquoi pas de classe de base [BaseEntity] ?

Le cours PHP 7 introduisait à cet endroit une classe parent [BaseEntity] dont dérivaient toutes les entités. Elle reposait sur une règle : les attributs d'une entité ont les mêmes noms que les clés du fichier JSON qu'elle encapsule. [BaseEntity] savait alors initialiser n'importe quelle entité à partir d'un fichier JSON ou d'un tableau associatif par la boucle :

1
2
3
foreach ($arrayOfAttributes as $key => $value) {
  $this->$key = $value;
}

Les attributs des entités étaient déclarés [protected] (sans type) pour que la classe parent puisse les modifier, et chaque entité ajoutait ses getters et ses setters. Nous ne reprenons pas cette solution :

  • la boucle ci-dessus crée silencieusement un attribut dynamique lorsqu'une clé du fichier JSON est mal orthographiée. C'est déprécié depuis PHP 8.2, et interdit pour les classes [readonly] ;
  • les entités étaient modifiables par n'importe quelle couche, via leurs setters ou la méthode [setFromArrayOfAttributes]. Nos entités sont immuables : une couche ne peut pas modifier par erreur un objet qu'elle a reçu d'une autre ;
  • la règle « mêmes noms dans le JSON et dans la classe » reste la nôtre, mais PHP 8 l'exploite directement : l'expression [new self(...$tableau)] transforme les clés du tableau en arguments nommés du constructeur (PHP 8.1). C'est PHP qui vérifie alors qu'aucune clé ne manque, qu'aucune n'est inconnue et que chaque valeur a le bon type. Il n'y a plus de code commun à factoriser dans une classe parent .

Enfin, la version 4 a déjà doté la classe [TaxAdminData] de la méthode [fromArray]. Nous allons voir qu'elle suffit pour construire un objet [TaxAdminData] à partir des données lues dans la base. Une conséquence importante est que la couche [métier] de la version 4 sera reprise sans modification : dans le cours PHP 7, le changement des entités avait obligé à modifier la couche [métier].

13.3.2.2. L'entité [Database]

L'entité [Database] servira à encapsuler les données du fichier JSON [Data/database.json] suivant :

{
    "dsn": "mysql:host=localhost;dbname=dbimpots-2019;charset=utf8mb4",
    "id": "admimpots",
    "pwd": "mdpimpots",
    "tableTranches": "tbtranches",
    "colId": "id",
    "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"
}

Ses attributs décrivent les caractéristiques de la base de données MySQL [dbimpots-2019] :

Attribut
Rôle
dsn
le DSN de la base
id
le propriétaire de la base
pwd
son mot de passe
tableTranches
le nom de la table contenant les tranches d'imposition
colId / colLimites / colCoeffR / colCoeffN
les noms des colonnes de la table [tableTranches]
tableConstantes
le nom de la table contenant les constantes de calcul de l'impôt
colPlafondQfDemiPart / colPlafondRevenusCelibatairePourReduction / colPlafondRevenusCouplePourReduction / colValeurReducDemiPart / colPlafondDecoteCelibataire / colPlafondDecoteCouple / colPlafondImpotCelibatairePourDecote / colPlafondImpotCouplePourDecote / colAbattementDixPourcentMax / colAbattementDixPourcentMin
les 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 des tables PostgreSQL ne suivent pas les mêmes règles que ceux de MySQL. On va être obligé 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. Par rapport au cours PHP 7, nous avons ajouté l'attribut [colId], le nom de la colonne clé primaire de la table des tranches : nous en aurons besoin pour relire les tranches dans l'ordre où elles ont été insérées.

La classe [Database] est la suivante [Entities/Database.php] :

<?php

declare(strict_types=1);

// espace de noms
namespace Application;

// la description de la base de données des données fiscales : un objet immuable
final readonly class Database
{
    // les constantes de calcul de l'impôt : chacune a sa colonne dans la table [tableConstantes]
    public const array CONSTANTES = [
        "plafondQfDemiPart",
        "plafondRevenusCelibatairePourReduction",
        "plafondRevenusCouplePourReduction",
        "valeurReducDemiPart",
        "plafondDecoteCelibataire",
        "plafondDecoteCouple",
        "plafondImpotCelibatairePourDecote",
        "plafondImpotCouplePourDecote",
        "abattementDixPourcentMax",
        "abattementDixPourcentMin",
    ];

    // constructeur privé : on passe obligatoirement par la méthode [fromJsonFile]
    private function __construct(
        // connexion à la base
        public string $dsn,
        public string $id,
        #[\SensitiveParameter] public string $pwd,
        // la table des tranches d'impôt et ses colonnes
        public string $tableTranches,
        public string $colId,
        public string $colLimites,
        public string $colCoeffR,
        public string $colCoeffN,
        // la table des constantes de calcul et ses colonnes
        public string $tableConstantes,
        public string $colPlafondQfDemiPart,
        public string $colPlafondRevenusCelibatairePourReduction,
        public string $colPlafondRevenusCouplePourReduction,
        public string $colValeurReducDemiPart,
        public string $colPlafondDecoteCelibataire,
        public string $colPlafondDecoteCouple,
        public string $colPlafondImpotCelibatairePourDecote,
        public string $colPlafondImpotCouplePourDecote,
        public string $colAbattementDixPourcentMax,
        public string $colAbattementDixPourcentMin,
    ) {
    }

    // construction à partir d'un fichier JSON dont les clés sont les noms des attributs
    public static function fromJsonFile(string $databaseFilename): self
    {
        // peut lancer une exception [ExceptionImpots]
        $data = Utilitaires::lireFichierJson($databaseFilename);
        // le JSON doit être un objet {...}
        if (!is_array($data) || array_is_list($data)) {
            throw new ExceptionImpots("Le fichier [$databaseFilename] ne contient pas un objet JSON", 20);
        }
        try {
            // les clés du tableau deviennent des arguments nommés du constructeur
            return new self(...$data);
        } catch (\Error $ex) {
            // clé inconnue, clé manquante, valeur qui n'est pas une chaîne...
            throw new ExceptionImpots("Le fichier [$databaseFilename] est incorrect ({$ex->getMessage()})", 21, $ex);
        }
    }

    // les noms des colonnes des constantes de calcul : [nom de la constante => nom de la colonne]
    public function colonnesConstantes(): array
    {
        $colonnes = [];
        foreach (self::CONSTANTES as $constante) {
            // la colonne de la constante [plafondQfDemiPart] est dans l'attribut [colPlafondQfDemiPart]
            $colonnes[$constante] = $this->{"col" . ucfirst($constante)};
        }
        return $colonnes;
    }
}

Commentaires

  • ligne 9 : comme [TaxAdminData], la classe [Database] est immuable et ne peut être dérivée ;
  • lignes 12-23 : les noms des dix constantes de calcul de l'impôt. Ce sont aussi les noms des attributs de la classe [TaxAdminData] ;
  • lignes 26-50 : le constructeur, privé, déclare par promotion un attribut public de type [string] pour chaque clé du fichier [database.json], avec le même nom ;
  • ligne 30 : l'attribut PHP 8.2 [#[\SensitiveParameter]] marque le paramètre comme sensible. Si une exception est lancée pendant l'exécution du constructeur, la valeur du mot de passe n'apparaîtra pas dans la pile des appels (stack trace) de l'exception, qui est souvent écrite dans des fichiers de logs : elle y est remplacée par un objet [SensitiveParameterValue] ;
  • lignes 53-68 : la méthode statique [fromJsonFile] construit un objet [Database] à partir d'un fichier JSON :
    • ligne 56 : la lecture et le décodage du fichier JSON sont confiés à la classe [Utilitaires] présentée plus loin. Ils peuvent lancer une exception [ExceptionImpots] ;
    • ligne 58 : le JSON doit être un objet, donc un tableau associatif et non une liste ;
    • ligne 63 : les clés du tableau [$data] deviennent des arguments nommés du constructeur. Si une clé manque, PHP lance une erreur [ArgumentCountError] ; si une clé est inconnue, une erreur [Error] (« Unknown named parameter ») ; si une valeur n'est pas une chaîne, une erreur [TypeError]. Ces trois erreurs dérivent de la classe [Error] ;
    • lignes 64-66 : on intercepte l'erreur et on la transforme en [ExceptionImpots] de code 21, en gardant l'erreur originelle ([$ex]) comme exception précédente. Le message de l'erreur PHP, inclus dans le message, indique précisément ce qui ne va pas ;
  • lignes 71-79 : la méthode [colonnesConstantes] rend le tableau associatif [nom de la constante => nom de la colonne]. Pour la constante [plafondQfDemiPart], le nom de la colonne est dans l'attribut [colPlafondQfDemiPart]. La fonction [ucfirst] met en majuscule la première lettre d'une chaîne, et la syntaxe [$this->{"col" . ucfirst($constante)}] désigne l'attribut dont le nom est la valeur de l'expression entre accolades. Cette méthode nous évitera d'écrire dix fois du code presque identique dans la couche [dao] .

13.3.2.3. La classe [Utilitaires]

La version 4 utilisait la classe [Utilitaires] pour enlever la marque de fin de ligne des lignes d'un fichier texte. Nous n'utilisons plus que des fichiers JSON : la classe [Utilitaires] rassemble désormais la lecture et l'écriture des fichiers JSON [Utilities/Utilitaires.php] :

<?php

declare(strict_types=1);

// espace de noms
namespace Application;

// une classe de fonctions utilitaires : elle n'est ni instanciable, ni dérivable
final class Utilitaires
{
    // constructeur privé : la classe ne peut être instanciée
    private function __construct()
    {
    }

    // lit un fichier JSON et rend son contenu décodé (les objets JSON deviennent des tableaux associatifs)
    public static function lireFichierJson(string $fileName): mixed
    {
        // on récupère le contenu du fichier
        $contenu = is_readable($fileName) ? file_get_contents($fileName) : false;
        if ($contenu === false) {
            throw new ExceptionImpots("Le fichier [$fileName] n'existe pas ou ne peut être lu", 15);
        }
        // on décode le JSON
        try {
            return json_decode($contenu, true, flags: JSON_THROW_ON_ERROR);
        } catch (\JsonException $ex) {
            throw new ExceptionImpots("Le fichier [$fileName] ne contient pas du JSON valide ({$ex->getMessage()})", 16, $ex);
        }
    }

    // enregistre une valeur au format JSON dans un fichier texte (créé s'il n'existe pas, écrasé sinon)
    public static function écrireFichierJson(string $fileName, mixed $data): void
    {
        // JSON indenté, caractères accentués non échappés, réels toujours écrits avec un point décimal (0.0)
        $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR);
        if (@file_put_contents($fileName, "$json\n") === false) {
            throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier [$fileName]", 14);
        }
    }
}

Commentaires

  • lignes 17-30 : la méthode [lireFichierJson] lit un fichier JSON et rend son contenu décodé, de type quelconque ([mixed]) : un objet JSON donne un tableau associatif, un tableau JSON une liste. Le code est celui que nous avons écrit dans la méthode [TaxAdminData::fromJsonFile] de la version 3. Les erreurs sont signalées par une [ExceptionImpots] de code 15 (fichier illisible) ou 16 (JSON invalide) ;
  • lignes 33-40 : la méthode [écrireFichierJson] enregistre une valeur au format JSON dans un fichier texte. Les options de [json_encode] sont combinées par l'opérateur [|] :
    • [JSON_PRETTY_PRINT] produit un JSON indenté, facile à lire ;
    • [JSON_UNESCAPED_UNICODE] écrit les caractères accentués tels quels (marié) au lieu de leur code (mari\u00e9) ;
    • [JSON_PRESERVE_ZERO_FRACTION] écrit le réel 0.0 sous la forme 0.0 et non 0 : on distingue ainsi les réels des entiers dans le fichier produit ;
    • [JSON_THROW_ON_ERROR] fait lancer une exception [JsonException] si la valeur ne peut être codée en JSON .

13.3.3. La couche [dao]

Maintenant nous pouvons écrire le code qui va transférer les données du fichier JSON [taxadmindata.json] dans les tables [tbtranches, tbconstantes] de la base MySQL [dbimpots-2019]. Nous adopterons l'architecture suivante :

Image

La couche [dao] implémentera l'interface [InterfaceDao4TransferAdminData2Database] suivante [Dao/InterfaceDao4TransferAdminData2Database.php] :

<?php

declare(strict_types=1);

// espace de noms
namespace Application;

// l'interface de la couche [dao] de transfert des données fiscales dans une base de données
interface InterfaceDao4TransferAdminData2Database
{
    // enregistre les données de l'administration fiscale dans la base de données
    // lance une exception [ExceptionImpots] en cas d'erreur
    public function transferAdminData2Database(): void;
}
  • ligne 13 : 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 [Dao/DaoTransferAdminDataFromJsonFile2Database.php] :

<?php

declare(strict_types=1);

// espace de noms
namespace Application;

// transfert des données de l'administration fiscale d'un fichier JSON vers une base de données
class DaoTransferAdminDataFromJsonFile2Database implements InterfaceDao4TransferAdminData2Database
{
    // la description de la base de données cible
    private readonly Database $database;
    // les données de l'administration fiscale
    private readonly TaxAdminData $taxAdminData;

    // le constructeur
    public function __construct(string $databaseFilename, string $taxAdminDataFilename)
    {
        // peuvent lancer une exception [ExceptionImpots]
        $this->database = Database::fromJsonFile($databaseFilename);
        $this->taxAdminData = TaxAdminData::fromJsonFile($taxAdminDataFilename);
    }

    // transfère les données fiscales dans la base de données
    #[\Override]
    public function transferAdminData2Database(): void
    {
        $database = $this->database;
        try {
            // on ouvre la connexion à la base de données
            $connexion = \PDO::connect($database->dsn, $database->id, $database->pwd);
            // 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'erreur au code appelant sous la forme d'une exception [ExceptionImpots]
            throw new ExceptionImpots("Erreur d'accès à la base de données ({$ex->getMessage()})", 30, $ex);
        }
        // la connexion est fermée lorsque la variable [$connexion] disparaît, à la fin de la méthode
    }

Commentaires

Nous utilisons ici ce que nous avons appris dans le chapitre sur MySQL.

  • ligne 9 : la classe [DaoTransferAdminDataFromJsonFile2Database] implémente l'interface [InterfaceDao4TransferAdminData2Database] ;
  • ligne 12 : l'attribut [$database] est l'objet de type [Database] encapsulant les données du fichier [database.json] ;
  • ligne 14 : l'attribut [$taxAdminData] est l'objet de type [TaxAdminData] encapsulant les données du fichier [taxadmindata.json] ;
  • lignes 17-22 : le constructeur reçoit en paramètres les noms des fichiers [database.json, taxadmindata.json] et initialise les deux attributs. Si l'un des fichiers est incorrect, une exception [ExceptionImpots] remonte au code appelant ;
  • ligne 26 : on implémente l'unique méthode de l'interface [InterfaceDao4TransferAdminData2Database] ;
  • ligne 31 : 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 désignerait donc la classe [Application\PDO] qui n'existe pas. La méthode [PDO::connect] rend ici un objet [Pdo\Mysql] puisque le DSN commence par [mysql:]. Rien dans cette classe ne dépend de MySQL : c'est le DSN, lu dans le fichier [database.json], qui détermine le SGBD utilisé ;
  • lignes 33-39 : on remplit les tables [tbtranches, tbconstantes] au sein d'une transaction. Les méthodes [fillTableTranches] et [fillTableConstantes] lancent une [PDOException] dès que quelque chose se passe mal. Dans ce cas, l'exécution se poursuit dans la clause [catch] et la base reste dans son état initial : on ne risque pas d'avoir une table [tbtranches] remplie et une table [tbconstantes] vide ;
  • lignes 40-47 : cas où une exception a été lancée par le code. Si une transaction est en cours, elle est annulée. Puis on lance une exception de type [ExceptionImpots], de code 30, avec le message de l'exception originelle, qui devient l'exception précédente. Le code appelant n'a ainsi qu'un type d'exception à gérer ;
  • ligne 49 : le cours PHP 7 fermait la connexion dans une clause [finally] par l'instruction [$connexion = NULL]. C'est inutile : la variable locale [$connexion] disparaît à la fin de la méthode, et avec elle la connexion .

Le code de la méthode [fillTableTranches] est le suivant :

    // remplissage de la table des tranches d'impôt
    private function fillTableTranches(\PDO $connexion): void
    {
        // raccourcis
        $database = $this->database;
        $taxAdminData = $this->taxAdminData;
        // on vide la table au cas où il y aurait quelque chose dedans
        $connexion->exec("delete from $database->tableTranches");
        // on prépare les insertions
        $statement = $connexion->prepare(
            "insert into $database->tableTranches ($database->colLimites, $database->colCoeffR, $database->colCoeffN)"
            . " values (:limites, :coeffR, :coeffN)"
        );
        // on exécute l'ordre préparé avec les valeurs de chaque tranche d'impôt
        foreach ($taxAdminData->limites as $i => $limite) {
            $statement->execute([
                "limites" => $limite,
                "coeffR" => $taxAdminData->coeffR[$i],
                "coeffN" => $taxAdminData->coeffN[$i],
            ]);
        }
    }

Commentaires

  • ligne 53 : 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 56-57 : deux raccourcis pour éviter d'avoir à écrire [$this->database] et [$this->taxAdminData] dans tout le code. On a là des copies de références d'objets et non pas des copies des objets eux-mêmes ;
  • ligne 59 : on supprime le contenu actuel de la table des tranches. Le nom de la table est inséré dans la chaîne par la syntaxe ["$database->tableTranches"] : PHP remplace l'expression par la valeur de l'attribut ;
  • lignes 61-64 : on prépare l'insertion de lignes dans la table. On utilise ici les noms de la table et des colonnes fournis par l'attribut [$database]. Les noms des tables et des colonnes ne peuvent pas être des paramètres d'un ordre préparé : seules les valeurs peuvent l'être. C'est pourquoi ils sont insérés dans le texte de l'ordre SQL. Ils viennent d'un fichier de configuration et non d'un utilisateur : il n'y a pas de risque d'injection SQL ;
  • lignes 66-72 : on exécute autant de fois que nécessaire l'instruction d'insertion préparée, une fois par tranche d'impôt. [$i] est le numéro de la tranche, qui sert d'indice dans les tableaux [coeffR] et [coeffN] .

Le code de la méthode [fillTableConstantes] est le suivant :

    // remplissage de la table des constantes
    private function fillTableConstantes(\PDO $connexion): void
    {
        // raccourci
        $database = $this->database;
        // on vide la table au cas où il y aurait quelque chose dedans
        $connexion->exec("delete from $database->tableConstantes");
        // [nom de la constante => nom de la colonne]
        $colonnes = $database->colonnesConstantes();
        // les paramètres de l'ordre préparé portent les noms des constantes : :plafondQfDemiPart, ...
        $paramètres = array_map(fn(string $constante): string => ":$constante", array_keys($colonnes));
        $statement = $connexion->prepare(
            "insert into $database->tableConstantes (" . implode(", ", $colonnes) . ")"
            . " values (" . implode(", ", $paramètres) . ")"
        );
        // les valeurs des paramètres : [nom de la constante => valeur de la constante]
        $valeurs = [];
        foreach (array_keys($colonnes) as $constante) {
            $valeurs[$constante] = $this->taxAdminData->$constante;
        }
        // on exécute l'ordre préparé : une seule ligne est insérée
        $statement->execute($valeurs);
    }

Commentaires

  • ligne 76 : la méthode [fillTableConstantes] reçoit elle aussi une connexion ouverte dans laquelle une transaction est en cours ;
  • ligne 81 : la table [tbconstantes] est vidée ;
  • ligne 83 : le tableau [nom de la constante => nom de la colonne], par exemple [["plafondQfDemiPart" => "plafondQfDemiPart", ...]] ;
  • ligne 85 : les noms des paramètres de l'ordre préparé : [[":plafondQfDemiPart", ...]]. Ce sont les noms des constantes précédés de deux-points ;
  • lignes 86-89 : préparation de l'ordre SQL d'insertion. La fonction [implode] construit la liste des colonnes et celle des paramètres, séparés par des virgules. On obtient l'ordre [insert into tbconstantes (plafondQfDemiPart, ...) values (:plafondQfDemiPart, ...)]. Le cours PHP 7 écrivait cet ordre en entier, colonne par colonne, sur une vingtaine de lignes ;
  • lignes 91-94 : le tableau des valeurs des paramètres [nom de la constante => valeur]. La syntaxe [$this->taxAdminData->$constante] désigne l'attribut de l'objet [TaxAdminData] dont le nom est la valeur de la variable [$constante] ;
  • ligne 96 : exécution de l'ordre d'insertion. Il n'y a qu'une ligne à insérer .

13.3.4. Le script principal

Le script principal [Main/MainTransferAdminDataFromJsonFile2MySQLDatabase.php] s'appuie sur la couche [dao] pour opérer le transfert de données :

<?php

// respect strict des types déclarés des paramètres et des résultats des fonctions
declare(strict_types=1);

// espace de noms
namespace Application;

// inclusion des interfaces et classes nécessaires au transfert
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../Dao/DaoTransferAdminDataFromJsonFile2Database.php";

// définition des constantes
const DATABASE_CONFIG_FILENAME = __DIR__ . "/../Data/database.json";
const TAXADMINDATA_FILENAME = __DIR__ . "/../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();
    print "Les données de l'administration fiscale ont été transférées dans la base de données\n";
} catch (ExceptionImpots $ex) {
    // on affiche l'erreur
    print "Erreur n° {$ex->getCode()} : {$ex->getMessage()}\n";
}
// fin
print "Terminé\n";

Commentaires

  • lignes 10-15 : chargement des classes et interfaces nécessaires au transfert. Les chemins sont relatifs au dossier du script ([Main]) : [__DIR__ . "/../Entities"] désigne le dossier [Entities] voisin ;
  • lignes 18-19 : les chemins des deux fichiers JSON, eux aussi relatifs au dossier du script. Le cours PHP 7 utilisait des chemins relatifs au dossier courant (["../Data/database.json"]) : le script ne fonctionnait que s'il était lancé depuis le dossier [Main] ;
  • ligne 23 : on instancie la couche [dao] en passant au constructeur les noms des deux fichiers JSON ;
  • ligne 25 : on opère le transfert de données ;
  • lignes 27-29 : on affiche le code et le message de l'exception. Le cours PHP 7 utilisait ici la fonction [utf8_encode], dépréciée depuis PHP 8.2 et inutile puisque tous nos messages sont en UTF-8 .

Lorsque nous exécutons ce script dans un terminal ouvert dans le dossier [Main], nous obtenons le résultat suivant :

1
2
3
> php MainTransferAdminDataFromJsonFile2MySQLDatabase.php
Les données de l'administration fiscale ont été transférées dans la base de données
Terminé

et le contenu suivant dans la base de données :

> mysql -u admimpots -pmdpimpots dbimpots-2019 -e "select * from tbtranches"
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+-----------+--------+----------+
| id | limites   | coeffr | coeffn   |
+----+-----------+--------+----------+
|  1 |   9964.00 |   0.00 |     0.00 |
|  2 |  27519.00 |   0.14 |  1394.96 |
|  3 |  73779.00 |   0.30 |  5798.00 |
|  4 | 156244.00 |   0.41 | 13913.69 |
|  5 |      0.00 |   0.45 | 20163.45 |
+----+-----------+--------+----------+
  • colonne [id] : les valeurs attribuées par MySQL à la clé primaire. La numérotation démarre à 1. Si on exécute de nouveau le script, les lignes sont supprimées puis réinsérées et MySQL continue la numérotation (6, 7…) : une valeur de clé auto-incrémentée n'est jamais réutilisée ;
  • les nombres ont deux décimales, conformément au type [DECIMAL(10, 2)] des colonnes .

L'option [\G] à la fin de l'ordre SQL fait afficher chaque ligne verticalement, une colonne par ligne d'écran, ce qui est plus lisible pour une table ayant beaucoup de colonnes :

> mysql -u admimpots -pmdpimpots dbimpots-2019 -e "select * from tbconstantes\G"
mysql: [Warning] Using a password on the command line interface can be insecure.
*************************** 1. row ***************************
                                    id: 1
                     plafondQfDemiPart: 1551.00
plafondRevenusCelibatairePourReduction: 21037.00
     plafondRevenusCouplePourReduction: 42074.00
                   valeurReducDemiPart: 3797.00
              plafondDecoteCelibataire: 1196.00
                   plafondDecoteCouple: 1970.00
     plafondImpotCelibatairePourDecote: 1595.00
          plafondImpotCouplePourDecote: 2627.00
              abattementDixPourcentMax: 12502.00
              abattementDixPourcentMin: 437.00

13.4. Calcul de l'impôt

13.4.1. Architecture

La version 4 de l'application de calcul de l'impôt utilisait une architecture en couches :

Image

La couche [dao] implémente une interface [InterfaceDao]. La classe [DaoImpotsWithTaxAdminDataInJsonFile] de la version 4 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 la 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.

Image

13.4.2. L'entité [TaxPayerData]

La classe [TaxPayerData] de la version 4 encapsule les données d'un contribuable [marié, enfants, salaire] et les résultats du calcul de son impôt [impôt, surcôte, décôte, réduction, taux]. Elle est reprise telle quelle. Les données des contribuables sont désormais dans le fichier JSON [Data/taxpayersdata.json] suivant :

[
    {
        "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": 200000
    }
]

Les clés des objets JSON sont les noms des attributs de la classe [TaxPayerData]. On notera :

  • ligne 8 : le contribuable n° 1 (les numéros commencent à 0) est erroné : ses trois données sont invalides ;
  • ligne 14 : le nombre d'enfants du contribuable n° 2 est donné sous la forme d'une chaîne de caractères. Nous l'accepterons ;
  • les onze autres contribuables sont ceux des tests officiels de l'administration fiscale, utilisés depuis la version 1 .

13.4.3. La couche [dao]

13.4.3.1. L'interface [InterfaceDao]

L'interface [InterfaceDao] de la version 4 est reprise sans modification [Dao/InterfaceDao.php] :

<?php

declare(strict_types=1);

// espace de noms
namespace Application;

// l'interface de la couche [dao]
// toutes les méthodes lancent une exception [ExceptionImpots] en cas d'erreur irrécupérable
interface InterfaceDao
{
    // lecture des données contribuables
    // les lignes erronées sont signalées dans le fichier texte [$errorsFilename]
    /** @return TaxPayerData[] */
    public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

    // lecture des données de l'administration fiscale (tranches d'impôt et constantes)
    public function getTaxAdminData(): TaxAdminData;

    // enregistrement des résultats
    /** @param TaxPayerData[] $taxPayersData */
    public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

Les méthodes [getTaxPayersData] et [saveResults] reçoivent des noms de fichiers : rien n'y indique qu'il s'agit de fichiers texte basiques ou de fichiers JSON. L'interface reste donc valable.

13.4.3.2. Le trait [TraitDao]

Le trait [TraitDao] implémente les méthodes [getTaxPayersData] et [saveResults] de l'interface [InterfaceDao]. Comme on est passé de fichiers texte basiques à des fichiers JSON, il nous faut le réécrire [Dao/TraitDao.php] :

<?php

declare(strict_types=1);

// espace de noms
namespace Application;

// implémentation des méthodes de [InterfaceDao] qui travaillent avec des fichiers JSON
trait TraitDao
{
    // lecture des données contribuables dans un fichier JSON
    #[\Override]
    public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array
    {
        // le contenu du fichier : un tableau d'objets {marié, enfants, salaire}
        // peut lancer une exception [ExceptionImpots]
        $contribuables = Utilitaires::lireFichierJson($taxPayersFilename);
        if (!is_array($contribuables) || !array_is_list($contribuables)) {
            throw new ExceptionImpots("Le fichier [$taxPayersFilename] ne contient pas un tableau JSON", 17);
        }
        // tableau des données contribuables
        $taxPayersData = [];
        // tableau des erreurs
        $errors = [];
        // on vérifie chacun des contribuables
        foreach ($contribuables as $numéro => $contribuable) {
            $erreurs = $this->check($contribuable);
            if ($erreurs === []) {
                // un contribuable de plus
                $taxPayersData[] = new TaxPayerData(
                    strtolower(trim($contribuable["marié"])),
                    (int) $contribuable["enfants"],
                    (int) $contribuable["salaire"]
                );
            } else {
                // une erreur de plus : on note le numéro du contribuable erroné dans le tableau JSON
                $errors[] = ["numéro" => $numéro, "erreurs" => $erreurs];
            }
        }
        // on enregistre les erreurs dans un fichier JSON
        Utilitaires::écrireFichierJson($errorsFilename, $errors);
        // résultat de la fonction
        return $taxPayersData;
    }

    // vérification des données d'un contribuable : rend la liste des attributs erronés
    private function check(mixed $contribuable): array
    {
        // le contribuable doit être un objet JSON
        if (!is_array($contribuable)) {
            return [["contribuable" => $contribuable]];
        }
        $erreurs = [];
        // le statut marital doit être oui ou non
        $marié = $contribuable["marié"] ?? null;
        if (!is_string($marié) || !in_array(strtolower(trim($marié)), ["oui", "non"], true)) {
            $erreurs[] = ["marié" => $marié];
        }
        // le nombre d'enfants et le salaire doivent être des entiers positifs ou nuls
        // donnés sous la forme d'un nombre (2) ou d'une chaîne ("2")
        foreach (["enfants", "salaire"] as $clé) {
            $valeur = $contribuable[$clé] ?? null;
            $valide = (is_int($valeur) && $valeur >= 0)
                || (is_string($valeur) && preg_match('/^\s*\d+\s*$/', $valeur) === 1);
            if (!$valide) {
                $erreurs[] = [$clé => $valeur];
            }
        }
        return $erreurs;
    }

    // enregistrement des résultats dans un fichier JSON
    #[\Override]
    public function saveResults(string $resultsFilename, array $taxPayersData): void
    {
        // les objets [TaxPayerData] sont transformés en objets JSON à partir de leurs attributs publics
        Utilitaires::écrireFichierJson($resultsFilename, $taxPayersData);
    }
}

Commentaires

  • lignes 13-44 : 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] ;
  • ligne 17 : le contenu du fichier JSON des données des contribuables est décodé dans le tableau [$contribuables]. Si le fichier JSON s'avère inexploitable, une exception [ExceptionImpots] est lancée ;
  • ligne 18 : le contenu doit être un tableau JSON, donc une liste PHP. Sinon, une exception de code 17 est lancée ;
  • ligne 22 : le tableau [$taxPayersData] va contenir les données des contribuables encapsulées dans des objets de type [TaxPayerData] ;
  • ligne 24 : on va cumuler les erreurs dans le tableau [$errors] ;
  • lignes 26-39 : construction du tableau [$taxPayersData]. Avant d'être encapsulées dans un objet [TaxPayerData], les données sont vérifiées par la méthode [check], qui rend la liste des attributs erronés, vide si les données sont correctes :
    • lignes 30-34 : cas où les données sont valides. Un nouvel objet [TaxPayerData] est construit et ajouté au tableau [$taxPayersData]. Le statut marital est mis en minuscules, le nombre d'enfants et le salaire convertis en entiers : [(int) "2"] vaut 2 ;
    • ligne 37 : cas où les données sont invalides. On note dans l'erreur le numéro du contribuable erroné dans le tableau JSON pour que l'utilisateur puisse le retrouver, puis l'erreur est ajoutée au tableau [$errors] ;
  • ligne 41 : on enregistre les erreurs rencontrées dans le fichier JSON [$errorsFilename]. Le cours PHP 7 écrivait une chaîne JSON par erreur, une par ligne : avec plusieurs erreurs, le fichier n'était pas un fichier JSON valide. Nous écrivons un tableau JSON des erreurs ;
  • ligne 43 : on rend le tableau des objets [TaxPayerData] construits : c'était l'objectif de la méthode ;
  • lignes 47-70 : la méthode privée [check] vérifie la validité des données d'un contribuable. Le paramètre [$contribuable] est de type [mixed] : un élément du tableau JSON peut être n'importe quoi (un objet, un nombre, une chaîne…). La méthode cumule les attributs erronés dans le tableau [$erreurs] sous la forme de tableaux [attribut erroné => valeur] :
    • ligne 50 : l'élément doit être un objet JSON ;
    • ligne 55 : l'opérateur [??] (null coalescing) rend la valeur de [$contribuable["marié"]] si elle existe et n'est pas [null], et [null] sinon. On évite ainsi l'avertissement que provoquerait une clé absente ;
    • lignes 56-58 : le statut marital doit être une chaîne valant oui ou non, sans tenir compte de la casse et des espaces ;
    • lignes 61-68 : le nombre d'enfants et le salaire doivent être des entiers positifs ou nuls, donnés sous la forme d'un nombre JSON (2) ou d'une chaîne ("2"). Une chaîne doit être formée de chiffres, éventuellement entourés d'espaces : c'est ce que vérifie l'expression régulière [/^\s*\d+\s*$/]. On notera la comparaison [preg_match(...) === 1] : la fonction [preg_match] rend 1 si la chaîne correspond au modèle, 0 sinon, et [false] en cas d'erreur ;
  • lignes 74-78 : l'implémentation de la méthode [saveResults] de l'interface [InterfaceDao]. Le tableau des objets [TaxPayerData] est directement enregistré au format JSON : la fonction [json_encode] transforme un objet en objet JSON dont les clés sont les noms de ses attributs publics. Le cours PHP 7 construisait la chaîne JSON du tableau « à la main », en concaténant les chaînes JSON des objets séparées par des virgules et en ajoutant les crochets .

13.4.3.3. La classe [DaoImpotsWithTaxAdminDataInDatabase]

La classe [DaoImpotsWithTaxAdminDataInDatabase] implémente l'interface [InterfaceDao] de la façon suivante [Dao/DaoImpotsWithTaxAdminDataInDatabase.php] :

<?php

declare(strict_types=1);

// espace de noms
namespace Application;

// une couche [dao] qui trouve les données de l'administration fiscale dans une base de données
class DaoImpotsWithTaxAdminDataInDatabase implements InterfaceDao
{
    // le trait implémente les méthodes [getTaxPayersData] et [saveResults] de l'interface
    use TraitDao;

    // la description de la base de données
    private readonly Database $database;
    // l'objet de type [TaxAdminData] qui contient les données de l'administration fiscale
    private readonly TaxAdminData $taxAdminData;

    // le constructeur
    public function __construct(string $databaseFilename)
    {
        // la description de la base de données - peut lancer une exception [ExceptionImpots]
        $this->database = Database::fromJsonFile($databaseFilename);
        $database = $this->database;
        try {
            // on ouvre la connexion à la base de données
            // les lignes lues seront rendues sous la forme de tableaux associatifs [colonne => valeur]
            $connexion = \PDO::connect(
                $database->dsn,
                $database->id,
                $database->pwd,
                [\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC]
            );
            // on démarre une transaction
            $connexion->beginTransaction();
            // on lit la table des tranches d'impôt
            $tranches = $this->getTranches($connexion);
            // on lit la table des constantes
            $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'erreur au code appelant sous la forme d'une exception [ExceptionImpots]
            throw new ExceptionImpots("Erreur d'accès à la base de données ({$ex->getMessage()})", 30, $ex);
        }
        // construction de l'objet [TaxAdminData] : les données lues sont vérifiées
        $this->taxAdminData = TaxAdminData::fromArray([...$tranches, ...$constantes]);
    }

Commentaires

  • ligne 6 : on garde l'espace de noms déjà utilisé pour les autres classes de l'application ;
  • ligne 9 : la classe [DaoImpotsWithTaxAdminDataInDatabase] implémente l'interface [InterfaceDao] ;
  • ligne 12 : 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], qui se contente de rendre l'attribut privé [$taxAdminData]. On en déduit que le constructeur devra initialiser cet attribut. C'est son unique rôle ;
  • ligne 20 : le constructeur reçoit comme unique paramètre le nom du fichier JSON [database.json] qui définit la base de données MySQL [dbimpots-2019] ;
  • ligne 23 : le fichier JSON est utilisé pour créer un objet de type [Database] mémorisé dans l'attribut [$database]. Si le fichier JSON n'a pu être exploité correctement, une exception [ExceptionImpots] est lancée ;
  • lignes 28-33 : on ouvre la connexion à la base de données. Le 4e paramètre fixe le mode de lecture par défaut des lignes : des tableaux associatifs [colonne => valeur] ;
  • ligne 35 : on démarre une transaction. Elle n'est pas indispensable 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 : les deux tables sont lues dans le même état, même si un autre programme les modifie au même moment ;
  • ligne 37 : la lecture de la table des tranches d'imposition [tbtranches] est faite par la méthode privée [getTranches]. Elle rend le tableau [["limites" => [...], "coeffR" => [...], "coeffN" => [...]]] ;
  • ligne 39 : la lecture de la table des constantes de calcul [tbconstantes] est faite par la méthode privée [getConstantes]. Elle rend le tableau [nom de la constante => valeur] ;
  • ligne 41 : si on arrive à cette ligne, c'est que tout s'est bien passé. On valide donc la transaction ;
  • lignes 42-49 : si on arrive là, c'est qu'une exception s'est produite. On annule donc la transaction s'il y en avait une en cours. Puis, pour avoir des exceptions homogènes, on relance l'erreur sous la forme d'une exception de type [ExceptionImpots] de code 30 ;
  • ligne 52 : l'expression [[...$tranches, ...$constantes]] fusionne les deux tableaux associatifs en un seul, qui a exactement les treize clés attendues par la méthode [TaxAdminData::fromArray]. Celle-ci vérifie les données lues dans la base, comme elle vérifiait celles du fichier JSON, et les convertit en réels. Le cours PHP 7 initialisait l'objet [TaxAdminData] attribut par attribut, avec dix appels de setters .

La méthode [getTranches] est la suivante :

    // lecture de la table des tranches d'impôt
    // rend le tableau ["limites" => [...], "coeffR" => [...], "coeffN" => [...]]
    private function getTranches(\PDO $connexion): array
    {
        // raccourci
        $database = $this->database;
        // on prépare puis on exécute la requête SELECT
        $statement = $connexion->prepare(
            "select $database->colLimites, $database->colCoeffR, $database->colCoeffN"
            . " from $database->tableTranches order by $database->colId"
        );
        $statement->execute();
        // toutes les lignes en une fois : un tableau de tableaux associatifs
        $tranches = $statement->fetchAll();
        // on extrait chaque colonne du résultat
        return [
            "limites" => array_column($tranches, $database->colLimites),
            "coeffR" => array_column($tranches, $database->colCoeffR),
            "coeffN" => array_column($tranches, $database->colCoeffN),
        ];
    }

Commentaires

  • ligne 57 : la méthode reçoit en paramètre une connexion ouverte dans laquelle une transaction est en cours ;
  • lignes 62-65 : l'ordre SELECT est préparé, puis exécuté. La clause [order by] sur la clé primaire garantit que les tranches sont relues dans l'ordre où elles ont été insérées. Le cours PHP 7 n'avait pas cette clause et comptait sur l'ordre dans lequel MySQL rendait les lignes, ce qui n'est pas garanti ;
  • ligne 68 : toutes les lignes du résultat sont lues en une fois. On obtient un tableau de tableaux associatifs, par exemple [[["limites" => "9964.00", "coeffr" => "0.00", "coeffn" => "0.00"], ...]]. On remarquera que les valeurs des colonnes de type [DECIMAL] sont rendues sous forme de chaînes de caractères : PHP n'a pas de type décimal exact et le pilote ne les convertit pas en [float], qui ferait perdre l'exactitude. La méthode [TaxAdminData::fromArray] fera la conversion ;
  • lignes 70-74 : la fonction [array_column] extrait une colonne d'un tableau de tableaux associatifs : [array_column($tranches, "limites")] rend [["9964.00", "27519.00", ...]] .

La méthode privée [getConstantes] est la suivante :

    // lecture de la table des constantes de calcul
    // rend le tableau [nom de la constante => valeur]
    private function getConstantes(\PDO $connexion): array
    {
        // [nom de la constante => nom de la colonne]
        $colonnes = $this->database->colonnesConstantes();
        // on prépare puis on exécute la requête SELECT
        $statement = $connexion->prepare(
            "select " . implode(", ", $colonnes) . " from {$this->database->tableConstantes}"
        );
        $statement->execute();
        // on exploite le résultat - 1 seule ligne ici - [false] si la table est vide
        $ligne = $statement->fetch() ?: [];
        // [nom de la constante => valeur de sa colonne] (null si la table est vide)
        return array_map(fn(string $colonne): mixed => $ligne[$colonne] ?? null, $colonnes);
    }

Commentaires

  • ligne 82 : le tableau [nom de la constante => nom de la colonne] ;
  • lignes 84-86 : l'ordre SELECT des dix colonnes est préparé, puis exécuté. On notera les accolades dans [{$this->database->tableConstantes}] : sans elles, PHP n'interpréterait dans la chaîne que [$this->database] ;
  • ligne 89 : la table n'a qu'une ligne, lue par la méthode [fetch]. Celle-ci rend [false] s'il n'y a pas de ligne. L'opérateur [?:] rend son opérande gauche s'il est évalué à vrai, son opérande droit sinon : [$ligne] vaut donc un tableau vide si la table est vide ;
  • ligne 91 : pour chaque constante, on va chercher dans [$ligne] la valeur de sa colonne. La fonction [array_map] appliquée à un seul tableau conserve ses clés : on obtient le tableau [nom de la constante => valeur]. Si la table est vide, toutes les valeurs sont [null] et la méthode [TaxAdminData::fromArray] signalera une valeur invalide .

On remarquera que la classe ne dépend pas du SGBD MySQL. C'est le fichier [database.json] qui fixe le SGBD utilisé via le DSN de la base de données.

13.4.4. La couche [métier]

Image

  • nous venons d'implémenter la couche [dao] [3] ;
  • parce que nous avons respecté l'interface [InterfaceDao], la couche [métier] [2] peut rester inchangée. Le cours PHP 7 devait cependant la modifier, parce qu'il avait aussi changé les entités échangées entre les couches : la méthode [executeBatchImpots] utilisait les setters de la classe [TaxPayerData] de la version 4, qui avaient disparu. Ici, les entités n'ont pas changé .

L'interface [InterfaceMetier] et la classe [Metier] de la version 4 sont donc reprises sans aucune modification dans le dossier [Metier]. Rappelons la méthode [executeBatchImpots] :

    // calcul des impôts en mode batch
    // --------------------------------------------------------------------------
    #[\Override]
    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);
        // on calcule l'impôt de chacun des contribuables
        $results = array_map(
            fn(TaxPayerData $taxPayerData): TaxPayerData => $taxPayerData->avecRésultats(
                $this->calculerImpot($taxPayerData->marié, $taxPayerData->enfants, $taxPayerData->salaire)
            ),
            $taxPayersData
        );
        // enregistrement des résultats
        $this->dao->saveResults($resultsFileName, $results);
    }
}
  • ligne 192 : la couche [métier] ne sait pas que les données des contribuables viennent désormais d'un fichier JSON : elle demande les données à la couche [dao] et reçoit un tableau d'objets [TaxPayerData], comme dans la version 4 ;
  • ligne 201 : de même, elle ignore que les résultats sont enregistrés au format JSON .

13.4.5. Le script principal

Image

  • nous venons d'implémenter les couches [dao] [3] et [métier] [2] ;
  • il nous reste à écrire le script principal [1] .

Le script principal [Main/MainCalculateImpotsWithTaxAdminDataInMySQLDatabase.php] est analogue à celui de la version 4 :

<?php

// respect strict des types déclarés des paramètres et des résultats des fonctions
declare(strict_types=1);

// espace de noms
namespace Application;

// inclusion des interfaces et classes
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.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__ . "/../Metier/InterfaceMetier.php";
require_once __DIR__ . "/../Metier/Metier.php";

// définition des constantes
const DATABASE_CONFIG_FILENAME = __DIR__ . "/../Data/database.json";
const TAXPAYERSDATA_FILENAME = __DIR__ . "/../Data/taxpayersdata.json";
const RESULTS_FILENAME = __DIR__ . "/../Data/resultats.json";
const ERRORS_FILENAME = __DIR__ . "/../Data/errors.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ôt en mode batch
    $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
    print "Les résultats ont été enregistrés dans le fichier [resultats.json]\n";
} catch (ExceptionImpots $ex) {
    // on affiche l'erreur
    print "Erreur n° {$ex->getCode()} : {$ex->getMessage()}\n";
}
// fin
print "Terminé\n";

Commentaires

  • lignes 10-19 : chargement de tous les fichiers nécessaires au calcul de l'impôt ;
  • lignes 22-25 : les noms des différents fichiers JSON de l'application ;
  • ligne 29 : construction de la couche [dao] ;
  • ligne 31 : construction de la couche [métier] ;
  • ligne 33 : appel de la méthode [executeBatchImpots] de la couche [métier] .

Résultats

1
2
3
> php MainCalculateImpotsWithTaxAdminDataInMySQLDatabase.php
Les résultats ont été enregistrés dans le fichier [resultats.json]
Terminé

L'application produit deux fichiers JSON dans le dossier [Data] :

  • [resultats.json] : les résultats des différents calculs d'impôts ;
  • [errors.json] : qui signale les erreurs trouvées dans le fichier JSON [taxpayersdata.json] .

Le fichier [errors.json] est le suivant :

[
    {
        "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é, et pourquoi. Le fichier des résultats [resultats.json] est lui le suivant :

[
    {
        "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.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
    }
]

On notera que le contribuable n° 2 du fichier [taxpayersdata.json], dont le nombre d'enfants était la chaîne "2", a dans les résultats le nombre d'enfants 2 (ligne 14) : la chaîne a été convertie en entier par la couche [dao]. Ces résultats sont conformes à ceux de la version 4.

13.5. Tests [PHPUnit]

Comme il a été fait pour la version 4, nous allons écrire des tests [PHPUnit] pour la version 5. Ils sont dans le dossier [version-05/Tests].

13.5.1. Test de la couche [dao]

Le test [Tests/DaoTest.php] est le suivant :

<?php

// respect strict des types déclarés des paramètres et des résultats des fonctions
declare(strict_types=1);

// espace de noms des tests
namespace Application\Tests;

// les classes testées
use Application\DaoImpotsWithTaxAdminDataInDatabase;
use Application\DaoTransferAdminDataFromJsonFile2Database;
use Application\ExceptionImpots;
use Application\InterfaceDao;
use Application\TaxAdminData;
use Application\TaxPayerData;
// les classes de PHPUnit
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

// chargement des classes de la couche [dao]
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.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__ . "/../Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../Dao/DaoTransferAdminDataFromJsonFile2Database.php";

final class DaoTest extends TestCase
{
    // le dossier des données de la version 5
    private const string DATA = __DIR__ . "/../Data";
    // la couche [dao] testée
    private InterfaceDao $dao;
    // un fichier temporaire pour les erreurs
    private string $errorsFilename;

    // exécutée avant chaque test
    #[\Override]
    protected function setUp(): void
    {
        // création de la couche [dao]
        $this->dao = new DaoImpotsWithTaxAdminDataInDatabase(self::DATA . "/database.json");
        // un fichier temporaire
        $this->errorsFilename = tempnam(sys_get_temp_dir(), "errors");
    }

    // exécutée après chaque test
    #[\Override]
    protected function tearDown(): void
    {
        // suppression du fichier temporaire
        unlink($this->errorsFilename);
    }

    // les données de l'administration fiscale lues dans la base de données
    public function testTaxAdminData(): void
    {
        $taxAdminData = $this->dao->getTaxAdminData();
        // constantes de calcul
        $this->assertSame(1551.0, $taxAdminData->plafondQfDemiPart);
        $this->assertSame(21037.0, $taxAdminData->plafondRevenusCelibatairePourReduction);
        $this->assertSame(42074.0, $taxAdminData->plafondRevenusCouplePourReduction);
        $this->assertSame(3797.0, $taxAdminData->valeurReducDemiPart);
        $this->assertSame(1196.0, $taxAdminData->plafondDecoteCelibataire);
        $this->assertSame(1970.0, $taxAdminData->plafondDecoteCouple);
        $this->assertSame(1595.0, $taxAdminData->plafondImpotCelibatairePourDecote);
        $this->assertSame(2627.0, $taxAdminData->plafondImpotCouplePourDecote);
        $this->assertSame(12502.0, $taxAdminData->abattementDixPourcentMax);
        $this->assertSame(437.0, $taxAdminData->abattementDixPourcentMin);
        // tranches de l'impôt
        $this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $taxAdminData->limites);
        $this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $taxAdminData->coeffR);
        $this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $taxAdminData->coeffN);
    }

    // le transfert du fichier JSON vers la base, puis la relecture de la base
    public function testTransfert(): void
    {
        // transfert
        (new DaoTransferAdminDataFromJsonFile2Database(self::DATA . "/database.json", self::DATA . "/taxadmindata.json"))
            ->transferAdminData2Database();
        // relecture de la base par une nouvelle couche [dao]
        $dao = new DaoImpotsWithTaxAdminDataInDatabase(self::DATA . "/database.json");
        // la base contient les données du fichier JSON
        $this->assertEquals(TaxAdminData::fromJsonFile(self::DATA . "/taxadmindata.json"), $dao->getTaxAdminData());
    }

    // des descriptions de la base de données incorrectes
    public static function basesInvalides(): array
    {
        // [nom du fichier, code d'erreur attendu]
        return [
            "fichier inexistant" => ["inexistant.json", 15],
            "clé manquante" => ["database-incomplet.json", 21],
            "mot de passe erroné" => ["database-mdp-errone.json", 30],
        ];
    }

    #[DataProvider("basesInvalides")]
    public function testBasesInvalides(string $fichier, int $code): void
    {
        // on attend une exception [ExceptionImpots] avec un code précis
        $this->expectException(ExceptionImpots::class);
        $this->expectExceptionCode($code);
        // construction d'une couche [dao] avec une description de base incorrecte
        new DaoImpotsWithTaxAdminDataInDatabase(__DIR__ . "/data/$fichier");
    }

    // les données des contribuables lues dans un fichier JSON
    public function testTaxPayersData(): void
    {
        $taxPayersData = $this->dao->getTaxPayersData(self::DATA . "/taxpayersdata.json", $this->errorsFilename);
        // 11 contribuables corrects
        $this->assertCount(11, $taxPayersData);
        $this->assertContainsOnlyInstancesOf(TaxPayerData::class, $taxPayersData);
        // le premier et le dernier
        $this->assertEquals(new TaxPayerData("oui", 2, 55555), $taxPayersData[0]);
        $this->assertEquals(new TaxPayerData("oui", 3, 200000), $taxPayersData[10]);
        // le contribuable n° 1 du fichier JSON est erroné
        $erreurs = json_decode(file_get_contents($this->errorsFilename), true);
        $this->assertSame([
            ["numéro" => 1, "erreurs" => [["marié" => "ouix"], ["enfants" => "2x"], ["salaire" => "55555x"]]],
        ], $erreurs);
    }

    // un fichier des contribuables inexistant
    public function testTaxPayersDataInexistant(): void
    {
        $this->expectException(ExceptionImpots::class);
        $this->expectExceptionCode(15);
        $this->dao->getTaxPayersData(self::DATA . "/inexistant.json", $this->errorsFilename);
    }
}

Commentaires

  • lignes 21-30 : définition de l'environnement du test. Nous chargeons les fichiers des deux couches [dao] de la version 5 : celle du transfert et celle du calcul de l'impôt ;
  • lignes 43-49 : avant chaque test, on construit la couche [dao] avec le fichier [Data/database.json], comme le fait le script principal ;
  • ligne 60 : la méthode [testTaxAdminData] est identique à celle de la version 4. Mais cette fois, les données testées ont été lues dans la base de données ;
  • lignes 81-90 : on teste le transfert du fichier JSON vers la base, puis on relit la base avec une nouvelle couche [dao]. L'objet [TaxAdminData] obtenu doit être égal ([assertEquals]) à celui construit à partir du fichier JSON. Ce test suppose que le transfert a bien remis dans la base les données du fichier [taxadmindata.json] : il ne modifie pas les données attendues par les autres tests ;
  • lignes 93-101 : trois descriptions incorrectes de la base de données : un fichier inexistant (code 15), un fichier [database-incomplet.json] auquel il manque la clé [colCoeffN] (code 21), un fichier [database-mdp-errone.json] avec un mot de passe erroné (code 30). Les deux derniers fichiers sont dans le dossier [Tests/data] ;
  • lignes 104-111 : on vérifie qu'une couche [dao] construite avec une description incorrecte lance une exception [ExceptionImpots] avec le bon code d'erreur ;
  • lignes 114-128 : la lecture des données des contribuables dans le fichier JSON. On vérifie le nombre de contribuables obtenus et le contenu du premier et du dernier, puis le contenu exact du fichier des erreurs ;
  • ligne 131 : un fichier des contribuables inexistant doit provoquer une exception de code 15 .

13.5.2. Test de la couche [métier]

Le test [Tests/MetierTest.php] est celui de la version 4. Seul l'environnement du test change :

<?php

// respect strict des types déclarés des paramètres et des résultats des fonctions
declare(strict_types=1);

// espace de noms des tests
namespace Application\Tests;

// les classes testées
use Application\DaoImpotsWithTaxAdminDataInDatabase;
use Application\InterfaceMetier;
use Application\Metier;
// les classes de PHPUnit
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

// chargement des classes des couches [dao] et [métier]
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.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__ . "/../Metier/InterfaceMetier.php";
require_once __DIR__ . "/../Metier/Metier.php";

final class MetierTest extends TestCase
{
    // le dossier des données de la version 5
    private const string DATA = __DIR__ . "/../Data";
    // la couche [métier] testée
    private InterfaceMetier $métier;

    // exécutée avant chaque test
    #[\Override]
    protected function setUp(): void
    {
        // création de la couche [dao]
        $dao = new DaoImpotsWithTaxAdminDataInDatabase(self::DATA . "/database.json");
        // création de la couche [métier]
        $this->métier = new Metier($dao);
    }
  • ligne 10 : la classe de la couche [dao] utilisée ;
  • lignes 18-27 : les fichiers chargés, ceux du script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] ;
  • ligne 41 : la couche [métier] est construite sur une couche [dao] qui lit la base de données .

Le fournisseur de données [contribuables] et la méthode [testCalculerImpot] sont ceux de la version 4.

13.5.3. Exécution des tests

Le SGBD MySQL doit être lancé ([Laragon] / Démarrer tout). Les tests sont exécutés dans un terminal ouvert dans le dossier [php8-oct-2026] :

> php vendor/bin/phpunit scripts-console/impots/version-05/Tests --testdox
PHPUnit 12.5.37 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.5.11
Configuration: .../php8-oct-2026/phpunit.xml

..................                                                18 / 18 (100%)

Time: 00:00.035, Memory: 16.00 MB

Dao (Application\Tests\Dao)
 ✔ Tax admin data
 ✔ Transfert
 ✔ Bases invalides with data set "fichier inexistant"
 ✔ Bases invalides with data set "clé manquante"
 ✔ Bases invalides with data set "mot de passe erroné"
 ✔ Tax payers data
 ✔ Tax payers data inexistant

Metier (Application\Tests\Metier)
 ✔ Calculer impot with data set "couple, 2 enfants, 55555"
 ✔ Calculer impot with data set "couple, 2 enfants, 50000"
 ✔ Calculer impot with data set "couple, 3 enfants, 50000"
 ✔ Calculer impot with data set "célibataire, 2 enfants, 100000"
 ✔ Calculer impot with data set "célibataire, 3 enfants, 100000"
 ✔ Calculer impot with data set "couple, 3 enfants, 100000"
 ✔ Calculer impot with data set "couple, 5 enfants, 100000"
 ✔ Calculer impot with data set "célibataire, 0 enfant, 100000"
 ✔ Calculer impot with data set "couple, 2 enfants, 30000"
 ✔ Calculer impot with data set "célibataire, 0 enfant, 200000"
 ✔ Calculer impot with data set "couple, 3 enfants, 200000"

OK (18 tests, 49 assertions)

Les 18 tests réussissent : 7 tests de la couche [dao] (dont 3 pour le fournisseur [basesInvalides]) et les 11 tests de la couche [métier].