18. Practice Exercise – Version 8
We will revisit the sample application – version 5 (link paragraph) and turn it into a client/server application.
18.1. Introduction
The architecture of version 5 was as follows:

- the layer called [dao] (Data Access Objects) handles interactions with the MySQL database and the local file system;
- the layer called [business] performs the tax calculation;
- the main script acts as the orchestrator: it instantiates the [DAO] and [business logic] layers and then communicates with the [business logic] layer to perform the necessary tasks;
We will migrate this architecture to the following client/server architecture:

- In [2], we will reuse the [DAO] layer from version 5, removing the methods for accessing the local file system. These methods will be migrated to the client’s [DAO] layer [6, 7];
- In [3], the [business] layer will remain the same as in version 5, minus its [executeBatchImpôts, saveResults] methods, which will be migrated to the client’s [DAO] layer [7];
- In [4], the server script must be written: it will need to:
- create the [business] and [DAO] layers [3, 2];
- communicate with the client script [5, 7];
- In [7], the client's [dao] layer must be written:
- it will be an HTTP client of the server script [4, 5];
- it will reuse the methods for accessing the local file system from the [dao] layer of version 5;
- in [8], the client’s [business] layer will conform to the [BusinessInterface] interface from version 5. Its implementation will, however, be different. In version 5, the [business] layer performed the tax calculation. Here, it is the server’s [business] layer that performs this calculation. The [business] layer will therefore call upon the [DAO] layer [7] to communicate with the server and request that it calculate the tax;
- in [9], the console script will need to instantiate the client’s [DAO, business] layers and launch its execution;
18.2. The server
We are focusing on the server side of the application.

This architecture will be implemented by the following scripts:

18.2.1. Entities exchanged between layers

The entities exchanged between layers are those from version 5 described in the linked section.
18.2.2. The [dao] layer

The [dao] layer implements the following [InterfaceServerDao] interface:
<?php
// namespace
namespace Application;
interface InterfaceServerDao {
// reading tax administration data
public function getTaxAdminData(): TaxAdminData;
}
- Line 9: The [getTaxAdminData] method retrieves tax administration data from a database;
The [InterfaceServerDao] interface is implemented by the following [ServerDao] class:
<?php
// namespace
namespace Application;
// definition of a ImpotsWithDataInDatabase class
class ServerDao implements InterfaceServerDao {
// the TaxAdminData object containing tax bracket data
private $taxAdminData;
// the [Database] type object containing the characteristics of the BD
private $database;
// manufacturer
public function __construct(string $databaseFilename) {
// store the JSON configuration of the bd
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// we prepare the attribute
$this->taxAdminData = new TaxAdminData();
try {
// open the database connection
$connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// we want every SGBD error to trigger an exception
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// start a transaction
$connexion->beginTransaction();
// fill in the tax bracket table
$this->getTranches($connexion);
// fill in the constants table
$this->getConstantes($connexion);
// the transaction is completed successfully
$connexion->commit();
} catch (\PDOException $ex) {
// is there a transaction in progress?
if (isset($connexion) && $connexion->inTransaction()) {
// transaction ends in failure
$connexion->rollBack();
}
// trace the exception back to the calling code
throw new ExceptionImpots($ex->getMessage());
} finally {
// close the connection
$connexion = NULL;
}
}
// reading data from the database
private function getTranches($connexion): void {
…
}
// reading the constants table
private function getConstantes($connexion): void {
…
}
// returns data for tax calculation
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
This code was presented in the linked section.
18.2.3. The [business] layer


The [business] layer implements the following [InterfaceServerMetier] interface:
<?php
// namespace
namespace Application;
interface InterfaceServerMetier {
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
The [InterfaceServerMetier] interface is implemented by the following [ServerMetier] class:
<?php
// namespace
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// dao layer
private $dao;
// tax administration data
private $taxAdminData;
//---------------------------------------------
// setter couche [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $dao) {
// a reference is stored on the [dao] layer
$this->dao = $dao;
// recover data for tax calculation
// method [getTaxAdminData] may throw a ExceptionImpots exception
// we then let it go back to the calling code
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// tAX CALCULATION
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// result
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
…
// result
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=annualwage-discount
// the allowance has a minimum and a maximum
private function getRevenuImposable(float $salaire): float {
…
// result
return floor($revenuImposable);
}
// calculates any discount
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// result
return ceil($décôte);
}
// calculates any reduction
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
..
// result
return ceil($réduction);
}
}
This code has already been discussed in Version 1 in the linked section. Its object-oriented version with a database was presented in the linked section.
18.2.4. The server script


The server script implements the [web] layer [4]. The [impots-server] script is configured by the following JSON file [config-server.json]:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
"databaseFilename": "Data/database.json",
"taxAdminDataFileName": "Data/taxadmindata.json",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/ExceptionImpots.php",
"/Entities/TaxAdminData.php",
"/Entities/Database.php",
"/Dao/InterfaceServerDao.php",
"/Dao/ServerDao.php",
"/Métier/InterfaceServerMetier.php",
"/Métier/ServerMetier.php"
],
"absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
"users": [
{
"login": "admin",
"passwd": "admin"
}
]
}
- line 1: the root directory from which file paths will be measured;
- line 2: the JSON configuration file for the MySQL database;
- line 3: the JSON file containing tax administration data;
- lines 5–14: the application files;
- line 15: the dependency on third-party libraries, in this case Symfony;
- lines 16–20: the array of users authorized to use the application;
The JSON files [database.json, taxadmindata.json] are those from version 5, as described in the linked section.
The [impots-server] script implements the [web] layer as follows:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definition of constants
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// retrieve the current query
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// does the user exist?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// set the response status code
if (!$trouvé) {
// not found - code 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// error msg
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// end
exit;
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// mistake?
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// marital status is restored
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// mistake?
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// the number of children
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// number of children must be an integer >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// mistake?
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// we recover the annual salary
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// salary must be an integer >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// mistake?
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// other parameters in the query?
foreach (\array_keys($request->query->all()) as $key) {
// valid parameter?
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// mistakes?
if ($erreurs) {
// an error code 400 is sent to the customer
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// we have everything you need to work
// server architecture creation
$msgErreur = "";
try {
// creation of the [dao] layer
$dao = new ServerDao($config["databaseFilename"]);
// creation of the [business] layer
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// we note the error
$msgErreur = utf8_encode($ex->getMessage());
}
// mistake?
if ($msgErreur) {
// an error code 500 is sent to the customer
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
Comments
- line 16: we load the configuration file;
- lines 18–26: we load all dependencies;
- line 29: the name of the [database.json] file;
- lines 32–33: declare the classes from the third-party libraries that will be used;
- lines 36–38: prepare a JSON response;
- lines 40–52: verify that the user making the request is indeed an authorized user;
- lines 54–63: if not, we send an HTTP 401 code indicating access denied. Upon receiving this code and the HTTP header [WWW-Authenticate => Basic realm=], most browsers display an authentication window prompting the user to log in;
- line 59: the server’s JSON response explains the cause of the error. All server responses will be a JSON string in the form of an array [‘response’=>’something’];
- Lines 64–117: We verify the validity of the request:
- a GET request with exactly three parameters;
- a [married] parameter whose value must be ‘yes’ or ‘no’;
- a parameter [children] whose value must be an integer >= 0;
- a parameter [salary] whose value must be an integer >=0;
- line 65: every time an error is detected, an error message is added to the array [$errors];
- lines 120–126: if an error occurs, the HTTP code [400 Bad Request] is sent to the client (line 122);
- line 123: the server’s JSON response explains the cause of the error;
- Starting from line 132, everything has been verified. We can instantiate the [dao, business] layers. This instantiation has a cost and should only be done if we are sure we have a valid request;
- lines 130–138: the server architecture is created. Constructing the [DAO] layer may throw an [ExceptionImpots] exception. If this exception occurs, the error is logged;
- lines 135–138: if an exception occurred, then we send HTTP status code 500 to the client. This code indicates that the server has crashed;
- line 143: the response explains the cause of the error;
- line 148 : the tax calculation is delegated to the [business] layer;
- lines 150–151: send the response;
Let’s test this script with a browser. Let’s request the secure URL [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

- in [1], the requested secure URL;
- in [2], the three parameters [married, children, salary];
- in [3], Laragon’s Apache server sent a self-signed SSL certificate. The browser detected this and displays a security warning: it considers the server’s site to be untrustworthy;
- in [4], we continue;

- in [6], we continue;

- In [7], the browser displays a window for the user to log in;
- in [9,10], type [admin] and [admin];

- in [13], the server's JSON response;
Let’s run some error tests:
We request the URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]
We get the following result:

We shut down the MySQL DBMS and request the URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

18.2.5. [Codeception] Tests
Each time we build a new version of the server, we will test the [business] and [DAO] layers as has been done since version 04 (see paragraphs link and link).
First, we associate the [scripts-web] project with the [Codeception] tests. To do this, follow the same procedure used for the [scripts-console] project in the link paragraph. We end up with a [scripts-web] project containing a [Test Files] folder:

We will create a test for the [dao] layer and one for the [business] layer.
18.2.5.1. Tests for the [dao] layer

The [ServerDaoTest] will be as follows:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// parent
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
Comments
- lines 9–24: We set up the same working environment as that of the server [impots-server.php]. This is done in lines 9–12 by defining the two constants on which the environment depends;
- lines 32–40: we create an instance of the [dao] layer to be tested, as was done in the server script [impots-server.php];
- From this point on, we are under the same conditions as the server script [impots-server.php]: we can start the tests;
- lines 43–45: the [testTaxAdminData] method is the one described in the linked section;
The test results are as follows:

18.2.5.2. Tests of the [business] layer

The [ServerMetierTest] test will be as follows:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test class
class ServerMetierTest extends \Codeception\Test\Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// creation of the [business] layer
$this->métier = new ServerMetier($dao);
}
// tests
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
Comments
- Lines 9–24: We set up the same working environment as that of the [impots-server.php] server. This is done in lines 9–12 by defining the two constants on which the environment depends;
- lines 30–38: we create an instance of the [business] layer to be tested, as was done in the server script [impots-server.php];
- From this point on, we are under the same conditions as the server script [impots-server.php]: we can start the tests;
- lines 40–53: the methods [test1, test2…, test11] are those described in the linked section;
The test results are as follows:

18.3. The client
We are focusing on the client-side of the application.

This architecture will be implemented by the following scripts:

18.3.1. Entities exchanged between layers

The entities listed above have all been described and are already in use:
- [BaseEntity] in the link section;
- [ExceptionImpots] in the link section;
- [TaxPayerData] in the link section;
18.3.2. The [dao] layer

The [dao] layer implements the following [InterfaceClientDao] interface:
<?php
// namespace
namespace Application;
interface InterfaceClientDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// recording results
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- line 9: the [getTaxPayersData] function loads taxpayer data from the [$taxPayersFilename] file into memory. If there are any errors, they are logged in the [$errorsFilename] file;
- line 12: the [calculateTaxes] function calculates a taxpayer’s tax;
- Line 15: The [saveResults] function saves the data from the [$taxPayersData] array—which represents the results of several tax calculations—to the [$resultsFilename] file;
The [InterfaceClientDao] interface is implemented by the following [ClientDao] class:
<?php
namespace Application;
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// using a Trait
use TraitDao;
// attributes
private $urlServer;
private $user;
// manufacturer
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// create a HTTP customer
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// make a request to the server
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// the answer is retrieved
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logs
// print "$json=json\n";
// retrieve response status
$statusCode = $response->getStatusCode();
// mistake?
if ($statusCode !== 200) {
// we have an error - we throw an exception
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// we return the answer
return $réponse;
}
}
Comments
- line 10: we insert [TraitDao] (see linked paragraph) which implements the methods [getTaxPayersData] and [saveResults]. This leaves only the [calculateTaxes] method to be implemented. This is implemented on lines 22–49;
- lines 16–19: the constructor of the [ClientDao] class takes two parameters:
- the URL [$urlServer] of the tax calculation server;
- the array [$user] of keys ‘login’ and ‘passwd’ that defines the user making the request;
- line 22: the [calculateTaxes] method receives the three parameters to be sent to the tax calculation server;
- lines 24–27: an HTTP client is created with:
- line 25: the credentials of the user making the request;
- line 26: the option that prevents the HTTP client from verifying the validity of the SSL certificate sent by the server;
- lines 29–34: the server is queried with the three parameters it expects;
- line 36: we retrieve the JSON response from the server. If we do not set the [false] parameter on the [Response::getContent] method, then if the server’s response status is in the [3xx-5xx] range (error case), the [Response] object throws an exception as soon as we attempt to retrieve the response content [Response::getContent] or its HTTP headers [Response::getHeaders]. Here, regardless of the HTTP status of the response, we want to be able to access its content, if only to log it (line 40);
- lines 37-38: the server’s response is a JSON string of an array [‘response’=>something]. We retrieve the [something];
- line 40: we log the JSON response in development mode;
- line 42: we retrieve the response status code;
- lines 44-49: if the HTTP status code is not 200, then our server has encountered a problem. We then throw an [ExceptionImpots] exception with a message consisting of the server’s JSON response appended with the HTTP status code;
- line 51: we return the result, which is an associative array with the keys [tax, surcharge, discount, reduction, rate];
18.3.3. The [business] layer


The [business] layer [8] implements the following [BusinessClientInterface]:
<?php
// namespace
namespace Application;
interface InterfaceClientMetier {
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- line 9: the [calculateTaxes] function calculates the tax;
- line 12: the [executeBatchImpots] function calculates the tax for taxpayers whose data is in the [$taxPayersFileName] file, writes the results to the [$resultsFileName] file, and writes any errors encountered to the [$errorsFileName] file;
The [BusinessClientInterface] interface is implemented by the following [BusinessClient] class:
<?php
// namespace
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// attribute
private $clientDao;
// manufacturer
public function __construct(InterfaceClientDao $clientDao) {
// the reference is stored on the [dao] layer
$this->clientDao = $clientDao;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// we let the exceptions coming from the [dao] layer flow upwards
// retrieve taxpayer data
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// results table
$results = [];
// we exploit them
foreach ($taxPayersData as $taxPayerData) {
// tax calculation
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// complete [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// put the result in the results table
$results [] = $taxPayerData;
}
// recording results
$this->clientDao->saveResults($resultsFileName, $results);
}
}
Comments
- lines 11–14: the constructor of the [ClientMetier] class receives a reference to the [dao] layer as a parameter;
- lines 17–19: the tax calculation is delegated to the [dao] layer;
- lines 20–38: the [executeBatchImpots] function was described in the link section;
18.3.4. The main script


The client script [MainImpotsClient.php] implements the [console] layer [9]. It is configured by the following JSON file [conf-client.json]:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": [
"Entities/BaseEntity.php",
"Entities/TaxPayerData.php",
"Entities/ExceptionImpots.php",
"Utilities/Utilitaires.php",
"Dao/InterfaceClientDao.php",
"Dao/TraitDao.php",
"Dao/ClientDao.php",
"Métier/InterfaceClientMetier.php",
"Métier/ClientMetier.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php"
],
"user": {
"login": "admin",
"passwd": "admin"
},
"urlServer": "https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php"
}
- Line 1: the client's root directory;
- line 2: the JSON file containing taxpayer data;
- line 3: the JSON file containing the results;
- line 4: the JSON file containing the errors;
- lines 6–19: the various dependencies of the client project;
- lines 20–23: the user sending requests to the tax calculation server;
- line 24: the secure URL of the tax calculation server;
The code for the [MainImpotsClient.php] script is as follows:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "../Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definition of constants
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// symfony dependencies
use Symfony\Component\HttpClient\HttpClient;
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$clientMetier = new ClientMetier($clientDao);
// tax calculation in batch mode
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// error is displayed
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;
Comments
- line 13: path to the configuration file;
- line 16: processing the configuration file;
- lines 18–26: loading dependencies;
- line 37: creation of the [dao] layer. We pass the two pieces of information the layer constructor expects:
- the URL of the tax calculation server;
- the credentials of the user who will make the requests;
- line 39: creation of the [business] layer. We pass a reference to the [dao] layer that was just created to the layer constructor;
- line 43: we ask the [business] layer to:
- calculate the taxes for all taxpayers in the file $config["taxPayerDataFileName"];
- write the results to the file $config["resultsFileName"];
- write errors to the file $config["errorsFileName"];
- Line 43 may throw exceptions;
- Line 46: Display the exception’s error message;
Running the client yields the same results as previous versions. Check the following files:
- [Data/taxpayersdata.json]: taxpayer data for whom the tax amount is calculated;
- [Data/results.json]: results for the various taxpayers in the [Data/taxpayersdata.json] file;
- [Data/errors.json]: errors that may have occurred while processing the [Data/taxpayersdata.json] file;
Let’s look at the possible error cases. First, let’s stop the Laragon server. The results in the client console are then as follows:
Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé
Now let’s start only the Apache server and not the MySQL DBMS:

The results in the client console are as follows:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}
Terminé
Now, let’s start MySQL and then modify the user who connects in [config-client]:
The results in the client console are as follows:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
18.3.5. Tests [Codeception]
As we did for previous versions, we will write [Codeception] tests for version 08.

18.3.5.1. Testing the [business] layer
The [ClientMetierTest.php] test is as follows:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// test class
class ClientMetierTest extends \Codeception\Test\Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$this->métier = new ClientMetier($clientDao);
}
// tests
public function test1() {
…
}
-------------
public function test11() {
…
}
}
Comments
- lines 10–26: definition of the test environment. We use the same one as the main script [MainImpotsClient] described in the linked section;
- lines 33–41: construction of the [dao] and [business] layers;
- line 40: the attribute [$this→business] references the [business] layer;
- lines 44–51: the methods [test1, test2…, test11] are those described in the linked section;
The test results are as follows:
