Skip to content

15. Practical Exercise – Version 7

Version 7 builds on version 6 by externalizing the constants used in the main scripts—namely, the names of the data files and the list of PHP files to be loaded—into a configuration file. Like version 6, it contains no new classes and uses those from version 5:

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

The [Data] folder contains the descriptions of the two [dbtaxes-2019] databases: the MySQL database from version 5 ([database-mysql.json]) and the PostgreSQL database from version 6 ([database-pgsql.json]). In the latter file, table names are no longer prefixed with the schema [public] (["tableBrackets": "tbbrackets"]).

15.1. Implementation

The configuration file is the following JSON [Data/config.json] file:

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

In this configuration:

  • lines 2–6: the paths to all JSON files in the application. The database used is PostgreSQL;
  • lines 7–20: the paths to all PHP files in the application, in the form [key => path]. These are the files from version 5;
  • lines 21–32: the dependencies for tax calculation in the form of a list of keys from the [dependencies] dictionary;
  • lines 33–40: the dependencies for transferring data from the JSON file [taxadmindata.json] to the database, in the same format.

All paths are relative to the [version-07] folder. The PHP 7 course added a key named [rootDirectory] to this file, specifying the absolute path to the version folder on the author’s machine (["C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-07"]): this had to be modified on each new machine. The scripts now calculate this folder path themselves based on the constant [__DIR__]. The [php8-oct-2026] folder can therefore be placed anywhere.

The script for transferring tax administration data to the database, [Main/MainTransferAdminDataFromJsonFile2Database.php], is now as follows:

<?php

// strict respect of the declared types of function parameters and results
declare(strict_types=1);

// namespace
namespace Application;

// the root folder of version 7: the paths of the configuration file are relative to this folder
const ROOT_DIRECTORY = __DIR__ . "/..";
// path of the configuration file
const CONFIG_FILENAME = ROOT_DIRECTORY . "/Data/config.json";

// we retrieve the configuration
$config = json_decode(file_get_contents(CONFIG_FILENAME), true, flags: JSON_THROW_ON_ERROR);

// we include the dependencies needed by the script
foreach ($config["dependencies4transfer"] as $dependency) {
    require_once ROOT_DIRECTORY . "/" . $config["dependencies"][$dependency];
}

// the paths of the data files
$databaseFilename = ROOT_DIRECTORY . "/" . $config["databaseFilename"];
$taxAdminDataFilename = ROOT_DIRECTORY . "/" . $config["taxAdminDataFilename"];

try {
    // creation of the [dao] layer
    $dao = new DaoTransferAdminDataFromJsonFile2Database($databaseFilename, $taxAdminDataFilename);
    // transfer of the data into the database
    $dao->transferAdminData2Database();
    print "The data of the tax administration have been transferred into the database described by [{$config["databaseFilename"]}]\n";
} catch (TaxException $ex) {
    // we display the error
    print "Error no. {$ex->getCode()}: {$ex->getMessage()}\n";
}
// end
print "Done\n";

Comments

The code remains the same as in version 6. The only difference is the use of the file [config.json] instead of the constants:

  • Line 10: The root directory for version 7, relative to which the paths in the configuration file are measured. [__DIR__] is the [Main] folder of the script; [__DIR__ . "/.."] is its parent folder, [version-07];
  • line 12: the path to the configuration file;
  • line 15: the function [file_get_contents] converts the file [config.json] into a string. The function [json_decode] then uses this string to construct the associative array [$config]. The second parameter, [true], of the function [json_decode] specifies that an associative array is desired rather than an object. With the [JSON_THROW_ON_ERROR] option, a syntax error in the file triggers a [JsonException] exception. We do not attempt to handle configuration file read errors here: this file is part of the application, and an error at this level is a programming error that PHP reports itself. Data errors (JSON data files, database) are always reported by a [TaxException];
  • lines 18–20: the dependencies required by the transfer script are included:
    • [$config["dependencies4transfer"]] is the table of dependencies required by the transfer script. It is a list of keys;
    • the paths to the files to be included are found in the dictionary [$config["dependencies"]] and are prefixed with the root directory. For example, the key [TaxException] refers to the file [version-07/../version-05/Entities/TaxException.php];
    • [require_once] (not [require] as in the PHP 7 course) ensures that a file is not loaded twice, which would cause the fatal error “Cannot redeclare class … (previously declared in …)”;
  • lines 23–24: the full paths to the data files. The PHP 7 course stored them in constants created by the function [define]. Variables are sufficient;
  • line 31: the name of the file describing the database being used is displayed. In a string enclosed in quotation marks, the array element [$config["databaseFilename"]] must be enclosed in curly braces.

Results:

1
2
3
> php MainTransferAdminDataFromJsonFile2Database.php
The data of the tax administration have been transferred into the database described by [Data/database-pgsql.json]
Done

Similarly, the tax calculation script becomes the following [Main/MainCalculateTaxWithTaxAdminDataInDatabase.php]:

<?php

// strict respect of the declared types of function parameters and results
declare(strict_types=1);

// namespace
namespace Application;

// the root folder of version 7: the paths of the configuration file are relative to this folder
const ROOT_DIRECTORY = __DIR__ . "/..";
// path of the configuration file
const CONFIG_FILENAME = ROOT_DIRECTORY . "/Data/config.json";

// we retrieve the configuration
$config = json_decode(file_get_contents(CONFIG_FILENAME), true, flags: JSON_THROW_ON_ERROR);

// we include the dependencies needed by the script
foreach ($config["dependencies4calculate"] as $dependency) {
    require_once ROOT_DIRECTORY . "/" . $config["dependencies"][$dependency];
}

// the paths of the data files
$databaseFilename = ROOT_DIRECTORY . "/" . $config["databaseFilename"];
$taxPayersDataFilename = ROOT_DIRECTORY . "/" . $config["taxPayersDataFilename"];
$resultsFilename = ROOT_DIRECTORY . "/" . $config["resultsFilename"];
$errorsFilename = ROOT_DIRECTORY . "/" . $config["errorsFilename"];

try {
    // creation of the [dao] layer
    $dao = new DaoTaxWithTaxAdminDataInDatabase($databaseFilename);
    // creation of the [business] layer
    $business = new Business($dao);
    // tax calculation in batch mode
    $business->executeBatchTaxes($taxPayersDataFilename, $resultsFilename, $errorsFilename);
    print "The results have been saved in the file [{$config["resultsFilename"]}]\n";
} catch (TaxException $ex) {
    // we display the error
    print "Error no. {$ex->getCode()}: {$ex->getMessage()}\n";
}
// end
print "Done\n";
  • Lines 18–20: This time, the dependencies for the tax calculation are included;
  • lines 23–26: the file paths for the application’s JSON files;
  • lines 28–39: the rest of the code is the same as in version 6.

Results:

1
2
3
> php MainCalculateTaxWithTaxAdminDataInDatabase.php
The results have been saved in the file [Data/results.json]
Done

The generated [Data/results.json] file is identical to the [results.json] file from versions 5 and 6.

The names of the two scripts have changed. In the PHP 7 course, they referred to PostgreSQL ([MainTransferAdminDataFromFile2PostgresSQLDatabase.php], [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php]). However, these scripts no longer depend on any specific DBMS: it is selected by the [databaseFilename] key in the configuration file. If you replace ["Data/database-pgsql.json"] with ["Data/database-mysql.json"] in that file, the transfer script will use the MySQL database without changing a single line of PHP code:

1
2
3
> php MainTransferAdminDataFromJsonFile2Database.php
The data of the tax administration have been transferred into the database described by [Data/database-mysql.json]
Done

This is the benefit of a configuration file: you can adapt the application to its environment (DBMS, file locations, etc.) without touching the code. We’ll see this configuration file again in future versions of the application.

15.2. [PHPUnit] Tests

This version, like the previous ones, is validated by [PHPUnit] tests. Their environment is also built based on the configuration file.

15.2.1. [dao] Layer Test

The [dao] layer tests will use the two database descriptions from the [Data] folder: We will thus use the same tests to verify that the [dao] layer works with both MySQL and PostgreSQL. The [Tests/DaoTest.php] test is as follows:

<?php

// strict respect of the declared types of function parameters and results
declare(strict_types=1);

// namespace of the tests
namespace Application\Tests;

// the tested classes
use Application\DaoTaxWithTaxAdminDataInDatabase;
use Application\DaoTransferAdminDataFromJsonFile2Database;
use Application\TaxAdminData;
// the PHPUnit classes
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class DaoTest extends TestCase
{
    // the root folder of version 7
    private const string ROOT_DIRECTORY = __DIR__ . "/..";
    // the configuration of the application
    private static array $config;

    // executed once, before the first test of the class
    #[\Override]
    public static function setUpBeforeClass(): void
    {
        // we retrieve the configuration
        self::$config = json_decode(file_get_contents(self::ROOT_DIRECTORY . "/Data/config.json"), true, flags: JSON_THROW_ON_ERROR);
        // we include the dependencies of the tax calculation and those of the data transfer
        $dependencies = array_unique([...self::$config["dependencies4calculate"], ...self::$config["dependencies4transfer"]]);
        foreach ($dependencies as $dependency) {
            require_once self::ROOT_DIRECTORY . "/" . self::$config["dependencies"][$dependency];
        }
    }

    // the two databases
    public static function databases(): array
    {
        // [database description file]
        return [
            "MySQL" => ["Data/database-mysql.json"],
            "PostgreSQL" => ["Data/database-pgsql.json"],
        ];
    }

    // the data of the tax administration read from the database
    #[DataProvider("databases")]
    public function testTaxAdminData(string $databaseFilename): void
    {
        $dao = new DaoTaxWithTaxAdminDataInDatabase(self::ROOT_DIRECTORY . "/$databaseFilename");
        $taxAdminData = $dao->getTaxAdminData();
        // calculation constants
        $this->assertSame(1551.0, $taxAdminData->halfShareQfCeiling);
        $this->assertSame(21037.0, $taxAdminData->singleIncomeCeilingForReduction);
        $this->assertSame(42074.0, $taxAdminData->coupleIncomeCeilingForReduction);
        $this->assertSame(3797.0, $taxAdminData->halfShareReductionValue);
        $this->assertSame(1196.0, $taxAdminData->singleDiscountCeiling);
        $this->assertSame(1970.0, $taxAdminData->coupleDiscountCeiling);
        $this->assertSame(1595.0, $taxAdminData->singleTaxCeilingForDiscount);
        $this->assertSame(2627.0, $taxAdminData->coupleTaxCeilingForDiscount);
        $this->assertSame(12502.0, $taxAdminData->tenPercentAllowanceMax);
        $this->assertSame(437.0, $taxAdminData->tenPercentAllowanceMin);
        // tax brackets
        $this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $taxAdminData->limits);
        $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);
    }

    // the transfer of the JSON file to the database, then the rereading of the database
    #[DataProvider("databases")]
    public function testTransfer(string $databaseFilename): void
    {
        // the JSON files
        $databaseFilename = self::ROOT_DIRECTORY . "/$databaseFilename";
        $taxAdminDataFilename = self::ROOT_DIRECTORY . "/" . self::$config["taxAdminDataFilename"];
        // transfer
        (new DaoTransferAdminDataFromJsonFile2Database($databaseFilename, $taxAdminDataFilename))->transferAdminData2Database();
        // rereading of the database by a [dao] layer
        $dao = new DaoTaxWithTaxAdminDataInDatabase($databaseFilename);
        // the database contains the data of the JSON file
        $this->assertEquals(TaxAdminData::fromJsonFile($taxAdminDataFilename), $dao->getTaxAdminData());
    }
}

Comments

  • Lines 10–12: The tested classes are imported. The [use] statement does not load the classes; it merely allows them to be referred to by their short names;
  • line 22: the application configuration is stored in a static (class) attribute;
  • Lines 26–35: The static method [setUpBeforeClass] of [TestCase] is executed once, before the first test of the class. It reads the configuration file and loads the application files. The PHP 7 course performed this task at the top level of the test file, outside the class. With PHPUnit, this code is executed when PHPUnit loads the file, at a time that is not under our control; the [setUpBeforeClass] method is designed for this purpose;
  • Line 31: The tests for the [dao] layer require dependencies from the tax calculation (for class [DaoTaxWithTaxAdminDataInDatabase]) and from the transfer (for class [DaoTransferAdminDataFromJsonFile2Database]). The two lists are combined, and then function [array_unique] removes the duplicates;
  • lines 38–45: the data source: the description files for the two databases;
  • lines 48–49: the [testTaxAdminData] test for version 5 is run once for each of the two databases;
  • lines 71–83: the same applies to the test for transferring data from the JSON file to the database.

15.2.2. Test of the [business] layer

The [Tests/BusinessTest.php] test is the Version 5 test with an environment built from the configuration file:

<?php

// strict respect of the declared types of function parameters and results
declare(strict_types=1);

// namespace of the tests
namespace Application\Tests;

// the tested classes
use Application\DaoTaxWithTaxAdminDataInDatabase;
use Application\InterfaceBusiness;
use Application\Business;
// the PHPUnit classes
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class BusinessTest extends TestCase
{
    // the root folder of version 7
    private const string ROOT_DIRECTORY = __DIR__ . "/..";
    // the configuration of the application
    private static array $config;
    // the tested [business] layer
    private InterfaceBusiness $business;

    // executed once, before the first test of the class
    #[\Override]
    public static function setUpBeforeClass(): void
    {
        // we retrieve the configuration
        self::$config = json_decode(file_get_contents(self::ROOT_DIRECTORY . "/Data/config.json"), true, flags: JSON_THROW_ON_ERROR);
        // we include the dependencies of the tax calculation
        foreach (self::$config["dependencies4calculate"] as $dependency) {
            require_once self::ROOT_DIRECTORY . "/" . self::$config["dependencies"][$dependency];
        }
    }

    // executed before each test
    #[\Override]
    protected function setUp(): void
    {
        // creation of the [dao] layer on the database of the configuration
        $dao = new DaoTaxWithTaxAdminDataInDatabase(self::ROOT_DIRECTORY . "/" . self::$config["databaseFilename"]);
        // creation of the [business] layer
        $this->business = new Business($dao);
    }

Comments

  • Lines 28–36: The configuration file is read, and the dependencies for the tax calculation are loaded, just as the main script [MainCalculateTaxWithTaxAdminDataInDatabase] does;
  • Line 43: The layer [dao] uses the database specified by the configuration file;
  • The data provider [taxpayers] and the method [testCalculateTax], not shown here, are those from version 4.

15.2.3. Running the Tests

Both DBMSs, MySQL and PostgreSQL, must be started. The test results are as follows:

> php vendor/bin/phpunit scripts-console/tax/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

Business (Application\Tests\Business)
 ✔ Calculate tax with data set "couple, 2 children, 55555"
 ✔ Calculate tax with data set "couple, 2 children, 50000"
 ✔ Calculate tax with data set "couple, 3 children, 50000"
 ✔ Calculate tax with data set "single, 2 children, 100000"
 ✔ Calculate tax with data set "single, 3 children, 100000"
 ✔ Calculate tax with data set "couple, 3 children, 100000"
 ✔ Calculate tax with data set "couple, 5 children, 100000"
 ✔ Calculate tax with data set "single, 0 children, 100000"
 ✔ Calculate tax with data set "couple, 2 children, 30000"
 ✔ Calculate tax with data set "single, 0 children, 200000"
 ✔ Calculate tax with data set "couple, 3 children, 200000"

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

OK (15 tests, 50 assertions)
  • lines 12–15: the tests for the [dao] layer were run with each of the two databases;
  • lines 18–28: the 11 tests for the [business] layer.

We have now completed the console versions of the tax calculation application. Version 8 will transform it into a client/server application: the database and the tax calculation will reside on a web server, queried by clients. It will incorporate the classes from version 5 and the configuration file technique from version 7.