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