Skip to content

20. Application Exercise – Version 10

The previous version showed that tax data, shared by all users of the application, should be stored in an [Application]-scope cache. We will use a Redis server [https://redis.io] to implement this.

20.1. Redis

The [Application] scope memory will be implemented by a Redis server. The PHP scripts that need this application memory will be clients of this server:

Image

20.2. Installing Redis

Laragon comes with a Redis server that is not enabled by default. You must therefore start by enabling it:

Image

  • In [3], enable the [Redis] server;
  • In [4], leave port [6379] as the default used by Redis clients;

Laragon services are automatically restarted after Redis is enabled:

Image

20.3. The Redis client in command mode

The Redis server can be queried in command mode. Open a Laragon terminal (see link section):

Image

  • In [1], the [redis-cli] command launches the client in command mode for the Redis server;

As of July 2019, the Redis client supports 172 commands for interacting with the server [https://redis.io/commands#list]. One of them [command count] [2] displays this number [3].

We will only cover the ones we need for our PHP application. We will use Redis for a single purpose: storing an array [‘attribute’=>’value’] in Redis memory. This is done with the Redis command [set attribute value] [4]. The value can then be retrieved using the [get attribute] command [5]. That’s all we’ll need.

It may be necessary to clear Redis’s memory. This is done with the [flushdb] command [6]. Then, if we query the value of the [title] attribute [7], we get a [nil] reference [8] indicating that the attribute was not found. We can also use the [exists] command [9-10] to check if an attribute exists.

To exit the Redis client, type the [quit] command [11].

20.4. Installing a Redis Client for PHP

We now need to install a Redis client for PHP:

Image

There are several libraries that implement a Redis client. We will use the [Predis] library [https://github.com/nrk/predis] (July 2019). Like the previous ones, this one is installed with [composer] in a Laragon terminal:

Image

20.5. Server code

Image

The configuration file [config-server.json] changes as follows:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-10",
    "databaseFilename": "Data/database.json",
    "relativeDependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Entities/TaxAdminData.php",
        "/../version-08/Entities/Database.php",
        "/../version-08/Dao/InterfaceServerDao.php",
        "/../version-08/Dao/ServerDao.php",
        "/../version-09/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/../version-09/Utilities/Logger.php",
        "/../version-09/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php",
        "C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
    ],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Data/logs.txt"
}

Comments

  • lines 5–15: Version 10 does not introduce anything new apart from the [impots-server.php] script. It uses elements from versions 08 and 09;
  • line 19: a dependency required for the [predis] library that we just installed;

The server code [impots-server.php] changes 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");
// class alias
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
 
// session
$session = new Session();
$session->start();


// 1st log
$logger->write("\n---nouvelle requête\n");
 
// retrieve the current query
$request = Request::createFromGlobals();
// authentication only the 1st time
if (!$session->has("user")) {

} else {
  // log
  $logger->write("Authentification prise en session…\n");
}
 
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());

 
// mistakes?
if ($erreurs) {
// an error code 400 HTTP_BAD_REQUEST is sent to the customer
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // completed
  exit;
} else {
  // logs
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
 
// we've got everything you need to work
// Redis
\Predis\Autoloader::register();
try {
  // customer [predis]
  $redis = new \Predis\Client();
  // connect to the server to see if it's there
  $redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
  // internal server error
  doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
  // completed
  exit;
}
 
// creation of the [dao] layer
if (!$redis->get("taxAdminData")) {
  // tax data is taken from the database
  $logger->write("données fiscales prises en base de données\n");
  try {
    // construction of the [dao] layer
    $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
    // put tax data in scope memory [application]
    // method [TaxAdminData]->__toString will be called implicitly
    $redis->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // we note the error
    doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
    // completed
    exit;
  }
} else {
  // tax data are taken from the [application] scope memory
  $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
  $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
  // istanciation of the [dao] layer
  $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
  // logs
  $logger->write("données fiscales prises dans redis\n");
}
// creation of the [business] layer
$métier = new ServerMetier($dao);
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// end
exit;
 
function doInternalServerError(string $message, Response $response, array $infos,
  Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $message: error message
  // $response : answer HTTP
  // $infos: information table for sending mail
  // $result: results table
  // $logger: application logger
  // $predisClient: a customer [predis]
  //
  // send an e-mail to the administrator
  // SendAdminMail intercepts all exceptions and logs them itself
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // an error code 500 is sent to the customer
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}

// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode,
  array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $response : answer HTTP
  // $result: results table
  // $statusCode: HTTP response status
  // $headers: HTTP headers to be included in the response
  // $logger: application logger
  // $predisClient: a customer [predis]
  //
  // status HTTTP
  $response->setStatusCode($statusCode);
  // body
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // headers
  $response->headers->add($headers);
  // shipping
  $response->send();
  // log
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
  // close connection [redis]
  if ($predisClient != NULL) {
    $predisClient->disconnect();
  }
}

Comments

  • line 15: we give the alias [ServerDaoWithRedis] to the class [\Application\ServerDaoWithSession] to reflect the change in the server script’s implementation;
  • lines 18–19: the session is maintained. Here, we need to keep two pieces of information in mind:
    • the fact that the user has authenticated successfully. This information has [session] scope: it is tied to a specific user and is not valid for other users;
    • the tax administration data. This information has [application] scope: it is not linked to a specific user but applies to all users;
  • lines 54–64: creation of the [redis] client that will communicate with the [redis] server. This client will communicate with the server’s default port. If the server were not communicating on its default port or if it were not on the [localhost] machine, this information would need to be passed to the constructor of the [\Predis\Client] class;
  • line 59: the client is immediately connected to the server to check if it responds;
  • lines 60–65: if the connection to the Redis server fails, an error response is sent to the client and an email is sent to the application administrator;
  • line 67: we query the [redis] server for the key [taxAdminData]. If it is not found, then the tax data is retrieved from the database (line 72);
  • line 75: the key [taxAdminData] is stored in [redis] memory along with the JSON string of the variable [$taxAdminData], which is an object of type [TaxAdminData]. The [$redis→set] method expects a string for the key’s value. It will therefore attempt to convert the [TaxAdminData] object to a [string] type. This implicitly calls the [TaxAdminData->__toString] method, which produces the JSON string of the [TaxAdminData] object;
  • line 84: the key [taxAdminData] is in the [redis] memory, so we retrieve its value. We know this is the JSON string of a [TaxAdminData] object. We then parse this to obtain an array of attributes;
  • line 85: from this array, a new [TaxAdminData] object is instantiated;
  • line 87: the [dao] layer is instantiated;

20.6. Client code

Image

Client version 10 is identical to version 9. The only change is to the configuration file [config-client.json]:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/TaxPayerData.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Utilities/Utilitaires.php",
        "/../version-08/Dao/InterfaceClientDao.php",
        "/../version-08/Dao/TraitDao.php",
        "/../version-09/Dao/ClientDao.php",
        "/../version-08/Métier/InterfaceClientMetier.php",
        "/../version-08/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-10/impots-server.php"
}

The only change is the server URL on line 24.

The results are the same as in version 09. Let’s just test a new error case:

Image

The result in the console is as follows:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"[redis], Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée. [tcp:\/\/127.0.0.1:6379]"}
Terminé

20.7. [Codeception] client tests

Image

The [ClientMetierTest] test class in version 10 is identical to that in version 09 with one exception:


<?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-10");


 
}
  • line 10: the test environment is that of the version 10 client;

Before starting the tests, let’s use the [redis-cli] client to delete the [taxAdminData] key from the [redis] server’s memory:

Image

Now, let’s run the test:

Image

Now let’s examine the server logs [logs.txt]:


05/07/19 08:52:16:396 :
---nouvelle requête
05/07/19 08:52:16:403 : Autentification en cours…
05/07/19 08:52:16:403 : Authentification réussie [admin, admin]
05/07/19 08:52:16:403 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
05/07/19 08:52:16:407 : données fiscales prises en base de données
05/07/19 08:52:16:420 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:546 :
---nouvelle requête
05/07/19 08:52:16:555 : Autentification en cours…
05/07/19 08:52:16:555 : Authentification réussie [admin, admin]
05/07/19 08:52:16:556 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
05/07/19 08:52:16:559 : données fiscales prises dans redis
05/07/19 08:52:16:559 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
05/07/19 08:52:16:668 :
---nouvelle requête
05/07/19 08:52:16:675 : Autentification en cours…
05/07/19 08:52:16:675 : Authentification réussie [admin, admin]
05/07/19 08:52:16:675 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
05/07/19 08:52:16:678 : données fiscales prises dans redis
05/07/19 08:52:16:678 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:776 :
---nouvelle requête

We have already mentioned that for each test, the test class constructor is re-executed, which means that the [ClientDao] class being tested is instantiated with a non-existent session cookie for each test. Everything therefore proceeds as if the 11 tests represented 11 different users, with 11 different sessions.

  • line 6: tax data is retrieved from the database;
  • lines 13, 20: tax data is retrieved from the [Redis] memory. We therefore have an [application] scope memory shared by all users of the application;

20.8. [Redis] Server Web Interface

We have seen that the [Redis] server can be managed in command mode. It can also be managed via a web interface:

Image

  • in [4], the administration URL;
  • in [5], the keys stored by the server;
  • in [6], the current server status;

By clicking on [5], you can view information about the [taxAdminData] key:

Image

  • in [7], the URL that provides access to the information for the [taxAdminData] key [8];
  • in [9], the key’s status;
  • in [10], its value: you can recognize the JSON string of a [TaxAdminData] object;
  • In [11], you can delete the key;
  • in [12], you can add another one;