Skip to content

18. Ejercicio práctico – version 8

Vamos a retomar la aplicación de ejemplo – version 5 (párrafo enlace) y la convertiremos en una aplicación cliente/servidor.

18.1. Introducción

La arquitectura de version 5 era la siguiente:

Image

  • la capa denominada [dao] (Data Access Objects) 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 director de orquesta: instancia las capas [dao] y [métier] y, a continuación, 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], recuperaremos la capa [dao] de la version 5 eliminándole 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 version 5 sin sus métodos [executeBatchImpôts, saveResults], que se migran a la capa [dao] [7] del cliente ;
  • en [4], hay que escribir el script del servidor: deberá:
    • crear las capas [métier] y [dao] [3, 2] ;
    • interactuar con el script del cliente [5, 7];
  • en [7], hay que escribir la capa [dao] del cliente:
    • será un cliente HTTP del script del servidor [4, 5];
    • tomará los métodos de acceso al sistema de archivos local de la capa [dao] de la version 5;
  • En [8], la capa [métier] del cliente respetará la interfaz [InterfaceMetier] de la version 5. Sin embargo, su implementación será diferente. En version 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 la capa [dao] [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 version 5 descritas en el apartado 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 la BD
  private $database;

  // constructor
  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 rellena la tabla de tramos impositivos
      $this->getTranches($connexion);
      // se rellena 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()) {
        // la transacción finaliza con un error
        $connexion->rollBack();
      }
      // se devuelve la excepción al código llamante
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // se cierra la conexión
      $connexion = NULL;
    }
  }

  // lectura de los datos de la base
  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 ha presentado en el apartado 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] está 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;

  //---------------------------------------------
  // configurar 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 el cálculo del impuesto
    // el método [getTaxAdminData] puede lanzar una excepción ExceptionImpots
    // se deja que se transmita al código llamante
    $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-desgravació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 en el apartado enlace de version 1. Su objeto version con una base de datos se ha presentado en el apartado 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 que 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] son los de version 5 descritos en el apartado 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 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 del servidor JSON
$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 - se comprueban los parámetros recibidos
$erreurs = [];
// deben ser 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 recupera 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";}
}

// ¿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 utiliza 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 comprueba que el usuario que realiza la solicitud forma parte de los usuarios autorizados;
  • líneas 54-63: si no es así, se envía el código HTTP 401, que indica un denegación 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 comprueba 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 entero >=0;
    • un parámetro [salaire] cuyo valor debe ser un entero >=0;
  • línea 65: cada vez que se detecta un error, se añade 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 coste 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 lanzar 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 el URL seguro [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 ha enviado un certificado SSL autofirmado. El navegador lo ha detectado y muestra una advertencia de seguridad: considera que el sitio del servidor no es de confianza;
  • en [4], continuamos;

Image

  • en [6], seguimos;

Image

  • en [7], el navegador muestra una ventana para que el usuario pueda autenticarse;
  • en [9,10], se introducen [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

Cortamos el SGBD MySQL y solicitamos el URL [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 version del servidor, probaremos las capas [métier] y [dao] tal y como se ha hecho desde la version 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 probarla, tal y 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 apartado 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 y como se hacía en el script del servidor [impots-server.php];
  • A partir de ahora estamos en las mismas condiciones que el script de servidor [impots-server.php]: podemos iniciar las pruebas;
  • líneas 40-53: los métodos [test1, test2…, test11] son los descritos en el apartado 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:

  • [BaseEntity] en el apartado «enlace»;
  • [ExceptionImpots] en el párrafo enlace;
  • [TaxPayerData] en el párrafo enlace;

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 se producen errores, estos 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] se implementa mediante 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-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 matriz [$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: el option, que hace que el cliente HTTP no compruebe la validez del certificado SSL enviado por el servidor;
  • líneas 29-34: se consulta al servidor con los tres parámetros que espera;
  • línea 36: se recupera la respuesta jSON del servidor. Si no se establece el parámetro [false] en el 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 en cuanto se intenta obtener el contenido de la respuesta [Response::getContent] o sus encabezados HTTP [Response::getHeaders]. Aquí, sea cual sea el estado HTTP de la respuesta, queremos poder acceder a su contenido, aunque solo sea para registrarlo (línea 40);
  • líneas 37-38: la respuesta del servidor es la cadena jSON de una matriz [‘réponse’=>qqChose]. Recuperamos el [qqChose];
  • línea 40: se registra la respuesta jSON en modo 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 encontrado un problema. A continuación, lanzamos una excepción de tipo [ExceptionImpots] con el mensaje, la respuesta jSON del servidor incrementada con el código HTTP de la respuesta;
  • línea 51: devolvemos el resultado, que es una tabla asociativa 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 batch
  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] se implementa mediante 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 batch
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // se permiten las excepciones procedentes 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 introduce 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 ha descrito 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 de 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 batch
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 pasan al constructor de la capa los dos datos que necesita:
    • el URL del servidor de cálculo de impuestos;
    • las credenciales del usuario que va a realizar las consultas;
  • línea 39: creación de la capa [métier]. Se 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 lanzar excepciones;
  • línea 46: visualización del mensaje de error de la excepción;

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

  • [Data/taxpayersdata.json]: datos de los contribuyentes para los que se calcula el importe del impuesto;
  • [Data/results.json]: resultados para los distintos contribuyentes del archivo [Data/taxpayersdata.json];
  • [Data/errors.json]: los errores que se hayan podido producir 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, iniciemos MySQL y luego modifiquemos en [config-client] el usuario que se conecta:

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 se hizo con los version anteriores, vamos a escribir pruebas [Codeception] para el version 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 la prueba. Utilizamos el mismo que el utilizado por el script principal [MainImpotsClient] descrito en el párrafo 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 apartado enlace;

Los resultados de la prueba son los siguientes:

Image