11. Practice Exercise – Version 4
The tax calculation application will implement the following layered architecture:

We will reuse the elements from version 3 of the linked section, modifying them to adapt them to the application’s new architecture. This is sometimes called “refactoring.” Here, we assume that the data required by the application is stored in text files. The [Dao] layer will handle interactions with these files.
11.1. Script Tree

11.2. Objects exchanged between layers
We will retain certain objects from version 3. We list them here as a reminder.
The [ExceptionImpots] exception is the exception that the [Dao] layer will throw when it encounters a problem either with data access or with the nature of the data (incorrect data).
<?php
// namespace
namespace Application;
class TaxException extends \RuntimeException {
public function __construct(string $message, int $code=0) {
parent::__construct($message, $code);
}
}
The [Utilities] class contains methods useful for managing text files (in this case, a single method):
<?php
// namespace
namespace Application;
// a class of utility functions
abstract class Utilities {
public static function cutNewLinechar(string $line): string {
// Remove the end-of-line character from $line if it exists
$length = strlen($line); // line length
while (substr($line, $length - 1, 1) == "\n" or substr($line, $length - 1, 1) == "\r") {
$line = substr($line, 0, $length - 1);
$length--;
}
// end - return the line
return($line);
}
}
The [TaxAdminData] class is the class that encapsulates tax administration data:
<?php
namespace Application;
class TaxAdminData {
// tax brackets
private $limits;
private $rate;
private $coeffN;
// tax calculation constants
private $half-share-income-limit;
private $singleIncomeLimitForReduction;
private $coupleIncomeLimitForReduction;
private $half-share-reduction-value;
private $singleDiscountCeiling;
private $coupleDiscountLimit;
private $coupleTaxCeilingForDiscount;
private $singleTaxCeilingForDiscount;
private $MaxTenPercentDeduction;
private $minTenPercentDeduction;
// initialization
public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
// retrieve the contents of the tax data file
$fileContents = \file_get_contents($taxAdminDataFilename);
…
// return the object
return $this;
}
private function check($value): \stdClass {
…
return $result;
}
// toString
public function __toString() {
// JSON string of the object
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
// getters and setters
public function getLimits() {
return $this->limits;
}
…
public function setLimits($limits) {
$this->limits = $limits;
return $this;
}
…
}
We add a new class [TaxPayerData] that encapsulates the data written to the results file:
<?php
// namespace
namespace Application;
// the data class
class TaxPayerData {
// data required to calculate the taxpayer's tax
private $married;
private $children;
private $salary;
// results of the tax calculation
private $amount;
private $surcharge;
private $discount;
private $reduction;
private $rate;
// setter
public function setFromParameters(string $married, int $numberOfChildren, int $annualSalary) : TaxPayerData{
// taxpayer data required to calculate the tax
$this->married = $married;
$this->children = $numberOfChildren;
$this->salary = $annualSalary;
// return the initialized object
return $this;
}
// getters and setters
public function isMarried() {
return $this->married;
}
…
public function setSpouse($spouse) {
$this->groom = $groom;
return $this;
}
…
// toString
public function __toString() {
// JSON string of the object
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
}
Note: Use automatic code generation to generate the constructor, getters, and setters (see the linked section). Note that the setters are ‘fluent’.
11.3. The [DAO] layer
Here we are focusing on the [1] layer of our application:

11.3.1. The [InterfaceDao] interface
The interface for the [DAO] layer will be as follows [InterfaceDao.php]:
<?php
// namespace
namespace Application;
interface InterfaceDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// Read tax administration data (tax brackets)
public function getTaxAdminData(): TaxAdminData;
// saving results
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Comments
- The requirements are as follows:
- Taxpayer data is stored in a text file;
- the results of the tax calculation are saved in a text file;
- Any errors are saved in a text file;
- it is unknown in what format the tax authority’s data is available. For each new format, the [InterfaceDao] interface must be implemented by a new class;
- methods of the interface that encounter a fatal error when accessing data must throw an exception of type [TaxException];
- line 9: the method that retrieves the taxpayer’s data [marital status, number of children, annual salary];
- the first parameter is the name of the text file containing this data;
- the second parameter is the name of the text file in which to record any errors encountered;
- line 12: the method that retrieves data from the tax authority. No parameters are passed here because we do not know how the data is stored;
- line 15: the method used to save the tax calculation results to a text file, the name of which is passed as a parameter;
When writing the [InterfaceDao] interface, we know that there will be different ways to implement the [getTaxAdminData] method depending on how the tax administration data is stored. The [InterfaceDao] interface will therefore be implemented by different classes, each handling a specific storage method for this data (arrays, text files, databases, web services). These derived classes will nevertheless share common code, specifically the implementation of the [getTaxPayersData] and [saveResults] methods. We know that this use case can be implemented in two ways (see linked paragraph):
- We create an abstract class C that contains the code common to the derived classes. Class C implements interface I, but certain methods that must be declared in the derived classes are declared as abstract in class C, and therefore class C itself is abstract. We then create classes C1 and C2 derived from C, each of which implements the undefined (abstract) methods of their parent class C in their own way;
- we create a trait T that is nearly identical to the abstract class C from the previous solution. This trait does not implement the interface I because, syntactically, it cannot. We then create classes C1 and C2 that implement the interface I and use the trait T. All that remains for these classes is to implement the methods of the interface I that are not implemented by the trait T they import;
For this example, we will use a trait [TraitDao] here.
11.3.2. The [TraitDao] trait
The code for the [TraitDao] trait is as follows [TraitDao.php]:
<?php
// namespace
namespace Application;
trait TraitDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// array of taxpayer data
$taxPayersData = [];
// array of errors
$errors = [];
// quite a few errors can occur when working with files
try {
// reading user data
// Each line has the format: marital status, number of children, annual salary
$taxPayersFile = fopen($taxPayersFilename, "r");
if (!$taxPayersFile) {
throw new ExceptionImpots("Unable to open taxpayer returns [$taxPayersFilename] for reading", 12);
}
// process the current line of the user data file
// which has the format: marital status, number of children, annual salary
$num = 1; // current row number
$nbErrors = 0; // number of errors encountered
while ($row = fgets($taxPayersFile, 100)) {
// skip empty lines
$line = trim($line);
if (strlen($line) == 0) {
// next line
$num++;
// loop again
continue;
}
// Remove any line break characters
$line = Utilities::cutNewLineChar($line);
// retrieve the 3 fields married:children:salary that make up $line
list($married, $children, $salary) = explode(",", $line);
// check them
// marital status must be yes or no
$married = trim(strtolower($married));
$error = ($married !== "yes" and $married !== "no");
if (!$error) {
// the number of children must be an integer
$children = trim($children);
if (!preg_match("/^\d+$/", $children)) {
$error = TRUE;
} else {
$children = (int) $children;
}
}
if (!$error) {
// the salary is an integer without the euro cents
$salary = trim($salary);
if (!preg_match("/^\d+$/", $salary)) {
$error = TRUE;
} else {
$salary = (int) $salary;
}
}
// error?
if ($error) {
$errors[] = "line [$num] of file [$taxPayersFilename] is incorrect";
$numberOfErrors++;
} else {
// store the information
$taxPayersData[] = (new TaxPayerData())->setFromParameters($married, $children, $salary);
}
// next line
$num++;
}
// Are we at the end of the file?
if (!feof($taxPayersFile)) {
// We exited the loop due to a read error
throw new TaxException("Error reading line [$num] of file [$taxPayersFilename]");
} else {
// We exited the loop at the end-of-file marker
// Save the errors to a text file
$this->saveString($errorsFilename, implode("\n", $errors));
// result of the function
return $taxPayersData;
}
} finally {
// close the file if it is open
if ($taxPayersFile) {
fclose($taxPayersFile);
}
}
}
// saving the results
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// save the array [$taxPayersData] to the text file [$resultsFileName]
// if the text file [$resultsFileName] does not exist, it is created
$this->saveString($resultsFilename, implode("\n", $taxPayersData));
}
// Save the results of an array to a text file
private function saveString(string $fileName, string $data): void {
// Save the array [$data] to the text file [$fileName]
// if the text file [$fileName] does not exist, it is created
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Error saving data to the text file [$fileName]");
}
}
}
Comments
- line 6: here we define a trait, not a class;
- lines 9–89: The [getTaxPayersData] method implements the method of the same name from the [InterfaceDao] interface. It retrieves taxpayer data [marital status, number of children, annual salary] from a text file named [$taxPayersFilename]. It returns this data as an array [$taxPayersData] of elements of type [TaxPayerData] (lines 67, 81);
- the [getTaxPayersData] method is very similar to the [AbstractBaseImpots::executeBatchImpots] method described in the linked section, with the following differences:
- the [getTaxPayersData] method only retrieves taxpayer data. It does not perform any tax calculations. Here, that is the role of the [business] layer;
- Just like the [executeBatchImpots] method, it reports errors. Here, errors are first stored in an array [$errors] (line 13), which is then saved to a text file at the end of the process (line 79). Depending on the situation, this array may or may not be empty;
- in the case of a fatal error, an [ExceptionImpots] exception is thrown (lines 20, 75);
- line 73: note the processing performed upon exiting the loop in lines 26–71. Indeed, the [fgets] function has the drawback of returning the boolean value FALSE both when reading lines encounters the end-of-file marker and when reading fails due to an error. To distinguish between the two cases, we check whether we have reached the end of the file using the [feof] function. If we have not reached the end of the file, it means an error has occurred, and we then throw an exception;
- lines 83–88: the [finally] block is executed regardless of whether an exception occurred during file processing;
- line 85: if the file has been opened, then the file’s ‘handle’ [$taxPayersFile] has the Boolean value TRUE; otherwise, it is FALSE;
- lines 99–105: the private method [saveString] used on line 79 to save the error array to a text file;
- line 99: the [saveString] method takes two parameters:
- [string $filename], which is the name of the text file used to save the data;
- [string $data], which is the string to be saved to the text file. This string will be a sequence of lines terminated by the newline character \n;
- line 102: the PHP function [file_puts_contents] writes a string to a text file. It opens the file, writes the string to it, and closes the file. It returns FALSE if an error occurs;
- line 103: if an error occurs, an exception is thrown;
- lines 92–96: implementation of the [saveResults] method of the [InterfaceDao] interface. The private method [saveString] is used again. Here, the second parameter of [saveString] is a string constructed from the array [$taxPayersData], whose elements are of type [TaxPayerData]. One might wonder what the result of the operation will be:
implode("\n", $taxPayersData)
We defined the following [__toString] method in the [TaxPayerData] class (see linked section):
public function __toString() {
// JSON string of the object
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
The operation
implode("\n", $taxPayersData)
will concatenate each element of the array [$taxPayersData]—converted to a string by its [__toString] method—with the newline character \n. This will result in a string of the form:
json1\njson2\n…
Conclusion
The [TraitDao] trait has implemented two of the methods from the [InterfaceDao] interface, [getTaxPayersData] and [saveResults]:
<?php
// namespace
namespace Application;
interface InterfaceDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// Read data from the tax administration (tax brackets)
public function getTaxAdminData(): TaxAdminData;
// save results
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
We still need to implement the [getTaxAdminData] method, which retrieves data from the tax administration.
11.3.3. The [ImpotsWithTaxAdminDataInJsonFile] class
The [ImpotsWithTaxAdminDataInJsonFile] class implements the [InterfaceDao] interface as follows:
<?php
// namespace
namespace Application;
// definition of a class ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
// use of a trait
use TraitDao;
// the TaxAdminData object containing tax bracket data
private $taxAdminData;
// the constructor
public function __construct(string $taxAdminDataFilename) {
// we want to initialize the [$this->taxAdminData] attribute
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// returns the data used to calculate the tax
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Comments
- line 7: the class [ImpotsWithTaxAdminDataInJsonFile] implements the interface [InterfaceDao];
- line 9: the [ImpotsWithTaxAdminDataInJsonFile] class uses the [traitDao] trait, which, as we know, implements the [getTaxPayersData] and [saveResults] methods of the [InterfaceDao] interface. All that remains, therefore, is for the [ImpotsWithTaxAdminDataInJsonFile] class to implement the [getTaxAdminData] method, which retrieves data from the tax administration;
- Line 11: the attribute of type [TaxAdminData] returned by the [getTaxAdminData] method on lines 20–22. This attribute is initialized by the constructor on lines 14–17;
We are now done with the [DAO] layer of our application: we have a class that fully implements the [InterfaceDao] interface we defined. We can now move on to the [business] layer.
11.4. The [business] layer
We will now implement layer [2] of our architecture:

11.4.1. The [InterfaceMétier] interface
The interface for the [business] layer will be as follows:
<?php
// namespace
namespace Application;
interface BusinessInterface {
// Calculate a taxpayer's taxes
public function calculateTax(string $married, int $children, int $salary): array;
// Calculate taxes in batch mode
public function executeBatchTaxes(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
Comments
- line 9: the [BusinessInterface] interface can calculate the tax amount for an individual taxpayer provided it is given the following information: marital status, number of children, annual salary. The [calculateTax] method does not use the [DAO] layer, so it does not throw exceptions;
- Line 9: The [BusinessInterface] interface can also calculate the tax amount for a group of taxpayers whose data is collected in the text file named [$taxPayersFileName]. It writes the results to a text file named [$resultsFileName]. The [executeBatchImpots] method must communicate with the [dao] layer, which handles file system access. Exceptions may then be propagated from the [dao] layer, which the [executeBatchImpots] method will not catch: it will allow them to propagate to the main script. Non-fatal errors are logged in the text file named [$errorsFileName];
- line 9: the [calculateTax] method is a purely [business] method. It does not concern itself with the source of the data it uses;
- line 12: the [executeBatchImpots] method will interact with the [dao] layer to read and write data to text files. It will repeatedly call the business method [calculerImpot];
11.4.2. The [Business] class
The [Metier] class implements the [InterfaceMetier] interface as follows:
<?php
// namespace
namespace Application;
class Business implements BusinessInterface {
// DAO layer
private $dao;
// tax administration data
private $taxAdminData;
//---------------------------------------------
// [dao] layer setter
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(DaoInterface $dao) {
// store a reference to the [dao] layer
$this->dao = $dao;
// retrieve the data needed to calculate the tax
// the [getTaxAdminData] method may throw an ExceptionImpots exception
// we let it propagate to the calling code
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// calculate the tax
// --------------------------------------------------------------------------
public function calculateTax(string $married, int $children, int $salary): array {
…
// result
return ["tax" => floor($tax), "surcharge" => $surcharge, "discount" => $discount, "reduction" => $reduction, "rate" => $rate];
}
// --------------------------------------------------------------------------
private function calculateTax2(string $married, int $children, float $salary): array {
…
// result
return ["tax" => $tax, "surcharge" => $surcharge, "rate" => $coeffR[$i]];
}
// taxableIncome = annualSalary - deduction
// the deduction has a minimum and a maximum
private function getTaxableIncome(float $salary): float {
…
// result
return floor($taxableIncome);
}
// calculates any tax deduction
private function getTaxDeduction(string $spouse, float $salary, float $taxes): float {
…
// result
return ceil($discount);
}
// calculates a possible reduction
private function getDiscount(string $married, float $salary, int $children, float $taxes): float {
…
// result
return ceil($discount);
}
// Calculate taxes in batch mode
public function executeBatchTaxes(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
…
// saving results
$this->dao->saveResults($resultsFileName, $results);
}
}
Comments
- line 6: the [Business] class implements the [BusinessInterface] interface, i.e., the [calculateTax] (lines 30–34) and [executeBatchTaxes] (lines 66–70) methods;
- line 8: a reference to the [dao] layer. This is required so that the [business] layer knows where to look when it needs external data. This attribute will be initialized via the setter in lines 14–17 or via the constructor in lines 19–26;
- line 10: the object of type [TaxAdminData] that encapsulates the tax administration data. This data is required by the business method [calculateTax]. This attribute is initialized via the constructor in lines 19–26;
- lines 19–26: the constructor initializes the class’s two attributes:
- the [$dao] attribute is initialized with the reference passed as a parameter to the constructor. Note that the type of this parameter is that of the [InterfaceDao] interface, allowing the [Metier] class to be initialized by any class implementing this interface;
- the attribute [$taxAdminData] is initialized by calling the [getTaxAdminData] method of the [dao] layer;
We conclude that when the methods [calculateTaxes] and [executeBatchTaxes] are executed, both attributes [$dao] and [$taxAdminData] are initialized.
The [calculateTaxes] method is as follows:
public function calculateTax(string $married, int $children, int $salary): array {
// $marié: yes, no
// $children: number of children
// $salary: annual salary
// $this->taxAdminData: tax administration data
//
// Check that we have the tax administration data
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// Calculate tax with children
$result1 = $this->calculateTax2($married, $children, $salary);
$tax1 = $result1["tax"];
// Calculate tax without children
if ($children != 0) {
$result2 = $this->calculateTax2($married, 0, $salary);
$tax2 = $result2["tax"];
// apply the family quotient cap
$halfPartCap = $this->taxAdminData->getQfHalfPartCap();
if ($children < 3) {
// $PLAFOND_QF_DEMI_PART euros for the first 2 children
$tax2 = $tax2 - $children * $half-share-cap;
} else {
// $PLAFOND_QF_DEMI_PART euros for the first 2 children, double that for subsequent children
$tax2 = $tax2 - 2 * $half-share_limit - ($children - 2) * 2 * $half-share_limit;
}
} else {
$tax2 = $tax1;
$result2 = $result1;
}
// take the higher tax
if ($tax1 > $tax2) {
$tax = $tax1;
$rate = $result1["rate"];
$surcharge = $result1["surcharge"];
} else {
$surcharge = $tax2 - $tax1 + $result2["surcharge"];
$tax = $tax2;
$rate = $result2["rate"];
}
// calculate any tax credit
$discount = $this->getDiscount($spouse, $salary, $tax);
$tax -= $discount;
// calculate any tax credit
$reduction = $this->getReduction($spouse, $salary, $children, $tax);
$tax -= $reduction;
// result
return ["tax" => floor($tax), "surcharge" => $surcharge, "discount" => $discount, "reduction" => $reduction, "rate" => $rate];
}
Comments
- This code is from the [AbstractBaseImpots::calculateTax] method in version 3, explained in the linked section. The same applies to the private methods [calculateTax2, getDiscount, getReduction, getTaxableIncome];
The [Metier::executeBatchImpots] method is as follows:
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// We allow exceptions originating from the [DAO] layer to be propagated
// retrieve taxpayer data
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// results array
$results = [];
// process them
foreach ($taxPayersData as $taxPayerData) {
// calculate the tax
$result = $this->calculateTax(
$taxPayerData->isMarried(),
$taxPayerData->getChildren(),
$taxPayerData->getSalary());
// update [$taxPayerData]
$taxPayerData->setAmount($result["tax"]);
$taxPayerData->setDiscount($result["discount"]);
$taxPayerData->setSurcharge($result["surcharge"]);
$taxPayerData->setRate($result["rate"]);
$taxPayerData->setReduction($result["reduction"]);
// add the result to the results array
$results[] = $taxPayerData;
}
// save the results
$this->dao->saveResults($resultsFileName, $results);
}
Comments
- line 1: the method must repeatedly call the [calculateTax] method for each taxpayer found in the text file named [$taxPayersFileName]. It must write the results to the text file named [$resultsFileName]. Non-fatal errors encountered are logged in the text file named [$errorsFileName]. The method does not throw exceptions itself but allows those thrown by the [dao] layer to propagate;
- Line 4: taxpayer data is requested from the [dao] layer. This returns an array of elements of type [TaxPayerData], which is a class of attributes [married, numberOfChildren, salary, amount, deduction, reduction, surcharge, rate] (see linked paragraph). If an exception occurs here, since it is not caught by a catch block, it will automatically propagate back to the calling code. This means that in the event of an exception, line 6 is not executed;
- line 6: the results array of type [TaxPayerData];
- lines 8–22: the tax is calculated for each element of the taxpayer array [$taxPayersData]. To do this, the internal method [calculateTax] is called (line 10);
- lines 15–19: the result is used to initialize the attributes of [TaxPayerData] that were not yet initialized;
- line 21: the result is added to the results array [$results];
- line 24: once the tax has been calculated for all taxpayers, the results are saved to a text file. The [dao] layer handles this task;
Conclusion
In general, the [business] layer is fairly simple to write because it interfaces with the [DAO] layer, which manages data access along with the associated error handling.
11.5. The main script
We will now write the script for layer [3] of our architecture:

The main script is as follows [main.php]:
<?php
// Strict adherence to the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// PHP error handling
//ini_set("display_errors", "0");
// Include interface and classes
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/TaxExceptions.php";
require_once __DIR__ . "/Utilities.php";
require_once __DIR__ . "/DaoInterface.php";
require_once __DIR__ . "/DaoProcessing.php";
require_once __DIR__ . "/TaxDaoWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/BusinessLogicInterface.php";
require_once __DIR__ . "/BusinessLogic.php";
// test -----------------------------------------------------
// definition of constants
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "results.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// Create the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// Create the [business] layer
$business = new Business($dao);
// Calculate taxes in batch mode
$businessLogic = new BusinessLogic($dao);
} catch (TaxException $ex) {
// display the error
print $ex->getMessage() . "\n";
}
// end
print "Done\n";
exit;
Comments
- Line 24: the name of the taxpayer data file;
- line 25: the name of the results file;
- line 26: the name of the error file;
- line 27: the name of the JSON file containing the tax authority data;
- line 31: creation of the [dao] layer;
- line 33: creation of the [business] layer based on this [dao] layer;
- line 35: execution of the [executeBatchImpots] method of the [business] layer;
- lines 36–39: we saw that the [business] layer could throw exceptions. They are caught here;
11.6. Visual tests
11.6.1. Test #1
With the following taxpayer file [taxpayersdata.txt]:
yes,2,55555
yes,2,50000
yes,3,50000
no,2,100000
no,3x,100000
yes,3,100000
yes,5,100000x
no,0,100000
yes,2,30000
no,0,200000
yes,3,200000
We obtain the following error file [errors.txt]:
Line [5] of the file [taxpayersdata.txt] is incorrect
Line [7] of the [taxpayersdata.txt] file is incorrect
and the following results file [resultats.txt]:
11.6.2. Test #2
In the main script, we assign a filename that does not exist to the taxpayer file:
The results displayed in the console are as follows:
Warning: fopen(taxpayersdata2.txt): failed to open stream: No such file or directory in C:\Data\st-2019\dev\php7\poly\scripts-console\impots\version-04\TraitDao.php on line 18
Unable to open taxpayer declarations [taxpayersdata2.txt] for reading
Done
Done.
- line 1: warnings from the PHP interpreter;
- line 2: error message from the exception thrown by the [dao] layer;
It is possible to suppress PHP interpreter error messages:

Line 21 of the code above instructs the system not to display PHP errors. During the development phase, it is necessary for them to be displayed. In production mode, they must be hidden.
The execution results are then as follows:
Unable to open taxpayer declarations [taxpayersdata2.txt] for reading
Done
11.7. Tests [Codeception]
Visual tests are very inadequate:
- we generally limit ourselves to just a few tests;
- we may not pay close enough attention during this visual inspection, and details can slip past us;
In the real world of professional development, tests are written by dedicated individuals for whom this is their primary role. They strive to make the tests as comprehensive as possible. To do this, they use testing frameworks.
Here, we will use the Codeception framework [https://codeception.com/] because it can be integrated into NetBeans. It is a framework with a wide range of capabilities. We will use only a few of them. The idea is to have a quick way, after each new version of the application exercise, to verify that it works. The existence of successful tests gives the developer confidence in the code they have written. This is an important factor.
11.7.1. Installing the [Codeception] framework
Like many PHP libraries, the [Codeception] framework is installed using [Composer]. So we open a Laragon terminal (see link in the paragraph).
First, we need to install the PHPUnit testing framework [https://phpunit.de/]. This is because Codeception uses the PHPUnit framework behind the scenes:

Next, we install the Codeception framework:

That’s it. Now let’s look at integrating [Codeception] into NetBeans.
11.7.2. Integrating [Codeception] into NetBeans

- In [1-2], access the project properties;
- In [3-4], we set [Codeception] as one of the project’s testing frameworks;


- In [5-8], initialize the [Codeception] framework for the project;

- In [9], a [tests] folder has been created, along with a [codeception.yml] configuration file in [10-11]. File [11] is the same as file [10]. Codeception simply created an [Important Files] folder to give file [10] a special designation;
- in [12-13], we return to the project properties;

- in [14-16], the [tests] folder [16] is designated as the project’s test folder;
- in [16], the [tests] folder then appears under the new name [Test Files]. The presence of this folder in a PHP project indicates that the project incorporates a unit testing framework;
- we will create our tests in the [unit] folder [17];
11.7.3. Tests for the [dao] layer

- We will create all our tests in the [unit] folder [1];
- The names of [Codeception] test classes must end with the keyword [Test], otherwise the classes will not be recognized as test classes;
Our [Codeception] test classes will have the following format [https://codeception.com/docs/05-UnitTests]:
<?php
// Strict adherence to the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// Loading the test environment
…
class DaoTest extends \Codeception\Test\Unit {
// test attributes
private $attribute1;
public function __construct() {
parent::__construct();
// Initialize the test environment
…
}
// tests
public function testTaxAdminData() {
// tests
$this->assertEquals($expected, $actual);
$this->assertEqualsWithDelta($expected, $actual, $delta);
$this->assertTrue($actual);
$this->assertFalse($actual);
$this->assertNull($actual);
$this->assertEmpty($actual);
$this->assertSame($expected, $actual);
…
}
}
Comments
- line 7: the test classes will be in the same namespace as the application under test;
- lines 9–10: here are the [require] statements to load the tested classes and interfaces;
- line 12: the name of the test class must end with the keyword [Test]. This class must extend the class [\Codeception\Test\Unit];
- lines 16–20: the constructor allows us to initialize the test environment;
- line 23: the names of test methods must begin with the keyword [test];
- lines 25–31: various test methods can be used;
The [DaoTest] test class will be as follows:
<?php
// Strict adherence to the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// include interface and classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/TaxExceptions.php";
require_once ROOT . "/Utilities.php";
require_once ROOT . "/DaoInterface.php";
require_once ROOT . "/DaoProcess.php";
require_once ROOT . "/TaxDaoWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/BusinessLogicInterface.php";
require_once ROOT . "/BusinessLogic.php";
require_once VENDOR . "/autoload.php";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// Create the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
Comments
To build the tests for a version of the application exercise, we will use an environment identical to the one used by the version’s main script. For version 04, this is the following [main.php] script:
<?php
// Strict adherence to the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// PHP error handling
ini_set("display_errors", "0");
// Include interface and classes
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/TaxExceptions.php";
require_once __DIR__ . "/Utilities.php";
require_once __DIR__ . "/DaoInterface.php";
require_once __DIR__ . "/DaoProcess.php";
require_once __DIR__ . "/TaxDaoWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/BusinessLogicInterface.php";
require_once __DIR__ . "/BusinessLogic.php";
// test -----------------------------------------------------
// definition of constants
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "results.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// Create the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// Create the [business] layer
$business = new Business($dao);
// Calculate taxes in batch mode
$businessLogic = new BusinessLogic($dao);
} catch (TaxException $ex) {
// display the error
print $ex->getMessage() . "\n";
}
// end
print "Done\n";
exit;
To test the [dao] layer, in the test class:
- we use the environment from lines 13–27 of [main.php];
- in the test class constructor, we instantiate the [dao] layer as in line 31;
- we write the test methods;
We will proceed in this manner for all test classes.
Let’s return to the complete code for the test class:
<?php
// Strict adherence to the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Include interface and classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/TaxExceptions.php";
require_once ROOT . "/Utilities.php";
require_once ROOT . "/DaoInterface.php";
require_once ROOT . "/DaoProcessing.php";
require_once ROOT . "/TaxDaoWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/BusinessLogicInterface.php";
require_once ROOT . "/BusinessLogic.php";
require_once VENDOR . "/autoload.php";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// Create the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
// calculation constants
$this->assertEquals(1551, $this->taxAdminData->getHalfShareIncomeLimit());
$this->assertEquals(21037, $this->taxAdminData->getIncomeLimitSingleForReduction());
$this->assertEquals(42074, $this->taxAdminData->getIncomeLimitForCoupleForReduction());
$this->assertEquals(3797, $this->taxAdminData->getReducedValueForHalfShare());
$this->assertEquals(1196, $this->taxAdminData->getSingleTaxDeductionLimit());
$this->assertEquals(1970, $this->taxAdminData->getCoupleDeductionLimit());
$this->assertEquals(1595, $this->taxAdminData->getSingleTaxCapForDeduction());
$this->assertEquals(2627, $this->taxAdminData->getPlafondImpotCouplePourDecote());
$this->assertEquals(12502, $this->taxAdminData->getMaxTenPercentDeduction());
$this->assertEquals(437, $this->taxAdminData->getMinTenPercentDeduction());
// tax brackets
$this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $this->taxAdminData->getLimites());
$this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $this->taxAdminData->getCoeffR());
$this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $this->taxAdminData->getCoeffN());
}
}
Comments
- lines 10–25: loading the environment required for testing and defining constants;
- lines 31–36: construction of the [dao] layer (line 34), followed by initialization of the [$taxAdminData] attribute (line 29). This attribute contains tax administration data;
- lines 39–55: the single test method. This consists of verifying that the content of the [$taxAdminData] attribute matches what is expected;
- lines 41–50: checks on the tax calculation constants;
- lines 52–55: checks on tax brackets. The [assertSame] method verifies that two PHP entities—in this case, arrays—are identical;
To run this test class, proceed as follows:

- in [1-2], run the test;
- [3]: the test results window;
- [4]: the executed test class;
- [5]: the results. Here, the single test method passed;
- [6]: when the test fails, or more commonly when no test has been run, check window [6]. Most often, the test environment failed to load, so no test could be executed. The errors displayed in [6] are the same as those you would see when running a standard PHP script;
Let’s look at an example of a failed test:
In the test class, we introduce an error in the definition of a constant:
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");
then we run the test. The result is as follows:

In window [4]:

11.7.4. [Business] Layer Tests
The [MetierTest] test class follows the same construction rules as the [DaoTest] class, but there are more test methods:
<?php
// Strict adherence to the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Include interface and classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/TaxExceptions.php";
require_once ROOT . "/Utilities.php";
require_once ROOT . "/DaoInterface.php";
require_once ROOT . "/DaoProcessing.php";
require_once ROOT . "/TaxDaoWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/BusinessLogicInterface.php";
require_once ROOT . "/BusinessLogic.php";
require_once VENDOR . "/autoload.php";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class MetierTest extends \Codeception\Test\Unit {
// business layer
private $business;
public function __construct() {
parent::__construct();
// creation of the [DAO] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
// Create the [business] layer
$this->business = new Business($dao);
}
// tests
public function test1() {
$result = $this->business->calculateTax("yes", 2, 55555);
$this->assertEqualsWithDelta(2815, $result["tax"], 1);
$this->assertEqualsWithDelta(0, $result["markup"], 1);
$this->assertEqualsWithDelta(0, $result["markdown"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEquals(0.14, $result["rate"]);
}
public function test2() {
$result = $this->businessLogic->calculateTax("yes", 2, 50000);
$this->assertEqualsWithDelta(1385, $result["tax"], 1);
$this->assertEqualsWithDelta(0, $result["surcharge"], 1);
$this->assertEqualsWithDelta(384, $result["discount"], 1);
$this->assertEqualsWithDelta(347, $result["reduction"], 1);
$this->assertEquals(0.14, $result["rate"]);
}
public function test3() {
$result = $this->businessLogic->calculateTax("yes", 3, 50000);
$this->assertEqualsWithDelta(0, $result["tax"], 1);
$this->assertEqualsWithDelta(0, $result["surcharge"], 1);
$this->assertEqualsWithDelta(720, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0.14, $result["rate"]);
}
public function test4() {
$result = $this->businessLogic->calculateTax("no", 2, 100000);
$this->assertEqualsWithDelta(19884, $result["tax"], 1);
$this->assertEqualsWithDelta(4480, $result["surcharge"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0.41, $result["rate"]);
}
public function test5() {
$result = $this->businessLogic->calculateTax("no", 3, 100000);
$this->assertEqualsWithDelta(16782, $result["tax"], 1);
$this->assertEqualsWithDelta(7176, $result["surcharge"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0.41, $result["rate"]);
}
public function test6() {
$result = $this->businessLogic->calculateTax("yes", 3, 100000);
$this->assertEqualsWithDelta(9200, $result["tax"], 1);
$this->assertEqualsWithDelta(2180, $result["surcharge"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0.3, $result["rate"]);
}
public function test7() {
$result = $this->businessLogic->calculateTax("yes", 5, 100000);
$this->assertEqualsWithDelta(4230, $result["tax"], 1);
$this->assertEqualsWithDelta(0, $result["surcharge"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0.14, $result["rate"]);
}
public function test8() {
$result = $this->businessLogic->calculateTax("no", 0, 100000);
$this->assertEqualsWithDelta(22986, $result["tax"], 1);
$this->assertEqualsWithDelta(0, $result["surcharge"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0.41, $result["rate"]);
}
public function test9() {
$result = $this->businessLogic->calculateTax("yes", 2, 30000);
$this->assertEqualsWithDelta(0, $result["tax"], 1);
$this->assertEqualsWithDelta(0, $result["surcharge"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0, $result["rate"]);
}
public function test10() {
$result = $this->businessLogic->calculateTax("no", 0, 200000);
$this->assertEqualsWithDelta(64210, $result["tax"], 1);
$this->assertEqualsWithDelta(7498, $result["surcharge"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0.45, $result["rate"]);
}
public function test11() {
$result = $this->businessLogic->calculateTax("yes", 3, 200000);
$this->assertEqualsWithDelta(42842, $result["tax"], 1);
$this->assertEqualsWithDelta(17283, $result["surcharge"], 1);
$this->assertEqualsWithDelta(0, $result["discount"], 1);
$this->assertEqualsWithDelta(0, $result["reduction"], 1);
$this->assertEquals(0.41, $result["rate"]);
}
}
Comments
- lines 10–25: loading of files defining the test environment. This is the same as for the [dao] layer;
- lines 31–37: instantiation of the [dao] and [business] layers;
- lines 40–47: a tax calculation test;
- line 41: a specific tax calculation is performed using the [business] layer;
- lines 42–46: verification that the results obtained match those of the tax authority’s simulator [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm];
- lines 23–26: equality tests are performed to within 1 euro. Indeed, we observed that rounding issues caused the document’s algorithm to yield the expected results to within 1 euro;
- line 27: the tax rate is calculated without any margin of error;
- lines 49–137: this type of test is repeated 10 times, each time with a different taxpayer configuration;
The tests yield the following results:

11.7.5. Tests for future versions
Moving forward, the tests for the [dao] and [business] layers will be identical to those in version 04. Only the test environment will change. We will therefore present only this environment and the test results.