Skip to content

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:

Image

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

Image

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

Image

This architecture will be implemented by the following scripts:

Image

18.2.1. Entities exchanged between layers

Image

The entities exchanged between layers are those from version 5 described in the linked section.

18.2.2. The [dao] layer

Image

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

Image

Image

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

Image

Image

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

Image

  • 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;

Image

  • in [6], we continue;

Image

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

Image

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

Image

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

Image

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:

Image

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

18.2.5.1. Tests for the [dao] layer

Image

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:

Image

18.2.5.2. Tests of the [business] layer

Image

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:

Image

18.3. The client

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

Image

This architecture will be implemented by the following scripts:

Image

18.3.1. Entities exchanged between layers

Image

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

Image

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

Image

Image

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

Image

Image

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:

Image

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

1
2
3
4
    "user": {
        "login": "x",
        "passwd": "x"
},

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.

Image

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:

Image