Skip to content

19. Ejercicio práctico – versión 9

En esta versión, vamos a mejorar el servidor de la siguiente manera:

  • Actualmente, en cada solicitud se buscan los datos de la administración tributaria en la base de datos. Vamos a utilizar una sesión:
    • en la primera solicitud de un usuario, los datos de la administración tributaria se buscan en la base de datos y se almacenan en la sesión;
    • en las siguientes solicitudes del mismo usuario, los datos de la administración tributaria se recuperan de la sesión. Se espera una ligera reducción en el tiempo de ejecución, ya que las consultas a la base de datos son costosas;
  • el servidor registrará en un archivo de texto los momentos importantes:
    • la autenticación exitosa o fallida;
    • la validez o no de los parámetros enviados por el cliente;
    • el resultado del cálculo del impuesto;
    • los distintos casos de error;
  • en caso de un error grave, se enviará un correo electrónico al administrador de la aplicación;

El cliente también deberá modificarse para gestionar la cookie de sesión que se le enviará.

19.1. El servidor

Nos enfocamos en la parte del servidor de la aplicación.

Image

Esta arquitectura se implementará mediante los siguientes scripts:

Image

19.1.1. Utilidades

Image

19.1.1.1. La clase [Logger]

La clase [Logger] se utilizará para escribir registros en un archivo de texto:


<?php

namespace Application;

class Logger {
  // atributo
  private $resource;

  // constructor
  public function __construct(string $logsFilename) {
    // apertura del archivo
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  // escritura de un mensaje en los registros
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }

  // cierre del archivo de registros
  public function close() {
    fclose($this->resource);
  }

}

Comentarios

  • línea 7: el recurso del archivo de registros;
  • línea 10: el constructor de la clase recibe como parámetro el nombre del archivo de registros;
  • línea 12: se abre el archivo de texto en modo de adición (a+): el archivo se abrirá y se conservará su contenido. Las escrituras se realizarán al final del contenido actual;
  • líneas 13-15: si no se pudo abrir el archivo, se lanza una excepción;
  • líneas 19-21: el método [write] permite escribir el mensaje [$message] en el archivo de registro, precedido de la fecha y la hora;
  • líneas 24-16: el método [close] permite cerrar el archivo de registro;

Nota: la aplicación de servidor puede atender a varios clientes al mismo tiempo. Sin embargo, solo hay un archivo de registro para todos. Por lo tanto, existe el riesgo de que haya accesos simultáneos para escribir en el archivo. Por eso, sería necesario sincronizar las escrituras para evitar que se mezclen. Para ello, PHP cuenta con los semáforos [https://www.php.net/manual/fr/book.sem.php]. Aquí ignoraremos la sincronización de las escrituras, pero hay que tener presente el problema.

19.1.1.2. La clase [SendAdminMail]

La clase [SendAdminMail] permite enviar un correo electrónico al administrador de la aplicación en caso de que esta se bloquee:


<?php

namespace Application;

class SendAdminMail {
  // atributos
  private $config;
  private $logger;

  // constructor
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }

  public function send() {
    // envía $this->config['message'] al servidor SMTP $this->config['smtp-server'] en el puerto $infos[smt-port]
    // si $this->config['tls'] es verdadero, se utilizará el soporte TLS
    // el correo se envía en nombre de $this->config['from']
    // para el destinatario $this->config['to']
    // el mensaje tiene como asunto $this->config['subject']
    // se adjuntan al correo los archivos adjuntos de $this->config['attachments']
    // el resultado del método
    try {
      // creación del mensaje
      $message = (new \Swift_Message())
        // asunto del mensaje
        ->setSubject($this->config["subject"])
        // remitente
        ->setFrom($this->config["from"])
        // destinatarios con un diccionario (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // texto del mensaje
        ->setBody($this->config["message"])
      ;
      // archivos adjuntos
      foreach ($this->config["attachments"] as $attachment) {
        // ruta del archivo adjunto
        $fileName = __DIR__ . $attachment;
        // se verifica que el archivo exista
        if (file_exists($fileName)) {
          // se adjunta el documento al mensaje
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // error
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // ¿Protocolo TLS?
      if ($this->config["tls"] === "TRUE") {
        // TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
          ->setUsername($this->config["user"])
          ->setPassword($this->config["password"]);
      } else {
        // no hay TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // el administrador del envío
      $mailer = new \Swift_Mailer($transport);
      // envío del mensaje
      $mailer->send($message);
      // fin
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // error
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }

}

Comentarios

  • línea 11: el constructor recibe dos parámetros:
    • [$config]: un array asociativo que contiene toda la información necesaria para enviar el correo electrónico;
    • [$logger]: un registrador que permite registrar los momentos importantes del envío del correo electrónico;

El array asociativo tendrá la siguiente forma:

1
2
3
4
5
6
7
8
9
{
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
}
  • líneas 16-76: el método [send] permite enviar el correo electrónico. Este código se presentó y describió en el párrafo enlace;

19.1.2. La capa [dao]

Image

El script [ServeurDaoWithSession.php] es el siguiente:


<?php

// espacio de nombres
namespace Application;

// definición de una clase ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {

  // constructor
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // el caso más sencillo
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // se pasa el control a la clase padre
      parent::__construct($databaseFilename);
    }
  }

}

Comentarios

  • línea 7: la clase [ServerDaoWithSession] de la versión 09 extiende la clase [ServerDao] de la versión 08. De hecho, la clase [ServerDao] sabe cómo utilizar la base de datos. Ahora solo nos queda considerar el caso en el que los datos de la administración tributaria ya se hayan obtenido:
  • línea 10: el constructor ahora recibe dos parámetros:
    • [string $databaseFilename]: nombre del archivo que contiene la información necesaria para conectarse a la base de datos si los datos de la administración tributaria aún no se han obtenido; de lo contrario, NULL;
    • [TaxAdminData $taxAdminData]: los datos de la administración tributaria si ya se han obtenido; de lo contrario, NULL;

Al iniciar una sesión web, se creará la capa [dao] con un objeto [$databaseFilename] que no sea NULL y un objeto [taxAdminData] NULL. A continuación, se buscarán en la base de datos los datos de la administración tributaria y se almacenarán en la sesión. En las consultas posteriores de la misma sesión, la capa [dao] se construirá con un objeto [databaseFilename], NULL y un objeto [taxAdminData] provenientes de la sesión, y no con NULL. Por lo tanto, no habrá búsqueda en la base de datos.

19.1.3. El script del servidor

El script del servidor [impots-server.php] se configura mediante el siguiente archivo jSON [config-server.json]:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
    "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",
        "/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/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"
}

El script 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");

// se recupera la configuración
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// se incluyen las dependencias necesarias para el script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// dependencias absolutas (bibliotecas de terceros)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// dependencias de Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

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

// preparación de la respuesta JSON del servidor
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// creación del archivo de registros
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // error interno del servidor
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // finalizado
  exit;
}

// 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")) {
  // registro
  $logger->write("Autentification en cours…\n");
  // autenticación

  }
  // ¿Se encontró al usuario?
  if (!$trouvé) {
    // no encontrado - código 401 HTTP_UNAUTHORIZED
    sendResponse(
      $response,
      ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
      Response::HTTP_UNAUTHORIZED,
      ["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
      $logger
    );
    // finalizado
    exit;
  } else {
    // se registra en la sesión que se ha autenticado al usuario
    $session->set("user", TRUE);
    // registro
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // registro
  $logger->write("Authentification prise en session…\n");
}
// tenemos un usuario válido; verificamos los parámetros recibidos
$erreurs = [];
// debemos tener tres parámetros GET


// ¿Hay 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
// creación de la capa [dao]
if (!$session->has("taxAdminData")) {
  // los datos 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 ServerDaoWithSession($config["databaseFilename"], NULL);
    // se cargan los datos en la sesión
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // se registra el error
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // finalizado
    exit;
  }
} else {
  // los datos se toman de la sesión
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // registros
  $logger->write("données fiscales prises en session\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);
// fin
exit;

function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // 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);
}

// función para enviar la respuesta HTTP al cliente
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $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
  //
  // 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();
  }
}

Comentarios

  • líneas 34-35: se inicia una sesión;
  • líneas 38-40: se prepara una respuesta jSON;
  • líneas 42-50: se intenta crear el archivo de registros. Si se produce una excepción, se llama al método [doInternalServer] (líneas 132-140);
  • línea 132: el método [doInternalServer] acepta cuatro parámetros:
    • [$message]: el mensaje que se va a registrar. Debe estar codificado en UTF-8;
    • [$response]: el objeto [Response] que encapsula la respuesta del servidor a su cliente;
    • [$logger]: el objeto [Logger] que permite generar los registros;
    • [$infos]: la información que permite enviar un correo electrónico al administrador de la aplicación;
  • líneas 135-137: se envía un correo electrónico al administrador de la aplicación;
  • línea 139: se envía la respuesta al cliente:
    • $response: respuesta HTTP;
    • $result: el servidor envía la cadena jSON de la tabla [‘réponse’=>["erreur" => $message]];
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], código 500;
    • $headers: [], no hay encabezados HTTP que agregar a la respuesta;
    • $logger: el registrador de la aplicación;
  • línea 58: gracias a la sesión establecida, solo se autenticará al cliente una vez:
    • una vez que el cliente se haya autenticado, se colocará una clave [user] en la sesión (línea 78);
    • en la siguiente solicitud del mismo cliente, la línea 58 evita una autenticación que ya no es necesaria;
  • línea 103: gracias a la sesión establecida, solo se buscarán los datos en la base de datos una sola vez:
    • en la primera solicitud, se realizará la búsqueda en la base de datos (línea 108). Los datos recuperados se almacenan luego en la sesión (línea 110) asociados a la clave [taxAdminData];
    • en las consultas siguientes, la clave [taxAdminData] se encontrará en la sesión (línea 103) y, entonces, los datos del disco se comunicarán directamente a la capa [dao] (línea 119);
  • líneas 111-116: la búsqueda de datos fiscales en la base de datos puede fallar. En ese caso, se envía al cliente un código [500 Internal Server Error];
  • línea 113: el mensaje de error de la excepción del controlador MySQL está codificado como ISO 8859-1. Se convierte a UTF-8 para que se registre correctamente;
  • el resto del código es prácticamente idéntico al de la versión anterior;
  • líneas 143-164: la función [sendResponse] envía todas las respuestas al cliente;
  • líneas 144-148: significado de los parámetros;
  • línea 153: la respuesta es siempre la cadena jSON de un array [‘résultat’=>qqChose];
  • línea 156: a veces hay que agregar encabezados HTTP a la respuesta. Este es el caso en la línea 71;
  • línea 158: se envía la respuesta;
  • líneas 160-163: se registra la respuesta y se cierra el registrador;

19.1.4. Pruebas [Codeception]

Image

Solo vamos a probar la capa [dao], que es la única que ha cambiado.

El código de la prueba [ServerDaoTest] es el siguiente:


<?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:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// ruta del archivo de configuración
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");

// se recupera la configuración
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// se incluyen las dependencias necesarias para el script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// dependencias absolutas (bibliotecas de terceros)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// prueba -----------------------------------------------------

class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    // padre
    parent::__construct();
    // se recupera la configuración
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creación de la capa [dao]
    $dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // pruebas
  public function testTaxAdminData() {

  }

}
  • líneas 9-24: se crea un entorno de ejecución idéntico al del script de servidor [impots-server];
  • línea 38: para construir la capa [dao], se instancia la clase [ServerDaoWithSession];

El resultado de las pruebas es el siguiente:

Image

19.2. El cliente

Nos enfocamos en la parte del cliente de la aplicación.

Image

Esta arquitectura se implementará mediante los siguientes scripts:

Image

En la nueva versión, solo cambian:

  • el archivo de configuración [config-client.json];
  • la capa [dao] del cliente;

19.2.1. la capa [dao]

La capa [Dao] evoluciona de la siguiente manera:


<?php

namespace Application;

// dependencias
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // uso de un Trait
  use TraitDao;
  // atributos
  private $urlServer;
  private $user;
  private $sessionCookie;

  // constructor
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }

  // cálculo del impuesto
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // ¿Cookie de sesión?
    if (!$this->sessionCookie) {
      // se crea un cliente HTTP
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // se envía la solicitud al servidor sin cookie de sesión
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // se envía la solicitud al servidor con la cookie de sesión
      // se crea un cliente HTTP
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // se recupera la respuesta
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // registros
    print "$json=json\n";
    // se recupera el estado de la respuesta
    $statusCode = $response->getStatusCode();
    // ¿Error?
    if ($statusCode !== 200) {
      // hay un error: se lanza una excepción
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    if (!$this->sessionCookie) {
      // se recupera la cookie de sesión
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // ¿Cookie de sesión?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // se devuelve la respuesta
    return $réponse;
  }

}

Comentarios

La modificación de la capa [dao] consiste en gestionar ahora una sesión:

  • línea 14: la cookie de la sesión;
  • líneas 25-39: en la primera solicitud, esta cookie no existe; por lo tanto, se realiza la solicitud al servidor enviando la información de autenticación (línea 28);
  • líneas 40-53: en las solicitudes siguientes, normalmente se cuenta con la cookie de sesión. Por lo tanto, no se envía la información de autenticación (líneas 42-44);
  • líneas 69-82: la respuesta del servidor a la primera solicitud incluirá una cookie de sesión. La recuperamos. Este código ya se ha utilizado y explicado en el párrafo del enlace;
  • línea 78: la cookie de sesión recuperada se almacena en el atributo de clase [$sessionCookie];

Nota: se podría haber conservado la versión anterior de la capa [dao] y realizar la autenticación en cada solicitud, ya que el costo de esta es insignificante. Con fines didácticos, se ha querido recordar cómo un cliente HTTP podía gestionar una sesión.

19.2.2. El archivo de configuración

El archivo de configuración jSON evoluciona de la siguiente manera:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
    "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",
        "/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-09/impots-server.php"
}

Solo cambia el URL de la línea 24.

19.3. Algunas pruebas

19.3.1. Prueba 1

En primer lugar, ejecutamos el cliente en un entorno sin errores. Los resultados siguen siendo los mismos que en las versiones anteriores. Pero ahora, en el lado del servidor, tenemos un archivo de registro [logs.txt]:


04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête

  • líneas 3-7: en la primera solicitud, se realiza la autenticación y la búsqueda de datos en la base;
  • líneas 9-14: en la siguiente solicitud, ya no hay autenticación y los datos se toman de la sesión. Esto se repite en las solicitudes siguientes (líneas 15 y siguientes);

19.3.2. Prueba 2

Ahora desactivemos la base de datos MySQL. Del lado del cliente, obtenemos el siguiente resultado en la consola:


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

Del lado del servidor, tenemos los siguientes registros de [logs.txt]:


04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête

Para obtener el correo electrónico recibido por el administrador de la aplicación, utilizamos el script [imap-03.php] del párrafo vinculado al siguiente archivo de configuración [config-imap-01.json]:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Se obtiene el siguiente resultado:

Image

El archivo [message_1.txt] contiene el siguiente texto:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.

19.3.3. Prueba 3

Ahora vamos a asegurarnos de que no se pueda crear el archivo [logs.txt]. Para ello, basta con crear una carpeta llamada [logs.txt]:

Image

Una vez hecho esto, ejecutemos el cliente.

Del lado del cliente, obtenemos los siguientes resultados en la consola:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé

Del lado del servidor, no hay registros, pero el administrador recibe el siguiente correo electrónico:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

Echec lors de la création du fichier de logs [Data/logs.txt]

19.3.4. Prueba 4

En esta ocasión, vamos a proporcionar, en el archivo de configuración del cliente, datos de identificación incorrectos para el cliente que se conecta.

El cliente muestra los siguientes resultados en la consola:


L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé

En el servidor, aparecen los siguientes registros:


---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}

19.3.5. Prueba 5

Volvamos a poner el usuario correcto, [admin, admin], en el archivo de configuración del cliente.

Ahora solicitemos el URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] del servidor directamente en un navegador sin pasar parámetros:

En el archivo de registros [logs.txt] del servidor, aparecen las siguientes líneas:


---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}

19.4. Pruebas [Codeception]

Al igual que se hizo con las versiones anteriores, vamos a escribir pruebas [Codeception] para la versión 09.

Image

19.4.0.1. Prueba de la capa [métier]

La prueba [ClientMetierTest.php] es la siguiente:


<?php

// Se respetan estrictamente 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-09");

// ruta del archivo de configuración
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");

// se recupera la configuración
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// se incluyen las dependencias necesarias para el script
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// dependencias absolutas (bibliotecas de terceros)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// utiliza
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;

// clase de prueba
class ClientMetierTest extends Unit {
  
}

Comentarios

  • en comparación con la clase de prueba de la versión 08, solo cambia la línea 10, que especifica la carpeta raíz del cliente que se va a probar;

Los resultados de la prueba son los siguientes:

Image

Es interesante revisar los registros del servidor [logs.txt]:


04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête

---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}

Se observa que los datos de la administración tributaria siempre se obtienen de la base de datos y nunca de la sesión. Volvamos al código de la prueba ejecutada:


<?php

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

// espacio de nombres
namespace Application;



// clase de prueba
class ClientMetierTest extends Unit {
  // capa de negocio
  private $métier;

  public function __construct() {
    parent::__construct();
    // se recupera la configuración
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creación de la capa [dao]
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // creación de la capa [métier]
    $this->métier = new ClientMetier($clientDao);
  }

  // pruebas
  public function test1() {

  }

  public function test2() {

  }

  public function test3() {

  }



}

En una clase de prueba [Codeception], el constructor se ejecuta para cada prueba.

  • línea 21: por lo tanto, se crea un nuevo [ClientDao] para cada prueba con una cookie de sesión NULL. Esto explica por qué este cliente no se beneficia de ninguna sesión;

Este ejemplo nos muestra que la sesión no es el lugar adecuado para almacenar los datos de la administración tributaria. De hecho, estos datos son comunes a todos los usuarios de la aplicación. Sin embargo, aquí se duplican en cada una de sus sesiones.

En programación web, se distinguen tres tipos de visibilidad para los datos compartidos:

  • datos compartidos por todos los usuarios de la aplicación web. Por lo general, son datos de solo lectura. PHP no cuenta de forma nativa con esta memoria;
  • datos compartidos por las solicitudes de un mismo cliente. Estos datos se almacenan en la sesión. En este caso, se habla de «sesión de cliente» para referirse a la memoria del cliente. Todas las solicitudes de un cliente tienen acceso a esta sesión. Pueden almacenar y leer información en ella. En los scripts anteriores, esta sesión está implementada por el objeto Symfony [HttpFoundation\Session\Session];
  • la memoria de solicitud, o contexto de solicitud. La solicitud de un usuario puede ser procesada por varias acciones sucesivas. El contexto de la solicitud permite que una acción 1 transmita información a una acción 2. En los scripts anteriores, la solicitud está implementada por el objeto Symfony [HttpFoundation\Request] y su memoria por el atributo [HttpFoundation\Request::attributes];

Image

Existen bibliotecas de terceros para dotar a PHP de una memoria de aplicación. La nueva versión del ejercicio práctico muestra el uso de una de ellas.