Skip to content

20. Ejercicio de aplicación – version 10

La version anterior mostró que los datos fiscales, compartidos por todos los usuarios de la aplicación, deberían almacenarse en una memoria de ámbito [Application]. Vamos a utilizar un servidor Redis [https://redis.io] para implementarla.

20.1. Redis

La memoria de ámbito [Application] se implementará mediante un servidor Redis. Los scripts PHP que necesiten esta memoria de aplicación serán clients de este servidor:

Image

20.2. Instalación de Redis

Laragon incluye un servidor Redis que no está activado por defecto. Por lo tanto, hay que empezar por activarlo:

Image

  • en [3], active el servidor [Redis];
  • en [4], dejar el puerto [6379] que Redis utiliza por defecto;

Los servicios de Laragon se reinician automáticamente tras la activación de Redis:

Image

20.3. El cliente Redis en modo comando

El servidor Redis puede consultarse en modo comando. Abrimos un terminal Laragon (véase el enlace del párrafo):

Image

  • en [1], el comando [redis-cli] inicia el cliente en modo de comando del servidor Redis;

En julio de 2019, el cliente Redis puede utilizar 172 comandos para interactuar con el servidor [https://redis.io/commands#list]. Uno de ellos, [command count] [2], muestra este número [3].

Solo vamos a presentar aquellas que vamos a necesitar en nuestra aplicación PHP. Vamos a utilizar Redis para una única cosa: almacenar una matriz [‘attribut’=>’valeur’] en la memoria de Redis. Esto se hace con el comando Redis [set attribut valeur] [4]. A continuación, el valor se puede recuperar con el comando [get attribut] [5]. Eso es todo lo que necesitaremos.

Puede ser necesario vaciar la memoria de Redis. Esto se hace con el comando [flushdb] [6]. A continuación, si solicitamos el valor del atributo [titre] [7], obtenemos una referencia [nil] [8] que indica que no se ha encontrado el atributo. También se puede utilizar el comando [exists] [9-10] para comprobar la existencia de un atributo.

Para salir del cliente Redis, escriba el comando [quit] [11].

20.4. Instalación de un cliente Redis para PHP

Ahora debemos instalar un cliente Redis para PHP:

Image

Existen varias bibliotecas que implementan un cliente Redis. Utilizaremos la biblioteca [Predis] [https://github.com/nrk/predis] (julio de 2019). Esta, al igual que las anteriores, se instala con [composer] en un terminal Laragon:

Image

20.5. Código del servidor

Image

El archivo de configuración [config-server.json] evoluciona de la siguiente manera:


{
    "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"
}

Comentarios

  • líneas 5-15: el archivo version 10 no aporta nada nuevo respecto al script [impots-server.php]. Utiliza elementos de las versiones 08 y 09;
  • línea 19: una dependencia necesaria para la biblioteca [predis] que acabamos de instalar;

El código del servidor [impots-server.php] evoluciona de la siguiente manera:


<?php

// Cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);

// espacio de nombres
namespace Application;

// gestión de errores mediante PHP
ini_set("display_errors", "0");
//
// ruta del archivo de configuración
define("CONFIG_FILENAME", "Data/config-server.json");
// alias de clase
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

// sesión
$session = new Session();
$session->start();


// primer registro
$logger->write("\n---nouvelle requête\n");

// se recupera la solicitud actual
$request = Request::createFromGlobals();
// autenticación solo la primera vez
if (!$session->has("user")) {

} else {
  // registro
  $logger->write("Authentification prise en session…\n");
}

// tenemos un usuario válido: se comprueban los parámetros recibidos
$erreurs = [];
// deben existir tres parámetros GET
$method = strtolower($request->getMethod());


// ¿Errores?
if ($erreurs) {
// se envía un código de error 400 HTTP_BAD_REQUEST al cliente
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // finalizado
  exit;
} else {
  // registros
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}

// Tenemos todo lo necesario para trabajar
// Redis
\Predis\Autoloader::register();
try {
  // cliente [predis]
  $redis = new \Predis\Client();
  // nos conectamos al servidor para ver si está ahí
  $redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
  // servidor interno error
  doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
  // finalizado
  exit;
}

// creación de la capa [dao]
if (!$redis->get("taxAdminData")) {
  // los datos fiscales se extraen de la base de datos
  $logger->write("données fiscales prises en base de données\n");
  try {
    // construcción de la capa [dao]
    $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
    // se introducen los datos fiscales en la memoria de ámbito [application]
    // se llamará implícitamente al método [TaxAdminData]->__toString
    $redis->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // se observa el error
    doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
    // finalizado
    exit;
  }
} else {
  // los datos fiscales se toman de la memoria de ámbito [application]
  $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
  $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
  // instanciación de la capa [dao]
  $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
  // registros
  $logger->write("données fiscales prises dans redis\n");
}
// creación de la capa [métier]
$métier = new ServerMetier($dao);
// cálculo del impuesto
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// se devuelve la respuesta
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// fin
exit;

function doInternalServerError(string $message, Response $response, array $infos,
  Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $message: mensaje de error
  // $response: respuesta HTTP
  // $infos: tabla de información para el envío del correo electrónico
  // $result: tabla de resultados
  // $logger: el registrador de la aplicación
  // $predisClient: un cliente [predis]
  //
  //: se envía un correo electrónico al administrador
  // SendAdminMail intercepta todas las excepciones y las registra por sí mismo
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // se envía un código de error 500 al cliente
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}

// función de envío de la respuesta HTTP al cliente
function sendResponse(Response $response, array $result, int $statusCode,
  array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $response: respuesta HTTP
  // $result: tabla de resultados
  // $statusCode: estado HTTP de la respuesta
  // $headers: encabezados HTTP que deben incluirse en la respuesta
  // $logger: el registrador de la aplicación
  // $predisClient: un cliente [predis]
  //
  // estado HTTTP
  $response->setStatusCode($statusCode);
  // cuerpo
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // encabezados
  $response->headers->add($headers);
  // envío
  $response->send();
  // registro
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
  // cierre de la conexión [redis]
  if ($predisClient != NULL) {
    $predisClient->disconnect();
  }
}

Comentarios

  • línea 15: se asigna el alias [ServerDaoWithRedis] a la clase [\Application\ServerDaoWithSession] para reflejar el cambio en la implementación del script del servidor;
  • líneas 18-19: se conserva la sesión. Aquí hay dos datos que debemos recordar:
    • el hecho de que el usuario se haya autenticado correctamente. Esta información tiene un alcance [session]: está vinculada a un usuario concreto y no es válida para los demás usuarios;
    • los datos de la administración tributaria. Esta información tiene un alcance [application]: no está vinculada a un usuario concreto, pero es válida para todos los usuarios;
  • líneas 54-64: creación del cliente [redis] que se comunicará con el servidor [redis]. Este cliente se comunicará con el puerto predeterminado del servidor. Si este no se comunicara en su puerto predeterminado o si no estuviera en la máquina [localhost], habría que pasar esta información al constructor de la clase [\Predis\Client];
  • línea 59: conectamos inmediatamente el cliente al servidor para comprobar si este responde;
  • líneas 60-65: si falla la conexión con el servidor Redis, se envía una respuesta de error al cliente y se envía un correo electrónico al administrador de la aplicación;
  • línea 67: se solicita al servidor [redis] la clave [taxAdminData]. Si no se encuentra, los datos fiscales se obtienen de la base de datos (línea 72);
  • línea 75: la clave [taxAdminData] se coloca en la memoria [redis] asociada a la cadena jSON de la variable [$taxAdminData], que es un objeto de tipo [TaxAdminData]. El método [$redis→set] espera una cadena de caracteres como valor de la clave. Por lo tanto, intentará transformar el objeto de tipo [TaxAdminData] en tipo [string]. Entonces, implícitamente, se llamará al método [TaxAdminData->__toString]. Este produce la cadena jSON del objeto [TaxAdminData];
  • línea 84: la clave [taxAdminData] se encuentra en la memoria [redis], por lo que se recupera su valor. Sabemos que se trata de la cadena jSON de un objeto [TaxAdminData]. A continuación, se decodifica esta cadena para obtener una matriz de atributos;
  • línea 85: a partir de esta matriz, se instancia un nuevo objeto [TaxAdminData];
  • línea 87: se instancia la capa [dao];

20.6. Código del cliente

Image

El version 10 del cliente es idéntico al version 9. Solo cambia el archivo de configuración [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"
}

Solo cambia, en la línea 24, el URL del servidor.

Los resultados son los mismos que en el version 09. Probemos simplemente un nuevo caso de error:

Image

El resultado en la consola es el siguiente:


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. Pruebas [Codeception] del cliente

Image

La clase de prueba [ClientMetierTest] de la version 10 es idéntica a la de la version 09, con una excepción:


<?php

// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);

// espacio de nombres
namespace Application;

// definición de constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");



}
  • línea 10: el entorno de prueba es el del cliente de version 10;

Antes de comenzar las pruebas, eliminemos con el cliente [redis-cli] la clave [taxAdminData] de la memoria del servidor [redis]:

Image

Ahora, ejecutemos la prueba:

Image

Ahora examinemos los registros [logs.txt] del servidor:


05/07/19 08:52:16:396 :
---nueva consulta
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 :
---nueva consulta
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 :
---nueva consulta
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 :
---nueva consulta

Ya se ha mencionado que, en cada prueba, se vuelve a ejecutar el constructor de la clase de prueba, lo que hace que la clase [ClientDao] sometida a prueba se instancie en cada prueba con una cookie de sesión inexistente. Por lo tanto, todo ocurre como si las 11 pruebas representaran a 11 usuarios diferentes, con 11 sesiones diferentes.

  • línea 6: los datos fiscales se obtienen de la base de datos;
  • líneas 13 y 20: los datos fiscales se obtienen de la memoria [redis]. Por lo tanto, se trata de una memoria de ámbito [application] compartida por todos los usuarios de la aplicación;

20.8. Interfaz web del servidor [Redis]

Hemos visto que el servidor [Redis] se puede gestionar en modo de comandos. También se puede gestionar a través de una interfaz web:

Image

  • en [4], el URL de administración;
  • en [5], las claves almacenadas por el servidor;
  • en [6], el estado actual del servidor;

Al hacer clic en [5], se obtiene información sobre la clave [taxAdminData]:

Image

  • en [7], el URL que da acceso a la información de la clave [taxAdminData] [8];
  • en [9], el estado de la clave;
  • en [10], su valor: se reconoce la cadena jSON de un objeto de tipo [TaxAdminData];
  • en [11], se puede eliminar la clave;
  • en [12], se puede añadir otra;