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 class ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
// the TaxAdminData object that contains tax bracket data
private $taxAdminData;
// the [Database] object containing the database properties
private $database;
// constructor
public function __construct(string $databaseFilename) {
// store the database's JSON configuration
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// prepare the attribute
$this->taxAdminData = new TaxAdminData();
try {
// Open the database connection
$connection = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// We want an exception to be thrown for every DBMS error
$connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// start a transaction
$connection->beginTransaction();
// populate the tax brackets table
$this->getTranches($connection);
// populate the constants table
$this->getConstants($connection);
// commit the transaction
$connection->commit();
} catch (\PDOException $ex) {
// Is there a transaction in progress?
if (isset($connection) && $connection->inTransaction()) {
// roll back the transaction
$connection->rollBack();
}
// throw the exception to the calling code
throw new ExceptionImpots($ex->getMessage());
} finally {
// close the connection
$connection = NULL;
}
}
// read data from the database
private function getSlices($connection): void {
…
}
// read the constants table
private function getConstants($connection): void {
…
}
// returns the data needed to calculate the tax
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 BusinessServerInterface {
// Calculate a taxpayer's taxes
public function calculateTax(string $married, int $children, int $salary): 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;
//---------------------------------------------
// [DAO] layer setter
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $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 then 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 getDiscount(string $married, float $salary, float $taxes): float {
…
// result
return ceil($discount);
}
// calculates a possible discount
private function getDiscount(string $married, float $salary, int $children, float $taxes): float {
..
// result
return ceil($discount);
}
}
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",
"/Business/BusinessServerInterface.php",
"/Business/ServerBusiness.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 the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// PHP error handling
//ini_set("display_errors", "0");
//
// path to the configuration file
define("CONFIG_FILENAME", "Data/config-server.json");
// retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include dependencies required by the script
$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;
// preparing the server's JSON response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// retrieve the current request
$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;
$found = FALSE;
while (!$found && $i < count($users)) {
$found = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// Set the response status code
if (!$found) {
// not found - code 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Tax Calculation Server\"")]);
// error message
$response->setContent(\json_encode(["response" => ["error" => "Authentication failed [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// end
exit;
}
// we have a valid user - we check the received parameters
$errors = [];
// we should have three GET parameters
$method = strtolower($request->getMethod());
$error = $method !== "get" || $request->query->count() != 3;
// error?
if ($error) {
$errors[] = "GET method required with only the parameters [married, children, salary]";
}
// retrieve marital status
if (!$request->query->has("married")) {
$errors[] = "married parameter missing";
} else {
$married = trim(strtolower($request->query->get("married")));
$error = $married !== "yes" && $married !== "no";
// error?
if ($error) {
$errors[] = "invalid married parameter [$married]";
}
}
// Get the number of children
if (!$request->query->has("children")) {
$errors[] = "missing children parameter";
} else {
$children = trim($request->query->get("children"));
// The number of children must be an integer >= 0
$error = !preg_match("/^\d+$/", $children);
// Error?
if ($error) {
$errors[] = "invalid children parameter [$children]";
}
}
// retrieve the annual salary
if (!$request->query->has("salary")) {
$errors[] = "salary parameter missing";
} else {
// The salary must be an integer >= 0
$salary = trim($request->query->get("salary"));
$error = !preg_match("/^\d+$/", $salary);
// error?
if ($error) {
$errors[] = "invalid salary parameter [$salary]";
}
}
// other parameters in the query?
foreach (\array_keys($request->query->all()) as $key) {
// Is the parameter valid?
if (!\in_array($key, ["married", "children", "salary"])) {
$errors[] = "invalid parameter [$key]";}
}
// errors?
if ($errors) {
// send a 400 error code to the client
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["response" => ["errors" => $errors]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// we have everything we need to work
// creating the server architecture
$errorMessage = "";
try {
// creating the [DAO] layer
$dao = new ServerDao($config["databaseFilename"]);
// Create the [business] layer
$business = new ServerBusiness($dao);
} catch (TaxException $ex) {
// log the error
$errorMessage = utf8_encode($ex->getMessage());
}
// error?
if ($errorMessage) {
// send a 500 error code to the client
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["response" => ["error" => $errorMessage]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// calculate tax
$result = $businessLogic->calculateTax($married, (int) $children, (int) $salary);
// return the response
$response->setContent(json_encode(["response" => $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 the 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");
// path to the configuration file
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include dependencies required by the script
$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();
// retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// create 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 the 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");
// path to the configuration file
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the dependencies required by the script
$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 $business;
public function __construct() {
parent::__construct();
// retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// create the [dao] layer
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// Create the [business] layer
$this->business = new ServerBusiness($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;
// Calculate a taxpayer's taxes
public function calculateTax(string $married, int $children, int $salary): array;
// save 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;
// constructor
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// tax calculation
public function calculateTax(string $married, int $children, int $salary): array {
// create an HTTP client
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// Send the request to the server
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"married" => $married,
"children" => $children,
"salary" => $salary
]]);
// retrieve the response
$json = $response->getContent(false);
$array = \json_decode($json, true);
$response = $array["response"];
// logs
// print "$json=json\n";
// retrieve the response status
$statusCode = $response->getStatusCode();
// Error?
if ($statusCode !== 200) {
// there is an error - throw an exception
$response = ["HTTP status" => $statusCode] + $response;
$message = \json_encode($response, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// return the response
return $response;
}
}
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 BusinessClientInterface {
// 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;
}
- 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;
// constructor
public function __construct(InterfaceClientDao $clientDao) {
// store the reference in the [dao] layer
$this->clientDao = $clientDao;
}
// calculate tax
public function calculateTax(string $married, int $children, int $salary): array {
return $this->clientDao->calculateTax($married, $children, $salary);
}
// Calculate taxes in batch mode
public function executeBatchTaxes(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// allow exceptions from the [DAO] layer to be propagated
// retrieve taxpayer data
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// results array
$results = [];
// process them
foreach ($taxPayersData as $taxPayerData) {
// calculate the tax
$result = $this->calculateTax(
$taxPayerData->isMarried(),
$taxPayerData->getChildren(),
$taxPayerData->getSalary());
// populate [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// put the result into the results array
$results[] = $taxPayerData;
}
// save the 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/DaoProcess.php",
"Dao/ClientDao.php",
"Business/BusinessClientInterface.php",
"Business/BusinessClient.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 the declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// PHP error handling
//ini_set("display_errors", "0");
//
// path to the configuration file
define("CONFIG_FILENAME", "../Data/config-client.json");
// retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include dependencies required by the script
$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"]);
// Create the [business] layer
$businessClient = new BusinessClient($daoClient);
// Calculate taxes in batch mode
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// display the error
print "The following error occurred: " . $ex->getMessage() . "\n";
}
// end
print "Done\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".
Done
Now let’s start only the Apache server and not the MySQL DBMS:

The results in the client console are as follows:
The following error occurred: {"HTTP status":500,"error":"SQLSTATE[HY000] [2002] No connection could be established because the target computer explicitly refused it.\r\n"}
Done
Now, let’s start MySQL and then modify the user who connects in [config-client]:
The results in the client console are as follows:
The following error occurred: {"HTTP status":401,"error":"Authentication failed [x, x]"}
Done
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 the 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");
// path to the configuration file
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include dependencies required by the script
$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 BusinessClientTest extends \Codeception\Test\Unit {
// business layer
private $business;
public function __construct() {
parent::__construct();
// retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// create the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// Create the [business] layer
$this->business = new ClientBusiness($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:
