Skip to content

18. Ejercicio práctico – versión 8

Retomaremos la aplicación de ejemplo – versión 5 (párrafo del enlace) y la convertiremos en una aplicación cliente/servidor.

18.1. Introduction

La arquitectura de la versión 5 era la siguiente:

Image

  • la capa denominada [dao] (Objetos de Acceso a Datos) se encarga de las comunicaciones con la base de datos MySQL y el sistema de archivos local;
  • la capa denominada [métier] realiza el cálculo del impuesto;
  • el script principal es el coordinador: instancia las capas [dao] y [métier] y luego se comunica con la capa [métier] para realizar las tareas necesarias;

Vamos a migrar esta arquitectura a la siguiente arquitectura cliente/servidor:

Image

  • En [2], tomaremos la capa [dao] de la versión 5 y le eliminaremos los métodos de acceso al sistema de archivos local. Estos métodos se migrarán a la capa [dao] del cliente [6, 7];
  • en [3], la capa [métier] seguirá siendo la de la versión 5 sin sus métodos [executeBatchImpôts, saveResults], que se migrarán a la capa [dao] y [7] del cliente;
  • en [4], se debe escribir el script del servidor: deberá:
    • crear las capas [métier], [dao] y [3, 2];
    • interactuar con el script de cliente [5, 7];
  • en [7], se debe escribir la capa [dao] del cliente:
    • será un cliente HTTP del script del servidor [4, 5];
    • esta adoptará los métodos de acceso al sistema de archivos local de la capa [dao] de la versión 5;
  • en [8], la capa [métier] del cliente respetará la interfaz [InterfaceMetier] de la versión 5. Sin embargo, su implementación será diferente. En la versión 5, la capa [métier] realizaba el cálculo del impuesto. Aquí, es la capa [métier] del servidor la que realiza este cálculo. Por lo tanto, la capa [métier] llamará a las capas [dao] y [7] para comunicarse con el servidor y solicitarle que calcule el impuesto;
  • en [9], el script de consola deberá instanciar las capas [dao, métier] del cliente e iniciar su ejecución;

18.2. El servidor

Nos interesa la parte del servidor de la aplicación.

Image

Esta arquitectura se implementará mediante los siguientes scripts:

Image

18.2.1. Las entidades intercambiadas entre las capas

Image

Las entidades intercambiadas entre las capas son las de la versión 5 descritas en el párrafo del enlace.

18.2.2. La capa [dao]

Image

La capa [dao] implementa la siguiente interfaz [InterfaceServerDao]:


<?php

// espacio de nombres
namespace Application;

interface InterfaceServerDao {

  // lectura de los datos de la administración tributaria
  public function getTaxAdminData(): TaxAdminData;
}
  • línea 9: el método [getTaxAdminData] recupera los datos de la administración tributaria de una base de datos;

La interfaz [InterfaceServerDao] está implementada por la siguiente clase [ServerDao]:


<?php

// espacio de nombres
namespace Application;

// Definición de una clase ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
  // el objeto de tipo TaxAdminData que contiene los datos de los tramos impositivos
  private $taxAdminData;
  // el objeto de tipo [Database] que contiene las características de BD
  private $database;

  // fabricante
  public function __construct(string $databaseFilename) {
    //: se guarda la configuración JSON de la base de datos
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // se prepara el atributo
    $this->taxAdminData = new TaxAdminData();
    try {
      // se abre la conexión a la base de datos
      $connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
      // se desea que, ante cada error de SGBD, se lance una excepción
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // se inicia una transacción
      $connexion->beginTransaction();
      // se completa la tabla de tramos impositivos
      $this->getTranches($connexion);
      // se completa la tabla de constantes
      $this->getConstantes($connexion);
      // se finaliza la transacción con éxito
      $connexion->commit();
    } catch (\PDOException $ex) {
      // ¿Hay alguna transacción en curso?
      if (isset($connexion) && $connexion->inTransaction()) {
        // se termina la transacción con un error
        $connexion->rollBack();
      }
      // se devuelve la excepción al código que la invocó
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // se cierra la conexión
      $connexion = NULL;
    }
  }

  // lectura de los datos de la base de datos
  private function getTranches($connexion): void {

  }

  // lectura de la tabla de constantes
  private function getConstantes($connexion): void {

  }

  // devuelve los datos que permiten calcular el impuesto
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }

}

Este código se presentó en el párrafo «enlace».

18.2.3. La capa [métier]

Image

Image

La capa [métier] implementa la siguiente interfaz [InterfaceServerMetier]:


<?php

// espacio de nombres
namespace Application;

interface InterfaceServerMetier {

  // cálculo de los impuestos de un contribuyente
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}

La interfaz [InterfaceServerMetier] es implementada por la siguiente clase [ServerMetier]:


<?php

// espacio de nombres
namespace Application;

class ServerMetier implements InterfaceServerMetier {
  // capa DAO
  private $dao;
  // datos de la administración tributaria
  private $taxAdminData;

  //---------------------------------------------
  // configurador de la capa [dao]
  public function setDao(InterfaceServerDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceServerDao $dao) {
    // se almacena una referencia en la capa [dao]
    $this->dao = $dao;
    // se recuperan los datos que permiten calcular el impuesto
    // el método [getTaxAdminData] puede generar una excepción ExceptionImpots
    // luego se permite que se transmita al código que la invocó
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// cálculo del impuesto
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // resultado
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

// --------------------------------------------------------------------------
  private function calculerImpot2(string $marié, int $enfants, float $salaire): array {

    // resultado
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=salarioAnual-deducción
  // la deducción tiene un mínimo y un máximo
  private function getRevenuImposable(float $salaire): float {

    // resultado
    return floor($revenuImposable);
  }

// calcula una posible reducción
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // resultado
    return ceil($décôte);
  }

// calcula una posible reducción
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    ..
    // resultado
    return ceil($réduction);
  }
}

Este código ya se ha visto y comentado desde la versión 1 en el párrafo enlace. Su versión orientada a objetos con una base de datos se presentó en el párrafo enlace.

18.2.4. El script del servidor

Image

Image

El script del servidor implementa la capa [web] [4]. El script [impots-server] se configura mediante el siguiente archivo jSON [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",
        "/Métier/InterfaceServerMetier.php",
        "/Métier/ServerMetier.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ]
}
  • línea 1: la carpeta raíz desde la cual se medirán las rutas de los archivos;
  • línea 2: el archivo jSON de configuración de la base de datos MySQL;
  • línea 3: el archivo jSON con los datos de la administración tributaria;
  • líneas 5-14: los archivos de la aplicación;
  • línea 15: la dependencia necesaria de las bibliotecas de terceros, en este caso Symfony;
  • líneas 16-20: la tabla de usuarios autorizados a utilizar la aplicación;

Los archivos jSON y [database.json, taxadmindata.json] corresponden a la versión 5 descrita en el párrafo del enlace.

El script [impots-server] implementa la capa [web] 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";
}

// definición de las constantes
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// dependencias de Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;

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

// se recupera la solicitud actual
$request = Request::createFromGlobals();
// autenticación
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// ¿Existe el usuario?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
  $trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
  $i++;
}
// se establece el código de estado de la respuesta
if (!$trouvé) {
  // no encontrado - código 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
  // mensaje de error
  $response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
  $response->send();
  // fin
  exit;
}
// tenemos un usuario válido: verificamos los parámetros recibidos
$erreurs = [];
// se deben tener tres parámetros GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// ¿Error?
if ($erreur) {
  $erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}

// se recupera el estado civil
if (!$request->query->has("marié")) {
  $erreurs[] = "paramètre marié manquant";
} else {
  $marié = trim(strtolower($request->query->get("marié")));
  $erreur = $marié !== "oui" && $marié !== "non";
  // ¿Error?
  if ($erreur) {
    $erreurs[] = "paramètre marié [$marié] invalide";
  }
}

// se recupera el número de hijos
if (!$request->query->has("enfants")) {
  $erreurs[] = "paramètre enfants manquant";
} else {
  $enfants = trim($request->query->get("enfants"));
  // el número de hijos debe ser un número entero >=0
  $erreur = !preg_match("/^\d+$/", $enfants);
  // ¿Error?
  if ($erreur) {
    $erreurs[] = "paramètre enfants [$enfants] invalide";
  }
}

// se obtiene el salario anual
if (!$request->query->has("salaire")) {
  $erreurs[] = "paramètre salaire manquant";
} else {
  // el salario debe ser un número entero >=0
  $salaire = trim($request->query->get("salaire"));
  $erreur = !preg_match("/^\d+$/", $salaire);
  // ¿Error?
  if ($erreur) {
    $erreurs[] = "paramètre salaire [$salaire] invalide";
  }
}

// ¿Hay otros parámetros en la consulta?
foreach (\array_keys($request->query->all()) as $key) {
  // ¿Parámetro válido?
  if (!\in_array($key, ["marié", "enfants", "salaire"])) {
    $erreurs[] = "paramètre [$key] invalide";}
}

// ¿Hay errores?
if ($erreurs) {
  // se envía un código de error 400 al cliente
  $response->setStatusCode(Response::HTTP_BAD_REQUEST);
  $response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// Tenemos todo lo necesario para trabajar
// creación de la arquitectura del servidor
$msgErreur = "";
try {
  // creación de la capa [dao]
  $dao = new ServerDao($config["databaseFilename"]);
  // creación de la capa [métier]
  $métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// se observa el error
  $msgErreur = utf8_encode($ex->getMessage());
}
// ¿Error?
if ($msgErreur) {
  // se envía un código de error 500 al cliente
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  $response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// cálculo del impuesto
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// se devuelve la respuesta
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();

Comentarios

  • línea 16: se procesa el archivo de configuración;
  • líneas 18-26: se cargan todas las dependencias;
  • línea 29: el nombre del archivo [database.json];
  • líneas 32-33: se declaran las clases de las bibliotecas de terceros que se van a utilizar;
  • líneas 36-38: se prepara una respuesta jSON;
  • líneas 40-52: se verifica que el usuario que realiza la solicitud sea uno de los usuarios autorizados;
  • líneas 54-63: si no es así, se envía el código HTTP 401, que indica un rechazo de acceso. Al recibir este código y el encabezado HTTP [WWW-Authenticate => Basic realm=], la mayoría de los navegadores muestran una ventana de autenticación en la que se invita al usuario a autenticarse;
  • línea 59: la respuesta jSON del servidor explica la causa del error. Todas las respuestas del servidor serán la cadena jSON de una tabla [‘réponse’=>’qq chose’];
  • líneas 64-117: se verifica la validez de la solicitud:
    • una solicitud GET con exactamente tres parámetros;
    • un parámetro [marié] cuyo valor debe ser «sí» o «no»;
    • un parámetro [enfants] cuyo valor debe ser un número entero >=0;
    • un parámetro [salaire] cuyo valor debe ser un número entero >=0;
  • línea 65: cada vez que se detecta un error, se agrega un mensaje de error a la tabla [$erreurs];
  • líneas 120-126: si hay un error, se envía el código HTTP [400 Bad Request] al cliente (línea 122);
  • línea 123: la respuesta jSON del servidor explica la causa del error;
  • a partir de la línea 132, todo ha sido verificado. Se pueden instanciar las capas [dao, métier]. Esta instanciación tiene un costo y solo debe realizarse si se tiene la certeza de que la solicitud es válida;
  • líneas 130-138: se crea la arquitectura del servidor. La construcción de la capa [dao] puede generar una excepción de tipo [ExceptionImpots]. Si se produce esta excepción, se registra el error;
  • líneas 135-138: si se ha producido una excepción, se envía el código HTTP 500 al cliente. Este código significa que el servidor ha fallado;
  • línea 143: la respuesta explica la causa del error;
  • línea 148 : el cálculo del impuesto se delega a la capa [métier];
  • líneas 150-151: envío de la respuesta;

Probemos este script con un navegador. Solicitemos la página segura URL a partir de [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

Image

  • en [1], la URL segura solicitada;
  • en [2], los tres parámetros [marié, enfants, salaire];
  • en [3], el servidor Apache de Laragon envió un certificado autofirmado SSL. El navegador lo detectó y muestra una advertencia de seguridad: considera que el sitio del servidor no es confiable;
  • en [4], seguimos adelante;

Image

  • en [6], seguimos;

Image

  • en [7], el navegador muestra una ventana para que el usuario pueda autenticarse;
  • en [9,10], escribimos [admin] y [admin];

Image

  • en [13], la respuesta jSON del servidor;

Hagamos algunas pruebas de error:

Solicitamos el URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]

Obtenemos el siguiente resultado:

Image

Eliminamos el SGBD y el MySQL y solicitamos el URL y el [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

Image

18.2.5. Pruebas [Codeception]

Cada vez que construyamos una nueva versión del servidor, probaremos las capas [métier] y [dao] tal como se ha hecho desde la versión 04 (véanse los párrafos enlace y enlace).

En primer lugar, asociamos el proyecto [scripts-web] a las pruebas [Codeception]. Para ello, siga el mismo procedimiento que se siguió para el proyecto [scripts-console] en el párrafo enlace. Obtenemos un proyecto [scripts-web] con una carpeta [Test Files]:

Image

Vamos a crear una prueba para la capa [dao] y otra para la capa [métier].

18.2.5.1. Pruebas de la capa [dao]

Image

La prueba [ServerDaoTest] será la 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-08");
// 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 ServerDao(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // pruebas
  public function testTaxAdminData() {

  }

}

Comentarios

  • líneas 9-24: se crea el mismo entorno de trabajo que el del servidor [impots-server.php]. Esto se hace en las líneas 9-12 con la definición de las dos constantes de las que depende el entorno;
  • líneas 32-40: se crea una instancia de la capa [dao] para someterla a prueba, tal como se hacía en el script del servidor [impots-server.php];
  • a partir de ahora nos encontramos en las mismas condiciones que el script del servidor [impots-server.php]: podemos iniciar las pruebas;
  • líneas 43-45: el método [testTaxAdminData] es el descrito en el párrafo del enlace;

Los resultados de la prueba son los siguientes:

Image

18.2.5.2. Pruebas de la capa [métier]

Image

La prueba [ServerMetierTest] será la 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-08");
// 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";
}

// clase de prueba
class ServerMetierTest extends \Codeception\Test\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]
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    // creación de la capa [métier]
    $this->métier = new ServerMetier($dao);
  }

  // pruebas
  public function test1() {

  }

  public function test2() {

  }

  ..

  public function test11() {

  }

}

Comentarios

  • líneas 9-24: se crea el mismo entorno de trabajo que el del servidor [impots-server.php]. Esto se hace en las líneas 9-12 con la definición de las dos constantes de las que depende el entorno;
  • líneas 30-38: se crea una instancia de la capa [métier] para probarla, tal como se hizo en el script del servidor [impots-server.php];
  • a partir de ahora nos encontramos en las mismas condiciones que el script del servidor [impots-server.php]: podemos iniciar las pruebas;
  • líneas 40-53: los métodos [test1, test2…, test11] son los descritos en el párrafo del enlace;

Los resultados de la prueba son los siguientes:

Image

18.3. El cliente

Nos interesa la parte del cliente de la aplicación.

Image

Esta arquitectura se implementará mediante los siguientes scripts:

Image

18.3.1. Las entidades intercambiadas entre capas

Image

Todas las entidades anteriores han sido descritas y ya se han utilizado:

18.3.2. La capa [dao]

Image

La capa [dao] implementa la siguiente interfaz [InterfaceClientDao]:


<?php

// espacio de nombres
namespace Application;

interface InterfaceClientDao {

  // lectura de los datos de los contribuyentes
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // cálculo de los impuestos de un contribuyente
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // registro de resultados
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
  • línea 9: la función [getTaxPayersData] carga en memoria los datos de los contribuyentes del archivo [$taxPayersFilename]. Si hay errores, se registran en el archivo [$errorsFilename];
  • línea 12: la función [calculerImpots] calcula el impuesto de un contribuyente;
  • línea 15: la función [saveResults] guarda en el archivo [$resultsFilename] los datos de la tabla [$taxPayersData], que representan los resultados de varios cálculos de impuestos;

La interfaz [InterfaceClientDao] está implementada por la siguiente clase [ClientDao]:


<?php

namespace Application;

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

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

  // 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 {
    // 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
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
    ]]);
    // se recupera la respuesta
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // registros
    // imprimir "$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);
    }
    // se devuelve la respuesta
    return $réponse;
  }

}

Comentarios

  • línea 10: se inserta [TraitDao] (véase el párrafo del enlace), que implementa los métodos [getTaxPayersData] y [saveResults]. Por lo tanto, solo queda por implementar el método [calculerImpots]. Este se implementa en las líneas 22 a 49;
  • líneas 16-19: el constructor de la clase [ClientDao] recibe dos parámetros:
    • el URL [$urlServer] del servidor de cálculo de impuestos;
    • la tabla [$user] de claves «login» y «passwd» que define al usuario que realiza la solicitud;
  • línea 22: el método [calculerImpots] recibe los tres parámetros que se enviarán al servidor de cálculo de impuestos;
  • líneas 24-27: se crea un cliente HTTP con:
    • línea 25: las credenciales del usuario que realiza la solicitud;
    • línea 26: la opción que hace que el cliente HTTP no verifique la validez del certificado SSL enviado por el servidor;
  • líneas 29-34: se consulta al servidor con los tres parámetros que este espera;
  • línea 36: se recupera la respuesta jSON del servidor. Si no se pasa el parámetro [false] al método [Response::getContent], entonces, si el estado de la respuesta del servidor se encuentra en el intervalo [3xx-5xx] (caso de error), el objeto [Response] lanza una excepción tan pronto como se intenta obtener el contenido de la respuesta [Response::getContent] o sus encabezados HTTP y [Response::getHeaders]. Aquí, independientemente del estado HTTP de la respuesta, queremos poder acceder a su contenido, aunque solo sea para registrarlo en el registro (línea 40);
  • líneas 37-38: la respuesta del servidor es la cadena jSON de una tabla [‘réponse’=>qqChose]. Recuperamos el [qqChose];
  • línea 40: se registra la respuesta jSON en modo de desarrollo;
  • línea 42: se recupera el código de estado de la respuesta;
  • líneas 44-49: si el código de estado HTTP no es 200, significa que nuestro servidor ha tenido un problema. Entonces lanzamos una excepción de tipo [ExceptionImpots] con el mensaje que consiste en la respuesta jSON del servidor más el código HTTP de la respuesta;
  • línea 51: devolvemos el resultado, que es un array asociativo con las claves [impôt, surcôte, décôte, réduction, taux];

18.3.3. La capa [métier]

Image

Image

La capa [métier] [8] implementa la siguiente interfaz [InterfaceClientMetier]:


<?php

// espacio de nombres
namespace Application;

interface InterfaceClientMetier {

  // cálculo de los impuestos de un contribuyente
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // cálculo de impuestos en modo por lotes
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • línea 9: la función [calculerImpots] calcula el impuesto;
  • línea 12: la función [executeBatchImpots] calcula el impuesto de los contribuyentes cuyos datos se encuentran en el archivo [$taxPayersFileName], guarda los resultados obtenidos en el archivo [$resultsFileName] y los errores encontrados en el archivo [$errorsFileName];

La interfaz [InterfaceClientMetier] está implementada por la siguiente clase [ClientMetier]:


<?php

// espacio de nombres
namespace Application;

class ClientMetier implements InterfaceClientMetier {
  // atributo
  private $clientDao;

  // constructor
  public function __construct(InterfaceClientDao $clientDao) {
    // se almacena la referencia en la capa [dao]
    $this->clientDao = $clientDao;
  }
  
  // cálculo del impuesto
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }

  // cálculo de impuestos en modo por lotes
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // se permiten las excepciones que provienen de la capa [dao]
    // se recuperan los datos de los contribuyentes
    $taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabla de resultados
    $results = [];
    // se procesan
    foreach ($taxPayersData as $taxPayerData) {
      // se calcula el impuesto
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // se completa [$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // se ingresa el resultado en la tabla de resultados
      $results [] = $taxPayerData;
    }
    // registro de los resultados
    $this->clientDao->saveResults($resultsFileName, $results);
  }

}

Comentarios

  • líneas 11-14: el constructor de la clase [ClientMetier] recibe como parámetro una referencia a la capa [dao];
  • líneas 17-19: el cálculo del impuesto se delega a la capa [dao];
  • líneas 20-38: la función [executeBatchImpots] se describió en el apartado «enlace»;

18.3.4. El script principal

Image

Image

El script de cliente [MainImpotsClient.php] implementa la capa [console] [9]. Se configura mediante el siguiente archivo jSON [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/TraitDao.php",
        "Dao/ClientDao.php",
        "Métier/InterfaceClientMetier.php",
        "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-08/impots-server.php"
}
  • línea 1: la carpeta raíz del cliente;
  • línea 2: el archivo jSON con los datos de los contribuyentes;
  • línea 3: el archivo jSON de los resultados;
  • línea 4: el archivo jSON de los errores;
  • líneas 6-19: las diferentes dependencias del proyecto del cliente;
  • líneas 20-23: el usuario que realiza las consultas al servidor de cálculo de impuestos;
  • línea 24: el archivo URL seguro del servidor de cálculo de impuestos;

El código del script [MainImpotsClient.php] es el siguiente:


<?php

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

// definición de constantes
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// dependencias de Symfony
use Symfony\Component\HttpClient\HttpClient;

// creación de la capa [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creación de la capa [métier]
$clientMetier = new ClientMetier($clientDao);

// cálculo de impuestos en modo por lotes
try {
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
  // se muestra el error
  print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;

Comentarios

  • línea 13: ruta del archivo de configuración;
  • línea 16: procesamiento del archivo de configuración;
  • líneas 18-26: carga de las dependencias;
  • línea 37: creación de la capa [dao]. Se le pasan al constructor de la capa los dos datos que espera:
    • el URL del servidor de cálculo de impuestos;
    • las credenciales del usuario que realizará las consultas;
  • línea 39: creación de la capa [métier]. Se le pasa al constructor de la capa una referencia a la capa [dao] que acaba de crearse;
  • línea 43: se le pide a la capa [métier] que:
    • calcule los impuestos de todos los contribuyentes del archivo $config["taxPayerDataFileName"];
    • guardar los resultados en el archivo $config["resultsFileName"];
    • guardar los errores en el archivo $config["errorsFileName"];
  • la línea 43 puede generar excepciones;
  • línea 46: muestra el mensaje de error de la excepción;

La ejecución del cliente arroja los mismos resultados que las versiones anteriores. Verifique los siguientes archivos:

  • [Data/taxpayersdata.json]: datos de los contribuyentes para quienes se calcula el monto del impuesto;
  • [Data/results.json]: resultados para los distintos contribuyentes del archivo [Data/taxpayersdata.json];
  • [Data/errors.json]: los errores que pudieron haber surgido al procesar el archivo [Data/taxpayersdata.json];

Veamos los posibles casos de error. En primer lugar, detengamos el servidor Laragon. Los resultados en la consola del cliente son entonces los siguientes:


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".
Terminé

Ahora iniciemos solo el servidor Apache y no el SGBD MySQL:

Image

Los resultados en la consola del cliente son entonces los siguientes:


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é

Ahora, ejecutemos MySQL y luego modifiquemos en [config-client] el usuario que inicia sesión:

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

Los resultados en la consola del cliente son entonces los siguientes:


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

18.3.5. Pruebas [Codeception]

Al igual que en versiones anteriores, vamos a escribir pruebas [Codeception] para la versión 08.

Image

18.3.5.1. Prueba de la capa [métier]

La prueba [ClientMetierTest.php] es la 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:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");

// 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";
}
//
// clase de prueba
class ClientMetierTest extends \Codeception\Test\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 test11() {

  }

}

Comentarios

  • líneas 10-26: definición del entorno de prueba. Utilizamos el mismo que el utilizado por el script principal [MainImpotsClient] descrito en el párrafo del enlace;
  • líneas 33-41: construcción de las capas [dao] y [métier];
  • línea 40: el atributo [$this→métier] hace referencia a la capa [métier];
  • líneas 44-51: los métodos [test1, test2…, test11] son los descritos en el párrafo enlace;

Los resultados de la prueba son los siguientes:

Image