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 :
| > 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 :
| > 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 :
| > 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.