Skip to content

15. Exercice d'application – version 7

La version 7 reprend la version 6 en externalisant dans un fichier de configuration les constantes utilisées dans les scripts principaux : noms des fichiers de données et liste des fichiers PHP à charger. Comme la version 6, elle ne contient aucune nouvelle classe et utilise celles de la version 5 :

version-07
├── Data
│   ├── config.json
│   ├── database-mysql.json
│   ├── database-pgsql.json
│   ├── errors.json
│   ├── results.json
│   ├── taxadmindata.json
│   └── taxpayersdata.json
├── Main
│   ├── MainCalculateImpotsWithTaxAdminDataInDatabase.php
│   └── MainTransferAdminDataFromJsonFile2Database.php
└── Tests
    ├── DaoTest.php
    └── MetierTest.php

Le dossier [Data] contient les descriptions des deux bases de données [dbimpots-2019] : la base MySQL de la version 5 [database-mysql.json] et la base PostgreSQL de la version 6 [database-pgsql.json]. Dans ce dernier fichier, les noms des tables ne sont plus préfixés par le schéma [public] (["tableTranches": "tbtranches"]).

15.1. Implémentation

Le fichier de configuration est le fichier JSON [Data/config.json] suivant :

{
    "databaseFilename": "Data/database-pgsql.json",
    "taxAdminDataFilename": "Data/taxadmindata.json",
    "taxPayersDataFilename": "Data/taxpayersdata.json",
    "resultsFilename": "Data/results.json",
    "errorsFilename": "Data/errors.json",
    "dependencies": {
        "ExceptionImpots": "../version-05/Entities/ExceptionImpots.php",
        "TaxAdminData": "../version-05/Entities/TaxAdminData.php",
        "TaxPayerData": "../version-05/Entities/TaxPayerData.php",
        "Database": "../version-05/Entities/Database.php",
        "Utilitaires": "../version-05/Utilities/Utilitaires.php",
        "InterfaceDao": "../version-05/Dao/InterfaceDao.php",
        "TraitDao": "../version-05/Dao/TraitDao.php",
        "DaoImpotsWithTaxAdminDataInDatabase": "../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php",
        "InterfaceDao4TransferAdminData2Database": "../version-05/Dao/InterfaceDao4TransferAdminData2Database.php",
        "DaoTransferAdminDataFromJsonFile2Database": "../version-05/Dao/DaoTransferAdminDataFromJsonFile2Database.php",
        "InterfaceMetier": "../version-05/Metier/InterfaceMetier.php",
        "Metier": "../version-05/Metier/Metier.php"
    },
    "dependencies4calculate": [
        "ExceptionImpots",
        "TaxAdminData",
        "TaxPayerData",
        "Database",
        "Utilitaires",
        "InterfaceDao",
        "TraitDao",
        "DaoImpotsWithTaxAdminDataInDatabase",
        "InterfaceMetier",
        "Metier"
    ],
    "dependencies4transfer": [
        "ExceptionImpots",
        "TaxAdminData",
        "Database",
        "Utilitaires",
        "InterfaceDao4TransferAdminData2Database",
        "DaoTransferAdminDataFromJsonFile2Database"
    ]
}

Dans cette configuration :

  • lignes 2-6 : les chemins de tous les fichiers JSON de l'application. La base de données utilisée est la base PostgreSQL ;
  • lignes 7-20 : les chemins de tous les fichiers PHP de l'application, sous la forme [clé => chemin]. Ce sont les fichiers de la version 5 ;
  • lignes 21-32 : les dépendances du calcul de l'impôt sous la forme d'une liste de clés du dictionnaire [dependencies] ;
  • lignes 33-40 : les dépendances du transfert en base des données du fichier JSON [taxadmindata.json], sous la même forme .

Tous les chemins sont relatifs au dossier [version-07]. Le cours PHP 7 ajoutait à ce fichier une clé [rootDirectory] donnant le chemin absolu du dossier de la version sur la machine de l'auteur (["C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-07"]) : il fallait la modifier sur chaque nouvelle machine. Les scripts calculent désormais ce dossier eux-mêmes à partir de la constante [__DIR__]. Le dossier [php8-oct-2026] peut ainsi être placé n'importe où.

Le script de transfert en base des données de l'administration fiscale [Main/MainTransferAdminDataFromJsonFile2Database.php] devient 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
namespace Application;

// le dossier racine de la version 7 : les chemins du fichier de configuration sont relatifs à ce dossier
const ROOT_DIRECTORY = __DIR__ . "/..";
// chemin du fichier de configuration
const CONFIG_FILENAME = ROOT_DIRECTORY . "/Data/config.json";

// on récupère la configuration
$config = json_decode(file_get_contents(CONFIG_FILENAME), true, flags: JSON_THROW_ON_ERROR);

// on inclut les dépendances nécessaires au script
foreach ($config["dependencies4transfer"] as $dependency) {
    require_once ROOT_DIRECTORY . "/" . $config["dependencies"][$dependency];
}

// les chemins des fichiers de données
$databaseFilename = ROOT_DIRECTORY . "/" . $config["databaseFilename"];
$taxAdminDataFilename = ROOT_DIRECTORY . "/" . $config["taxAdminDataFilename"];

try {
    // création de la couche [dao]
    $dao = new DaoTransferAdminDataFromJsonFile2Database($databaseFilename, $taxAdminDataFilename);
    // 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 décrite par [{$config["databaseFilename"]}]\n";
} catch (ExceptionImpots $ex) {
    // on affiche l'erreur
    print "Erreur n° {$ex->getCode()} : {$ex->getMessage()}\n";
}
// fin
print "Terminé\n";

Commentaires

Le code reste celui de la version 6. La seule différence est l'exploitation du fichier [config.json] en lieu et place des constantes :

  • ligne 10 : le dossier racine de la version 7, à partir duquel sont mesurés les chemins du fichier de configuration. [__DIR__] est le dossier [Main] du script, [__DIR__ . "/.."] son dossier parent [version-07] ;
  • ligne 12 : le chemin du fichier de configuration ;
  • ligne 15 : 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 tableau associatif [$config]. Le second paramètre [true] de la fonction [json_decode] indique qu'on veut un tableau associatif plutôt qu'un objet. Avec l'option [JSON_THROW_ON_ERROR], une erreur de syntaxe dans le fichier lance une exception [JsonException]. On ne cherche pas à gérer ici les erreurs de lecture du fichier de configuration : ce fichier fait partie de l'application, et une erreur à son niveau est une erreur de programmation que PHP signale lui-même. Les erreurs de données (fichiers JSON des données, base de données) sont, elles, toujours signalées par une [ExceptionImpots] ;
  • lignes 18-20 : on inclut les dépendances nécessaires au script de transfert :
    • [$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 sont trouvés dans le dictionnaire [$config["dependencies"]], et préfixés par le dossier racine. Par exemple, la clé [ExceptionImpots] donne le fichier [version-07/../version-05/Entities/ExceptionImpots.php] ;
    • [require_once] (et non [require] comme dans le cours PHP 7) garantit qu'un fichier n'est pas chargé deux fois, ce qui provoquerait l'erreur fatale « Cannot redeclare class … (previously declared in …) » ;
  • lignes 23-24 : les chemins complets des fichiers de données. Le cours PHP 7 les mettait dans des constantes créées par la fonction [define]. Des variables suffisent ;
  • ligne 31 : on affiche le nom du fichier décrivant la base de données utilisée. Dans une chaîne entre guillemets, l'élément de tableau [$config["databaseFilename"]] doit être entouré d'accolades .

Résultats :

1
2
3
> php MainTransferAdminDataFromJsonFile2Database.php
Les données de l'administration fiscale ont été transférées dans la base décrite par [Data/database-pgsql.json]
Terminé

De la même façon, le script de calcul de l'impôt devient le suivant [Main/MainCalculateImpotsWithTaxAdminDataInDatabase.php] :

<?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;

// le dossier racine de la version 7 : les chemins du fichier de configuration sont relatifs à ce dossier
const ROOT_DIRECTORY = __DIR__ . "/..";
// chemin du fichier de configuration
const CONFIG_FILENAME = ROOT_DIRECTORY . "/Data/config.json";

// on récupère la configuration
$config = json_decode(file_get_contents(CONFIG_FILENAME), true, flags: JSON_THROW_ON_ERROR);

// on inclut les dépendances nécessaires au script
foreach ($config["dependencies4calculate"] as $dependency) {
    require_once ROOT_DIRECTORY . "/" . $config["dependencies"][$dependency];
}

// les chemins des fichiers de données
$databaseFilename = ROOT_DIRECTORY . "/" . $config["databaseFilename"];
$taxPayersDataFilename = ROOT_DIRECTORY . "/" . $config["taxPayersDataFilename"];
$resultsFilename = ROOT_DIRECTORY . "/" . $config["resultsFilename"];
$errorsFilename = ROOT_DIRECTORY . "/" . $config["errorsFilename"];

try {
    // création de la couche [dao]
    $dao = new DaoImpotsWithTaxAdminDataInDatabase($databaseFilename);
    // création de la couche [métier]
    $métier = new Metier($dao);
    // calcul de l'impôt en mode batch
    $métier->executeBatchImpots($taxPayersDataFilename, $resultsFilename, $errorsFilename);
    print "Les résultats ont été enregistrés dans le fichier [{$config["resultsFilename"]}]\n";
} catch (ExceptionImpots $ex) {
    // on affiche l'erreur
    print "Erreur n° {$ex->getCode()} : {$ex->getMessage()}\n";
}
// fin
print "Terminé\n";
  • lignes 18-20 : on inclut cette fois les dépendances du calcul de l'impôt ;
  • lignes 23-26 : les chemins des fichiers JSON de l'application ;
  • lignes 28-39 : le reste du code est celui de la version 6 .

Résultats :

1
2
3
> php MainCalculateImpotsWithTaxAdminDataInDatabase.php
Les résultats ont été enregistrés dans le fichier [Data/results.json]
Terminé

Le fichier [Data/results.json] produit est identique au fichier [resultats.json] des versions 5 et 6.

Les noms des deux scripts ont changé. Dans le cours PHP 7, ils mentionnaient PostgreSQL ([MainTransferAdminDataFromFile2PostgresSQLDatabase.php], [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php]). Mais ces scripts ne dépendent plus d'aucun SGBD : celui-ci est choisi par la clé [databaseFilename] du fichier de configuration. Si on y remplace ["Data/database-pgsql.json"] par ["Data/database-mysql.json"], le script de transfert travaille avec la base MySQL, sans qu'une ligne de code PHP soit modifiée :

1
2
3
> php MainTransferAdminDataFromJsonFile2Database.php
Les données de l'administration fiscale ont été transférées dans la base décrite par [Data/database-mysql.json]
Terminé

C'est l'intérêt d'un fichier de configuration : on adapte l'application à son environnement (SGBD, emplacement des fichiers…) sans toucher au code. Nous retrouverons ce fichier de configuration dans les versions suivantes de l'application.

15.2. Tests [PHPUnit]

Cette version, comme les précédentes, est validée par des tests [PHPUnit]. Leur environnement est lui aussi construit à partir du fichier de configuration.

15.2.1. Test de la couche [dao]

Les tests de la couche [dao] vont exploiter les deux descriptions de bases de données du dossier [Data] : nous vérifions ainsi avec les mêmes tests que la couche [dao] fonctionne avec MySQL et avec PostgreSQL. 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\TaxAdminData;
// les classes de PHPUnit
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class DaoTest extends TestCase
{
    // le dossier racine de la version 7
    private const string ROOT_DIRECTORY = __DIR__ . "/..";
    // la configuration de l'application
    private static array $config;

    // exécutée une fois, avant le premier test de la classe
    #[\Override]
    public static function setUpBeforeClass(): void
    {
        // on récupère la configuration
        self::$config = json_decode(file_get_contents(self::ROOT_DIRECTORY . "/Data/config.json"), true, flags: JSON_THROW_ON_ERROR);
        // on inclut les dépendances du calcul de l'impôt et celles du transfert des données
        $dependencies = array_unique([...self::$config["dependencies4calculate"], ...self::$config["dependencies4transfer"]]);
        foreach ($dependencies as $dependency) {
            require_once self::ROOT_DIRECTORY . "/" . self::$config["dependencies"][$dependency];
        }
    }

    // les deux bases de données
    public static function bases(): array
    {
        // [fichier de description de la base]
        return [
            "MySQL" => ["Data/database-mysql.json"],
            "PostgreSQL" => ["Data/database-pgsql.json"],
        ];
    }

    // les données de l'administration fiscale lues dans la base de données
    #[DataProvider("bases")]
    public function testTaxAdminData(string $databaseFilename): void
    {
        $dao = new DaoImpotsWithTaxAdminDataInDatabase(self::ROOT_DIRECTORY . "/$databaseFilename");
        $taxAdminData = $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
    #[DataProvider("bases")]
    public function testTransfert(string $databaseFilename): void
    {
        // les fichiers JSON
        $databaseFilename = self::ROOT_DIRECTORY . "/$databaseFilename";
        $taxAdminDataFilename = self::ROOT_DIRECTORY . "/" . self::$config["taxAdminDataFilename"];
        // transfert
        (new DaoTransferAdminDataFromJsonFile2Database($databaseFilename, $taxAdminDataFilename))->transferAdminData2Database();
        // relecture de la base par une couche [dao]
        $dao = new DaoImpotsWithTaxAdminDataInDatabase($databaseFilename);
        // la base contient les données du fichier JSON
        $this->assertEquals(TaxAdminData::fromJsonFile($taxAdminDataFilename), $dao->getTaxAdminData());
    }
}

Commentaires

  • lignes 10-12 : les classes testées sont importées. L'instruction [use] ne charge pas les classes : elle permet seulement de les désigner par leur nom court ;
  • ligne 22 : la configuration de l'application, dans un attribut statique (de classe) ;
  • lignes 26-35 : la méthode statique [setUpBeforeClass] de [TestCase] est exécutée une seule fois, avant le premier test de la classe. On y lit le fichier de configuration et on charge les fichiers de l'application. Le cours PHP 7 faisait ce travail au niveau principal du fichier de test, en dehors de la classe. Avec PHPUnit, ce code est exécuté lorsque PHPUnit charge le fichier, à un moment qu'on ne maîtrise pas ; la méthode [setUpBeforeClass] est faite pour cela ;
  • ligne 31 : les tests de la couche [dao] ont besoin des dépendances du calcul de l'impôt (pour la classe [DaoImpotsWithTaxAdminDataInDatabase]) et de celles du transfert (pour la classe [DaoTransferAdminDataFromJsonFile2Database]). On réunit les deux listes, puis la fonction [array_unique] élimine les doublons ;
  • lignes 38-45 : le fournisseur de données : les fichiers de description des deux bases de données ;
  • lignes 48-49 : le test [testTaxAdminData] de la version 5 est exécuté une fois pour chacune des deux bases ;
  • lignes 71-83 : de même pour le test du transfert des données du fichier JSON vers la base de données .

15.2.2. Test de la couche [métier]

Le test [Tests/MetierTest.php] est celui de la version 5 avec un environnement construit à partir du fichier de configuration :

<?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;

final class MetierTest extends TestCase
{
    // le dossier racine de la version 7
    private const string ROOT_DIRECTORY = __DIR__ . "/..";
    // la configuration de l'application
    private static array $config;
    // la couche [métier] testée
    private InterfaceMetier $métier;

    // exécutée une fois, avant le premier test de la classe
    #[\Override]
    public static function setUpBeforeClass(): void
    {
        // on récupère la configuration
        self::$config = json_decode(file_get_contents(self::ROOT_DIRECTORY . "/Data/config.json"), true, flags: JSON_THROW_ON_ERROR);
        // on inclut les dépendances du calcul de l'impôt
        foreach (self::$config["dependencies4calculate"] as $dependency) {
            require_once self::ROOT_DIRECTORY . "/" . self::$config["dependencies"][$dependency];
        }
    }

    // exécutée avant chaque test
    #[\Override]
    protected function setUp(): void
    {
        // création de la couche [dao] sur la base de données de la configuration
        $dao = new DaoImpotsWithTaxAdminDataInDatabase(self::ROOT_DIRECTORY . "/" . self::$config["databaseFilename"]);
        // création de la couche [métier]
        $this->métier = new Metier($dao);
    }

Commentaires

  • lignes 28-36 : on lit le fichier de configuration et on charge les dépendances du calcul de l'impôt, comme le fait le script principal [MainCalculateImpotsWithTaxAdminDataInDatabase] ;
  • ligne 43 : la couche [dao] utilise la base de données désignée par le fichier de configuration ;
  • le fournisseur de données [contribuables] et la méthode [testCalculerImpot], non reproduits ici, sont ceux de la version 4 .

15.2.3. Exécution des tests

Les deux SGBD, MySQL et PostgreSQL, doivent être lancés. Les résultats des tests sont les suivants :

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

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

...............                                                   15 / 15 (100%)

Time: 00:00.602, Memory: 16.00 MB

Dao (Application\Tests\Dao)
 ✔ Tax admin data with data set "MySQL"
 ✔ Tax admin data with data set "PostgreSQL"
 ✔ Transfert with data set "MySQL"
 ✔ Transfert with data set "PostgreSQL"

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 (15 tests, 50 assertions)
  • lignes 12-15 : les tests de la couche [dao] ont été exécutés avec chacune des deux bases ;
  • lignes 18-28 : les 11 tests de la couche [métier] .

Nous arrivons au terme des versions console de l'application de calcul de l'impôt. La version 8 la transformera en application client / serveur : la base de données et le calcul de l'impôt seront sur un serveur web, interrogé par des clients. Elle reprendra les classes de la version 5 et la technique du fichier de configuration de la version 7.