Skip to content

14. Clientes HTTP JavaScript del servicio de cálculo de impuestos

14.1. Introduction

Aquí nos proponemos escribir un cliente [node.js] de la versión 14 del servicio de cálculo de impuestos. La arquitectura cliente/servidor será la siguiente:

Image

Analizaremos dos versiones del cliente:

  • la versión 1 del cliente tendrá la siguiente estructura en capas: [main, dao]:

Image

  • la versión 2 del cliente tendrá una estructura [main, métier, dao]. La capa [métier] del servidor se trasladará al cliente:

Image

14.2. Cliente HTTP 1

Image

Como ya mencionamos, el cliente HTTP 1 implementa la siguiente arquitectura cliente/servidor:

Image

Implementaremos:

  • la capa [dao] en forma de una clase;
  • la capa [main] en forma de un script que utilice esta clase;

14.2.1. La capa [dao]

La capa [dao] se implementará mediante la siguiente clase [Dao1.js]:


'use strict';

// importaciones
import qs from 'qs'

class Dao1 {

  // constructor
  constructor(axios) {
    // biblioteca axios para realizar las solicitudes HTTP
    this.axios = axios;
    // cookie de sesión
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // iniciar sesión
  async  initSession() {
    // opciones de la consulta HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parámetros de la consulta URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // ejecución de la consulta HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // opciones de la solicitud HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Tipo de contenido: 'application/x-www-form-urlencoded',
      },
      // cuerpo del POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parámetros de la URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // ejecución de la consulta HTTP
    return await this.getRemoteData(options);
  }

  // cálculo del impuesto
  async  calculerImpot(marié, enfants, salaire) {
    // opciones de la consulta HHTP [post /main.php?action=calculer-impot]
    const options = {
      method: "POST",
      headers: {
        '«Content-type»: «application/x-www-form-urlencoded»,
      },
      // cuerpo de POST [marié, enfants, salaire]
      data: qs.stringify({
        marié: marié,
        enfants: enfants,
        salaire: salaire
      }),
      // parámetros del URL
      params: {
        action: 'calculer-impot'
      }
    };
    // ejecución de la consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  // lista de simulaciones
  async  listeSimulations() {
    // opciones de la consulta HHTP [get /main.php?action=lister-simulations]
    const options = {
      method: "GET",
      // parámetros de la consulta URL
      params: {
        action: 'lister-simulations'
      },
    };
    // ejecución de la consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  // lista de simulaciones
  async  supprimerSimulation(index) {
    // opciones de la consulta HHTP  [get /main.php?action=supprimer-simulation&numéro=index]
    const options = {
      method: "GET",
      // parámetros de la consulta URL
      params: {
        action: 'supprimer-simulation',
        numéro: index
      },
    };
    // ejecución de la consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  async  getRemoteData(options) {
    // para la cookie de sesión
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // ejecución de la consulta HTTP
    let response;
    try {
      // solicitud asíncrona
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // el parámetro [error] es una instancia de excepción; puede presentar diversas formas
      if (error.response) {
        // la respuesta del servidor se encuentra en [error.response]
        response = error.response;
      } else {
        // se vuelve a generar el error
        throw error;
      }
    }
    //: la respuesta es el conjunto completo de la respuesta HTTP del servidor (encabezados HTTP + la respuesta en sí)
    // se recupera la cookie de sesión, si existe
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie es un array
      // se busca la cookie de sesión en esta tabla
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // se busca la cookie de sesión
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // se guarda la cookie de sesión
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // la encontramos
          trouvé = true;
        } else {
          // el siguiente elemento
          i++;
        }
      }
    }
    // la respuesta del servidor está en [response.data]
    return response.data;
  }
}

// exportación de la clase
export default Dao1;
  • Aquí aplicamos lo que aprendimos en el párrafo «enlace», donde presentamos la biblioteca [axios] que permite realizar consultas HTTP tanto en [node.js] como en un navegador. Analizaremos en particular el script del párrafo «enlace»;
  • líneas 9-15: el constructor de la clase. Esta tendrá tres propiedades:
    • [axios]: el objeto [axios] que permite realizar las consultas HTTP. Este es pasado por el código que lo invoca;
    • [sessionCookieName]: dependiendo de los servidores, la cookie de sesión puede tener diferentes nombres. En este caso, es [PHPSESSID];
    • [sessionCookie]: la cookie de sesión enviada por el servidor y almacenada por el cliente;
  • líneas 53-76: la función asíncrona [calculerImpot] realiza la solicitud [post /main.php?action=calculer-impot] enviando los parámetros [marié, enfants, salaire]. Devuelve la cadena jSON transmitida por el servidor en forma de un objeto de JavaScript;
  • líneas 79-92: la función asíncrona [listeSimulations] realiza la solicitud [get /main.php?action=lister-simulations. Devuelve la cadena jSON enviada por el servidor en forma de un objeto de JavaScript;
  • líneas 95-109: la función asíncrona [supprimerSimulation] realiza la solicitud [get /main.php?action=supprimer-simulation&numéro=index]. Devuelve la cadena jSON enviada por el servidor en forma de un objeto JavaScript;
  • línea 121: se utiliza la notación [this.axios] porque, en este caso, el objeto [axios] enviado al constructor se ha almacenado en la propiedad [this.axios];
  • línea 161: se exporta la clase [Dao1] para poder utilizarla;

14.2.2. El script [main1.js]

El script [main1.js] realiza una serie de llamadas al servidor utilizando la clase [Dao1]:

  • inicialización de una sesión jSON;
  • autenticación con [admin, admin];
  • solicita tres cálculos de impuestos;
  • solicita la lista de simulaciones;
  • elimina una de ellas;

El código es el siguiente:


// importación de axios
import axios from 'axios';
// importación de la clase Dao1
import Dao from './Dao1';

// función asíncrona [main]
async function main() {
  // configuración de Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/versión-14';
  // instanciación de la capa [dao]
  const dao = new Dao(axios);
  // uso de la capa [dao]
  try {
    // inicialización de sesión
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // autenticación
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // cálculos de impuestos
    log("-----------calculer-impot x 3");
    response = await Promise.all([
      dao.calculerImpot("oui", 2, 45000),
      dao.calculerImpot("non", 2, 45000),
      dao.calculerImpot("non", 1, 30000)
    ]);
    log(response);
    // lista de simulaciones
    log("-----------liste-des-simulations");
    response = await dao.listeSimulations();
    log(response);
    // eliminación de una simulación
    log("-----------suppression simulation n° 1");
    response = await dao.supprimerSimulation(1);
    log(response);
  } catch (error) {
    // se registra el error
    console.log("erreur=", error.message);
  }
}

// registro jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// ejecución
main();

Comentarios

  • línea 2: se importa la biblioteca [axios];
  • línea 4: se importa la clase [Dao];
  • línea 7: la función [main], que se comunica con el servidor, es asíncrona;
  • líneas 9-10: configuración predeterminada de las solicitudes HTTP que se enviarán al servidor:
    • línea 9: [timeout] de 2 segundos;
    • línea 10: todas las URL tienen como prefijo la URL base de la versión 14 del servidor de cálculo de impuestos;
  • línea 12: se construye la capa [Dao]. Ahora ya se puede utilizar;
  • líneas 46-48: la función [log] tiene como objetivo mostrar la cadena jSON de un objeto JavaScript en un formato mejorado: en formato vertical con una sangría de dos espacios (tercer parámetro);
  • líneas 15-18: inicialización de la sesión jSON;
  • líneas 19-22: autenticación;
  • líneas 23-30: se solicitan tres cálculos de impuestos en paralelo. Gracias a [await Promise.all], la ejecución queda bloqueada hasta que se hayan obtenido los tres resultados;
  • líneas 31-34: lista de simulaciones;
  • líneas 35-38: eliminación de una simulación;
  • líneas 39-42: manejo de una posible excepción;

Los resultados de la ejecución son los siguientes:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 1\main1.js"
"-----------init-session"
{
  "action": "init-session",
  "état": 700,
  "réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
  "action": "authentifier-utilisateur",
  "état": 200,
  "réponse": "Authentification réussie [admin, admin]"
}
"-----------calculer-impot x 3"
[
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14
    }
  },
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "non",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 3250,
      "surcôte": 370,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.3
    }
  },
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14
    }
  }
]
"-----------liste-des-simulations"
{
  "action": "lister-simulations",
  "état": 500,
  "réponse": [
    {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 3250,
      "surcôte": 370,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.3,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14,
      "arrayOfAttributes": null
    }
  ]
}
"-----------suppression simulation n° 1"
{
  "action": "supprimer-simulation",
  "état": 600,
  "réponse": [
    {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14,
      "arrayOfAttributes": null
    }
  ]
}

[Done] exited with code=0 in 0.516 seconds

14.3. Cliente HTTP 2

Image

La arquitectura del cliente HTTP2 es la siguiente:

Image

Se ha trasladado la capa [métier] del servidor al cliente JavaScript. A diferencia de lo que hicimos en el curso PHP7, la capa [main] no tendrá que pasar aquí por la capa [métier] para llegar a la capa [dao]. Utilizaremos estas dos capas como centros de competencia:

  • la capa [main] pasa por la capa [dao] tan pronto como necesita datos que se encuentran en el servidor;
  • la capa [main] solicita a la capa [métier] que realice los cálculos del impuesto;
  • la capa [métier] es independiente de la capa [dao] y nunca recurre a ella;

14.3.1. La clase de JavaScript [Métier]

La esencia de la clase [Métier] en PHP se describió en el artículo enlazado. Se trata de un código bastante complejo que recordamos aquí, no para explicarlo, sino para poder traducirlo a JavaScript:


<?php

// espacio de nombres
namespace Application;

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

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

  public function __construct(InterfaceDao $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
    // entonces 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 {
    // $marié: sí, no
    // $enfants: número de hijos
    // $salaire: salario anual
    // $this->taxAdminData: datos de la administración tributaria
    //
    // se verifica que se cuente con los datos de la administración tributaria
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // cálculo del impuesto con hijos
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // cálculo del impuesto sin hijos
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // aplicación del límite máximo del coeficiente familiar
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euros para los dos primeros hijos
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euros para los dos primeros hijos, el doble para los siguientes
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // se toma el impuesto más alto
    if ($impot1 > $impot2) {
      $impot = $impot1;
      $taux = $result1["taux"];
      $surcôte = $result1["surcôte"];
    } else {
      $surcôte = $impot2 - $impot1 + $result2["surcôte"];
      $impot = $impot2;
      $taux = $result2["taux"];
    }
    // cálculo de una posible deducción
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // cálculo de una posible reducción de impuestos
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // 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 {
    // $marié: sí, no
    // $enfants: número de hijos
    // $salaire: salario anual
    // $this->taxAdminData: datos de la administración tributaria
    //
    // número de participaciones
    $marié = strtolower($marié);
    if ($marié === "oui") {
      $nbParts = $enfants / 2 + 2;
    } else {
      $nbParts = $enfants / 2 + 1;
    }
    // 1 parte por hijo a partir del tercero
    if ($enfants >= 3) {
      // media participación adicional por cada hijo a partir del tercero
      $nbParts += 0.5 * ($enfants - 2);
    }
    // ingresos imponibles
    $revenuImposable = $this->getRevenuImposable($salaire);
    // recargo
    $surcôte = floor($revenuImposable - 0.9 * $salaire);
    // por problemas de redondeo
    if ($surcôte < 0) {
      $surcôte = 0;
    }
    // coeficiente familiar
    $quotient = $revenuImposable / $nbParts;
    // cálculo del impuesto
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // se coloca al final de la tabla de límites para detener el bucle siguiente
    $limites[count($limites) - 1] = $quotient;
    // búsqueda de la tasa impositiva
    $i = 0;
    while ($quotient > $limites[$i]) {
      $i++;
    }
    // debido a que se colocó $quotient al final de la tabla $limites, el bucle anterior
    // no puede salirse de los límites de la tabla $limites
    // ahora podemos calcular el impuesto
    $impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
    // 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 {
    // deducción del 10 % del salario
    $abattement = 0.1 * $salaire;
    // esta deducción no puede superar $this->taxAdminData->getAbattementDixPourCentMax()
    if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMax();
    }
    // la deducción no puede ser menor que $this->taxAdminData->getAbattementDixPourcentMin()
    if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMin();
    }
    // ingresos imponibles
    $revenuImposable = $salaire - $abattement;
    // resultado
    return floor($revenuImposable);
  }

// calcula un posible descuento
  private function getDecôte(string $marié, float $salaire, float $impots): float {
    // inicialmente, un descuento nulo
    $décôte = 0;
    // monto máximo del impuesto para obtener la reducción
    $plafondImpôtPourDécôte = $marié === "oui" ?
      $this->taxAdminData->getPlafondImpotCouplePourDecote() :
      $this->taxAdminData->getPlafondImpotCelibatairePourDecote();
    if ($impots < $plafondImpôtPourDécôte) {
      // monto máximo de la reducción
      $plafondDécôte = $marié === "oui" ?
        $this->taxAdminData->getPlafondDecoteCouple() :
        $this->taxAdminData->getPlafondDecoteCelibataire();
      // descuento teórico
      $décôte = $plafondDécôte - 0.75 * $impots;
      // la deducción no puede superar el monto del impuesto
      if ($décôte > $impots) {
        $décôte = $impots;
      }
      // no hay descuento <0
      if ($décôte < 0) {
        $décôte = 0;
      }
    }
    // resultado
    return ceil($décôte);
  }

// calcula una posible reducción
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    // el límite máximo de ingresos para tener derecho a la reducción del 20 %
    $plafondRevenuPourRéduction = $marié === "oui" ?
      $this->taxAdminData->getPlafondRevenusCouplePourReduction() :
      $this->taxAdminData->getPlafondRevenusCelibatairePourReduction();
    $plafondRevenuPourRéduction += $enfants * $this->taxAdminData->getValeurReducDemiPart();
    if ($enfants > 2) {
      $plafondRevenuPourRéduction += ($enfants - 2) * $this->taxAdminData->getValeurReducDemiPart();
    }
    // ingresos imponibles
    $revenuImposable = $this->getRevenuImposable($salaire);
    // descuento
    $réduction = 0;
    if ($revenuImposable < $plafondRevenuPourRéduction) {
      // reducción del 20 %
      $réduction = 0.2 * $impots;
    }
    // resultado
    return ceil($réduction);
  }

  // cálculo de impuestos en modo por lotes
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // se permiten excepciones provenientes de la capa [dao]
    // se recuperan los datos de los contribuyentes
    $taxPayersData = $this->dao->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->setMontant($result["impôt"]);
      $taxPayerData->setDécôte($result["décôte"]);
      $taxPayerData->setSurCôte($result["surcôte"]);
      $taxPayerData->setTaux($result["taux"]);
      $taxPayerData->setRéduction($result["réduction"]);
      // se ingresa el resultado en la tabla de resultados
      $results [] = $taxPayerData;
    }
    // registro de los resultados
    $this->dao->saveResults($resultsFileName, $results);
  }

}
  • líneas 19-26: el constructor de la clase PHP. Como dijimos que íbamos a construir una capa [métier] independiente de la capa [dao], realizaremos dos modificaciones en este constructor en JavaScript:
    • no recibirá una instancia de la capa [dao] (ya no la necesita);
    • no solicitará los datos fiscales de la administración [taxAdminData] a la capa [dao]: será el código que lo invoca el que transmita estos datos al constructor;
  • líneas 197-122: no implementaremos el método [executeBatchImpots], cuyo objetivo final era guardar los resultados de las simulaciones en un archivo de texto. Queremos un código que funcione tanto en [node.js] como en un navegador. Sin embargo, no es posible guardar datos en el sistema de archivos de la máquina que ejecuta el navegador del cliente;

Con estas restricciones, el código de la clase de JavaScript [Métier] es el siguiente:


'use strict';

// clase Métier
class Métier {

  // constructor
  constructor(taxAdmindata) {
    // this.taxAdminData: datos de la administración tributaria
    this.taxAdminData = taxAdmindata;
  }

  // cálculo del impuesto
  // --------------------------------------------------------------------------
  calculerImpot(marié, enfants, salaire) {
    // casado: sí, no
    // hijos: número de hijos
    // salario: salario anual
    // this.taxAdminData: datos de la administración tributaria
    //
    // cálculo del impuesto con hijos
    const result1 = this.calculerImpot2(marié, enfants, salaire);
    const impot1 = result1["impôt"];
    // cálculo del impuesto sin hijos
    let result2, impot2, plafondDemiPart;
    if (enfants !== 0) {
      result2 = this.calculerImpot2(marié, 0, salaire);
      impot2 = result2["impôt"];
      // aplicación del límite máximo del coeficiente familiar
      plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
      if (enfants < 3) {
        // PLAFOND_QF_DEMI_PART euros para los dos primeros hijos
        impot2 = impot2 - enfants * plafondDemiPart;
      } else {
        // PLAFOND_QF_DEMI_PART euros para los dos primeros hijos, el doble para los siguientes
        impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
      }
    } else {
      // no se vuelve a calcular el impuesto
      impot2 = impot1;
      result2 = result1;
    }
    // se toma el impuesto más alto de [impot1, impot2]
    let impot, taux, surcôte;
    if (impot1 > impot2) {
      impot = impot1;
      taux = result1["taux"];
      surcôte = result1["surcôte"];
    } else {
      surcôte = impot2 - impot1 + result2["surcôte"];
      impot = impot2;
      taux = result2["taux"];
    }
    // cálculo de una posible deducción
    const décôte = this.getDecôte(marié, impot);
    impot -= décôte;
    // cálculo de una posible reducción de impuestos
    const réduction = this.getRéduction(marié, salaire, enfants, impot);
    impot -= réduction;
    // resultado
    return {
      "impôt": Math.floor(impot), "surcôte": surcôte, "décôte": décôte, "réduction": réduction,
      "taux": taux
    };
  }

  // --------------------------------------------------------------------------
  calculerImpot2(marié, enfants, salaire) {
    // casado: sí, no
    // hijos: número de hijos
    // salario: salario anual
    // this->taxAdminData: datos de la administración tributaria
    //
    // número de cuotas
    marié = marié.toLowerCase();
    let nbParts;
    if (marié === "oui") {
      nbParts = enfants / 2 + 2;
    } else {
      nbParts = enfants / 2 + 1;
    }
    // 1 parte por hijo a partir del tercero
    if (enfants >= 3) {
      // media participación adicional por cada hijo a partir del tercero
      nbParts += 0.5 * (enfants - 2);
    }
    // ingresos imponibles
    const revenuImposable = this.getRevenuImposable(salaire);
    // recargo
    let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
    // por problemas de redondeo
    if (surcôte < 0) {
      surcôte = 0;
    }
    // cuota familiar
    const quotient = revenuImposable / nbParts;
    // cálculo del impuesto
    const limites = this.taxAdminData.limites;
    const coeffR = this.taxAdminData.coeffR;
    const coeffN = this.taxAdminData.coeffN;
    // se coloca al final de la tabla de límites para detener el bucle siguiente
    limites[limites.length - 1] = quotient;
    // búsqueda de la tasa impositiva
    let i = 0;
    while (quotient > limites[i]) {
      i++;
    }
    // debido a que se colocó el cociente familiar al final de la tabla de límites, el bucle anterior
    // no puede sobrepasar los límites de la tabla
    // ahora se puede calcular el impuesto
    const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
    // 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
  getRevenuImposable(salaire) {
    // deducción del 10 % del salario
    let abattement = 0.1 * salaire;
    // esta deducción no puede superar taxAdminData.getAbattementDixPourCentMax()
    if (abattement > this.taxAdminData.abattementDixPourCentMax) {
      abattement = this.taxAdminData.abattementDixPourcentMax;
    }
    // la deducción no puede ser inferior a taxAdminData.getAbattementDixPourcentMin()
    if (abattement < this.taxAdminData.abattementDixPourcentMin) {
      abattement = this.taxAdminData.abattementDixPourcentMin;
    }
    // ingresos imponibles
    const revenuImposable = salaire - abattement;
    // resultado
    return Math.floor(revenuImposable);
  }

  // calcula un posible descuento
  getDecôte(marié, impots) {
    // inicialmente, un descuento nulo
    let décôte = 0;
    // monto máximo del impuesto para obtener la reducción
    let plafondImpôtPourDécôte = marié === "oui" ?
      this.taxAdminData.plafondImpotCouplePourDecote :
      this.taxAdminData.plafondImpotCelibatairePourDecote;
    let plafondDécôte;
    if (impots < plafondImpôtPourDécôte) {
      // monto máximo de la reducción
      plafondDécôte = marié === "oui" ?
        this.taxAdminData.plafondDecoteCouple :
        this.taxAdminData.plafondDecoteCelibataire;
      // descuento teórico
      décôte = plafondDécôte - 0.75 * impots;
      // la deducción no puede superar el monto del impuesto
      if (décôte > impots) {
        décôte = impots;
      }
      // no hay descuento <0
      if (décôte < 0) {
        décôte = 0;
      }
    }
    // resultado
    return Math.ceil(décôte);
  }

  // calcula una posible reducción
  getRéduction(marié, salaire, enfants, impots) {
    // el límite máximo de ingresos para tener derecho a la reducción del 20 %
    let plafondRevenuPourRéduction = marié === "oui" ?
      this.taxAdminData.plafondRevenusCouplePourReduction :
      this.taxAdminData.plafondRevenusCelibatairePourReduction;
    plafondRevenuPourRéduction += enfants * this.taxAdminData.valeurReducDemiPart;
    if (enfants > 2) {
      plafondRevenuPourRéduction += (enfants - 2) * this.taxAdminData.valeurReducDemiPart;
    }
    // ingresos imponibles
    const revenuImposable = this.getRevenuImposable(salaire);
    // descuento
    let réduction = 0;
    if (revenuImposable < plafondRevenuPourRéduction) {
      // reducción del 20 %
      réduction = 0.2 * impots;
    }
    // resultado
    return Math.ceil(réduction);
  }
}

// exportación de la clase
export default Métier;
  • El código de JavaScript sigue al pie de la letra el código PHP;
  • Se exporta la clase [Métier], línea 187;

14.3.2. La clase de JavaScript [Dao2]

Image

La clase [Dao2] implementa la capa [dao] del cliente de JavaScript anterior de la siguiente manera:


'use strict';

// importaciones
import qs from 'qs'

class Dao2 {

  // constructor
  constructor(axios) {
    this.axios = axios;
    // cookie de sesión
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // inicialización de sesión
  async  initSession() {
    // opciones de la solicitud HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parámetros de la consulta URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // ejecución de la consulta HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // opciones de la consulta HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        '«Content-type»: 'application/x-www-form-urlencoded',
      },
      // cuerpo del POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parámetros de la URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // ejecución de la consulta HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // opciones de la consulta HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // parámetros de la consulta URL
      params: {
        action: 'get-admindata'
      }
    };
    // ejecución de la consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  async  getRemoteData(options) {
    // para la cookie de sesión
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // ejecución de la consulta HTTP
    let response;
    try {
      // solicitud asíncrona
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // el parámetro [error] es una instancia de excepción; puede presentar diversas formas
      if (error.response) {
        // la respuesta del servidor se encuentra en [error.response]
        response = error.response;
      } else {
        // se vuelve a generar el error
        throw error;
      }
    }
    // la respuesta es el conjunto completo de la respuesta HTTP del servidor (encabezados HTTP + la respuesta en sí)
    // se recupera la cookie de sesión, si existe
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie es un array
      // se busca la cookie de sesión en este array
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // se busca la cookie de sesión
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // se guarda la cookie de sesión
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // la encontramos
          trouvé = true;
        } else {
          // el siguiente elemento
          i++;
        }
      }
    }
    // la respuesta del servidor está en [response.data]
    return response.data;
  }
}

// exportación de la clase
export default Dao2;

Comentarios

  • La clase [Dao2] solo implementa tres de las posibles solicitudes al servidor de cálculo de impuestos:
    • [init-session] (líneas 17-29): para inicializar la sesión jSON;
    • [authentifier-utilisateur] (líneas 31-50): para autenticarse;
    • [get-admindata] (líneas 52-65): para obtener los datos de la administración tributaria que permitirán realizar los cálculos del impuesto, del lado del cliente;
  • líneas 52-65: introducimos una nueva acción [get-admindata] hacia el servidor. Esta acción no se había implementado hasta ahora. Lo hacemos ahora.

14.3.3. Modificación del servidor de cálculo de impuestos

El servidor de cálculo de impuestos debe implementar una nueva acción. Lo haremos en la versión 14 del servidor. La acción que se debe implementar tiene las siguientes características:

  • es solicitada por una operación [get /main.php?action=get-admindata];
  • devuelve la cadena jSON de un objeto que encapsula los datos de la administración tributaria;

Vamos a repasar cómo agregar una acción a nuestro servidor.

La modificación se realizará en NetBeans:

Image

En [2], modificamos el archivo [config.json] para agregar la nueva acción:


{
    "databaseFilename": "Config/database.json",
    "corsAllowed": true,
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-14",
    "relativeDependencies": [

        "/Entities/BaseEntity.php",
        "/Entities/Simulation.php",
        "/Entities/Database.php",
        "/Entities/TaxAdminData.php",
        "/Entities/ExceptionImpots.php",

        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php",        

        "/Model/InterfaceServerDao.php",
        "/Model/ServerDao.php",
        "/Model/ServerDaoWithSession.php",
        "/Model/InterfaceServerMetier.php",
        "/Model/ServerMetier.php",

        "/Responses/InterfaceResponse.php",
        "/Responses/ParentResponse.php",
        "/Responses/JsonResponse.php",
        "/Responses/XmlResponse.php",
        "/Responses/HtmlResponse.php",

        "/Controllers/InterfaceController.php",
        "/Controllers/InitSessionController.php",
        "/Controllers/ListerSimulationsController.php",
        "/Controllers/AuthentifierUtilisateurController.php",
        "/Controllers/CalculerImpotController.php",
        "/Controllers/SupprimerSimulationController.php",
        "/Controllers/FinSessionController.php",
        "/Controllers/AfficherCalculImpotController.php",
        "/Controllers/AdminDataController.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php",
        "C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
    ],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Logs/logs.txt",
    "actions":
            {
                "init-session": "\\InitSessionController",
                "authentifier-utilisateur": "\\AuthentifierUtilisateurController",
                "calculer-impot": "\\CalculerImpotController",
                "lister-simulations": "\\ListerSimulationsController",
                "supprimer-simulation": "\\SupprimerSimulationController",
                "fin-session": "\\FinSessionController",
                "afficher-calcul-impot": "\\AfficherCalculImpotController",
                "get-admindata": "\\AdminDataController"
            },
    "types": {
        "json": "\\JsonResponse",
        "html": "\\HtmlResponse",
        "xml": "\\XmlResponse"
    },
    "vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
    "vue-erreurs": "vue-erreurs.php"
}

La modificación consiste en:

  • línea 67: agregar la acción [get-admindata] y asociarla a un controlador;
  • línea 36: declarar este controlador en la lista de clases que debe cargar la aplicación PHP;

El siguiente paso es implementar el controlador [AdminDataController] [3]:


<?php

namespace Application;

// dependencias de Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias de la capa [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

class AdminDataController implements InterfaceController {

  // $config es la configuración de la aplicación
  // procesamiento de una solicitud Request
  // utiliza la sesión Session y puede modificarla
  // $infos son datos adicionales específicos de cada controlador
  // devuelve un array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    //: debe haber un único parámetro GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      // se observa el error
      $message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
      $état = 1001;
      // se devuelve el resultado al controlador principal
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }

    // se puede trabajar
    // Redis
    \Predis\Autoloader::register();
    try {
      // cliente [predis]
      $redis = new \Predis\Client();
      // nos conectamos al servidor para ver si está disponible
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // salió mal
      // se devolvió el resultado con un error al controlador principal
      $état = 1050;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }

    // Recuperación de datos de la administración tributaria
    // primero se busca en la caché [redis]
    if (!$redis->get("taxAdminData")) {
      try {
        // se buscan los datos fiscales en la base de datos
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // taxAdminData
        $taxAdminData = $dao->getTaxAdminData();
        // se guardan en Redis los datos recuperados
        $redis->set("taxAdminData", $taxAdminData);
      } catch (\RuntimeException $ex) {
        // Algo salió mal
        // Se devuelve el resultado con un error al controlador principal
        $état = 1041;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // los datos fiscales se almacenan en la memoria [redis] del ámbito [application]
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      // se instancia un objeto [TaxAdminData] a partir de la tabla de atributos anterior
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
    }

    // se devuelve el resultado al controlador principal
    $état = 1000;
    return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
  }

}

Comentarios

  • línea 12: al igual que los demás controladores del servidor, [AdminDataController] implementa la interfaz [InterfaceController], que consiste en el método [execute] de las líneas 19 a 79;
  • línea 78: al igual que los demás controladores del servidor, el método [AdminDataController.execute] devuelve un array [$status, $état, [‘réponse’=>$response]] con:
    • [$status]: el código de estado de la respuesta HTTP;
    • [$état]: un código interno de la aplicación que representa el estado en el que se encuentra el servidor tras ejecutar la solicitud del cliente;
    • [$response]: un array que encapsula la respuesta que se enviará al cliente. Aquí, este array se transformará posteriormente en la cadena jSON;
  • líneas 25-34: se verifica que la acción [get-admindata] del cliente sea sintácticamente correcta;
  • líneas 37-74: se recupera un objeto [TaxAdminData] que se encuentra:
    • líneas 56-59: en la base de datos si no se ha encontrado en la caché [redis];
    • líneas 70-73: en la caché [redis];

Este código retoma el del controlador [CalculerImpotController] explicado en el artículo enlazado. De hecho, este controlador también debía recuperar el objeto [TaxAdminData] que encapsula los datos de la administración tributaria.

Durante las pruebas del cliente de JavaScript, la forma jSON de [TaxAdminData] presentó un problema cuando este objeto se encontró en la caché [redis]. Para entenderlo, veamos en qué formato se almacena este objeto en [redis]:

Image

Image

  • En [5-7], se observa que los valores numéricos se han almacenado como cadenas de caracteres. PHP lo ha aceptado porque el operador + en los cálculos entre números y cadenas provoca implícitamente un cambio de tipo de la cadena a un número. Pero JavaScript hace lo contrario: el operador + en los cálculos entre números y cadenas provoca implícitamente un cambio de tipo del número a una cadena de caracteres. Por lo tanto, los cálculos de la clase de JavaScript [Métier] son erróneos;

Para solucionar este problema, modificamos el método [TaxAdminData.setFromArrayOfAttributes] utilizado en la línea 71 del controlador para instanciar un objeto [TaxAdminData] (véase el artículo) a partir de la cadena jSON que se encuentra en la caché [redis]:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // tramos impositivos
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // constantes para el cálculo del impuesto
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // inicialización
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // padre
    parent::setFromJsonFile($taxAdminDataFilename);
    // se verifican los valores de los atributos
    $this->checkAttributes();
    // se devuelve el objeto
    return $this;
  }

  protected function check($value): \stdClass {
    // $value es una matriz de elementos de tipo cadena o un único elemento
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // se transforma la matriz de cadenas en una matriz de números reales
    $newTableau = [];
    $result = new \stdClass();
    // los elementos del array deben ser números decimales positivos o nulos
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // se coloca el número flotante en newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // se anota el error
        $result->erreur = TRUE;
        // salimos
        return $result;
      }
    }
    // se devuelve el resultado
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // un solo valor
      $result->value = $newTableau[0];
    } else {
      // una lista de valores
      $result->value = $newTableau;
    }
    return $result;
  }

  // inicialización mediante una tabla de atributos
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // padre
    parent::setFromArrayOfAttributes($arrayOfAttributes);
    // se verifican los valores de los atributos
    $this->checkAttributes();
    // se devuelve el objeto
    return $this;
  }

  // verificación de los valores de los atributos
  protected function checkAttributes() {
    // se verifica que los valores de los atributos sean números reales >=0
    foreach ($this as $key => $value) {
      if (is_string($value)) {
        // $value debe ser un número real >=0 o una matriz de números reales >=0
        $result = $this->check($value);
        // ¿Error?
        if ($result->erreur) {
          // se lanza una excepción
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // se registra el valor
          $this->$key = $result->value;
        }
      }
    }

    // se devuelve el objeto
    return $this;
  }

  // getter y setter
  ...

}

Comentarios

  • línea 5: la clase [TaxAdminData] extiende la clase [BaseEntity], que ya cuenta con el método [setFromArrayOfAttributes]. Dado que este no es adecuado, lo redefinimos en las líneas 67-75;
  • línea 70: primero se utiliza el método [setFromArrayOfAttributes] de la clase padre para inicializar los atributos de la clase;
  • línea 72: el método [checkAttributes] verifica que los valores asociados sean números. Si son cadenas, estas se convierten en números;
  • línea 74: el objeto [$this] resultante es, entonces, un objeto con atributos de valores numéricos;
  • líneas 78-93: el método [checkAttributes] verifica que los valores asociados a los atributos del objeto sean numéricos;
  • línea 80: se recorre la lista de atributos;
  • línea 81: si el valor de un atributo es de tipo [string];
  • línea 83: entonces se verifica que esa cadena represente un número;
  • línea 90: si es así, la cadena se convierte en un número y se asigna al atributo comprobado;
  • líneas 85-86: si no es así, se lanza una excepción;
  • líneas 32-65: la función [check] hace un poco más de lo necesario. Procesa tanto tablas como valores únicos. Sin embargo, aquí solo se invoca para verificar un valor de tipo [string]. Devuelve un objeto con las propiedades [erreur, value], donde:
    • [erreur] es un valor booleano que indica si hay un error o no;
    • [value] es el parámetro [value] de la línea 32, transformado en un número o una matriz de números, según el caso;

La clase [BaseEntity], que podía tener un atributo llamado [arrayOfAttributes], se modifica para que ya no lo tenga: de hecho, este contamina la cadena jSON de [TaxAdminData]. La clase se reescribe de la siguiente manera:


<?php

namespace Application;

class BaseEntity {
  
  // inicialización a partir de un archivo JSON
  public function setFromJsonFile(string $jsonFilename) {
    // se recupera el contenido del archivo de datos fiscales
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // ¿Error?
    if (!$fileContents) {
      // se registra el error
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // se recupera el código JSON del archivo de configuración en una tabla asociativa
      $arrayOfAttributes = \json_decode($fileContents, true);
      // ¿error?
      if ($arrayOfAttributes === FALSE) {
        // se registra el error
        $erreur = TRUE;
        $message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // ¿Error?
    if ($erreur) {
      // se lanza una excepción
      throw new ExceptionImpots($message);
    }
    // inicialización de los atributos de la clase
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // se verifica la presencia de todos los atributos
    $this->checkForAllAttributes($arrayOfAttributes);
    // se devuelve el objeto
    return $this;
  }

  public function checkForAllAttributes($arrayOfAttributes) {
    // se verifica que todas las claves se hayan inicializado
    foreach (\array_keys($arrayOfAttributes) as $key) {
      if (!isset($this->$key)) {
        throw new ExceptionImpots("L'attribut [$key] de la classe "
          . get_class($this) . " n'a pas été initialisé");
      }
    }
  }

  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // se inicializan algunos atributos de la clase (no necesariamente todos)
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // se devuelve el objeto
    return $this;
  }

  // toString
  public function __toString() {
    // atributos del objeto
    $arrayOfAttributes = \get_object_vars($this);
    // cadena jSON del objeto
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }

}

Comentarios

  • línea 20: el atributo [$this→arrayOfAttributes] se ha transformado en una variable que ahora debe pasarse al método [checkForAllAttributes], línea 38, que anteriormente operaba sobre el atributo [$this→arrayOfAttributes];

Debido a este cambio en [BaseEntity], la clase [Database] también debe modificarse ligeramente:


<?php

namespace Application;

class Database extends BaseEntity {
  // atributos
  protected $dsn;
  protected $id;
  protected $pwd;
  protected $tableTranches;
  protected $colLimites;
  protected $colCoeffR;
  protected $colCoeffN;
  protected $tableConstantes;
  protected $colPlafondQfDemiPart;
  protected $colPlafondRevenusCelibatairePourReduction;
  protected $colPlafondRevenusCouplePourReduction;
  protected $colValeurReducDemiPart;
  protected $colPlafondDecoteCelibataire;
  protected $colPlafondDecoteCouple;
  protected $colPlafondImpotCelibatairePourDecote;
  protected $colPlafondImpotCouplePourDecote;
  protected $colAbattementDixPourcentMax;
  protected $colAbattementDixPourcentMin;

  // setter
  // inicialización
  public function setFromJsonFile(string $jsonFilename) {
    // padre
    parent::setFromJsonFile($jsonFilename);
    // devuelve el objeto
    return $this;
  }

  // getters y setters
  ...
}

Comentarios

  • En el código original, después de la línea 30, se llamaba al método [parent::checkForAllAttributes]. Ya no es necesario hacerlo, ya que ahora el método [parent::setFromJsonFile($jsonFilename)] se encarga de ello automáticamente;

14.3.4. Pruebas del servidor con [Postman]

[Postman] se presentó en el artículo enlace.

Utilizamos las siguientes pruebas de Postman:

Image

Image

Image

El resultado jSON de esta última consulta es el siguiente:

Image

  • En [5-8], se puede observar que los atributos de la cadena jSON sí tienen valores numéricos (y no cadenas de caracteres). Este resultado permitirá que la clase de JavaScript [Métier] se ejecute normalmente;

14.3.5. El script principal [main]

Image

El script principal [main] del cliente de JavaScript es el siguiente:


// importaciones
import axios from 'axios';

// importaciones
import Dao from './Dao2';
import Métier from './Métier';

// función asíncrona [main]
async function main() {
  // configuración de Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/versión-14';
  // instanciación de la capa [dao]
  const dao = new Dao(axios);
  // solicitudes HTTP
  let taxAdminData;
  try {
    // inicialización de sesión
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // autenticación
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // datos fiscales
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response.réponse;
  } catch (error) {
    // se registra el error
    console.log("erreur=", error.message);
    // fin
    return;
  }

  // instanciación de la capa [métier]
  const métier = new Métier(taxAdminData);

  // cálculos de impuestos
  log("-----------calculer-impot x 3");
  const simulations = [];
  simulations.push(métier.calculerImpot("oui", 2, 45000));
  simulations.push(métier.calculerImpot("non", 2, 45000));
  simulations.push(métier.calculerImpot("non", 1, 30000));
  // lista de simulaciones
  log("-----------liste-des-simulations");
  log(simulations);
  // eliminación de una simulación
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

// registro jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// ejecución
main();

Comentarios

  • líneas 5-6: importaciones de las clases [Dao] y [Métier];
  • línea 9: la función asíncrona [main], que organizará la comunicación con el servidor mediante la clase [Dao] y solicitará a la clase [Métier] que realice los cálculos de impuestos;
  • líneas 10-36: el script llama sucesivamente y de manera bloqueante a los métodos [initSession, authentifierUtilisateur, getAdminData] de la capa [dao];
  • línea 38: ya no se necesita la capa [dao]. Se cuenta con todos los elementos para que funcione la capa [métier] del cliente JavaScript;
  • líneas 41-46: se realizan tres cálculos de impuestos y se acumulan los resultados en una tabla [simulations];
  • línea 49: se muestra la tabla de simulaciones;
  • línea 52: se elimina una de ellas;

Los resultados de la ejecución del script principal son los siguientes:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 2\main2.js"
"-----------init-session"
{
  "action": "init-session",
  "état": 700,
  "réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
  "action": "authentifier-utilisateur",
  "état": 200,
  "réponse": "Authentification réussie [admin, admin]"
}
"-----------get-admindata"
{
  "action": "get-admindata",
  "état": 1000,
  "réponse": {
    "limites": [
      9964,
      27519,
      73779,
      156244,
      0
    ],
    "coeffR": [
      0,
      0.14,
      0.3,
      0.41,
      0.45
    ],
    "coeffN": [
      0,
      1394.96,
      5798,
      13913.69,
      20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
  }
}
"-----------calculer-impot x 3"
"-----------liste-des-simulations"
[
  {
    "impôt": 502,
    "surcôte": 0,
    "décôte": 857,
    "réduction": 126,
    "taux": 0.14
  },
  {
    "impôt": 3250,
    "surcôte": 370,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.3
  },
  {
    "impôt": 1687,
    "surcôte": 0,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.14
  }
]
"-----------suppression simulation n° 1"
[
  {
    "impôt": 502,
    "surcôte": 0,
    "décôte": 857,
    "réduction": 126,
    "taux": 0.14
  },
  {
    "impôt": 1687,
    "surcôte": 0,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.14
  }
]

[Done] exited with code=0 in 0.583 seconds

14.4. Cliente HTTP 3

Image

En esta sección, ejecutamos la aplicación [Client HTTP 2] en un navegador siguiendo la siguiente arquitectura:

Image

La migración no es inmediata. Si bien [node.js] puede ejecutar JavaScript ES6, este no suele ser el caso de los navegadores. Por lo tanto, es necesario utilizar herramientas que traduzcan el código ES6 a código ES5 que los navegadores recientes puedan entender. Afortunadamente, estas herramientas son potentes y bastante fáciles de usar.

Aquí hemos seguido el artículo [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].

En la carpeta [client HTTP 3/src], hemos colocado los elementos [main.js, Métier.js, Dao2.js] de la aplicación [Client Http 2] que acabamos de desarrollar.

14.4.1. Inicialización del proyecto

Vamos a trabajar en la carpeta [client http 3]. Abrimos un terminal en [VSCode] y nos desplazamos a esta carpeta:

Image

Inicializamos este proyecto con el comando [npm init] y aceptamos las respuestas predeterminadas a las preguntas que se nos plantean:

Image

  • en [4-5], el archivo de configuración del proyecto [package.json] generado a partir de las diferentes respuestas dadas;

14.4.2. Instalación de las dependencias del proyecto

Vamos a instalar las siguientes dependencias:

  • [@babel/core]: el núcleo de la herramienta [Babel] [https://babeljs.io] que transforma el código ES 2015+ en código ejecutable en navegadores recientes y más antiguos;
  • [@babel/preset-env]: forma parte del conjunto de herramientas Babel. Interviene antes de la transpilación de ES6 a ES5;
  • [babel-loader]: esta dependencia permite que la herramienta [webpack] utilice la herramienta [Babel];
  • [webpack]: el «director de orquesta». Es [webpack] la que recurre a Babel para realizar la transpilación de los códigos ES6 → ES5 y, posteriormente, es ella la que ensambla todos los archivos resultantes en un único archivo;
  • [webpack-cli]: necesario para [webpack];
  • [@webpack-cli/init]: se utiliza para configurar [webpack];
  • [webpack-dev-server]: proporciona un servidor web de desarrollo que opera por defecto en el puerto 8080. Cuando se modifican los archivos fuente, recarga automáticamente la aplicación web;

Las dependencias del proyecto se instalan de la siguiente manera en un terminal de [VSCode]:

npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

Image

Tras la instalación de las dependencias, el archivo [package.json] ha cambiado de la siguiente manera:


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "client jS du serveur de calcul de l'impôt",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "cross-env": "^6.0.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  }
}
  • líneas 12-19: las dependencias del proyecto son [devDependencies]: se necesitan durante la fase de desarrollo, pero ya no en la fase de producción. De hecho, en producción se utiliza el archivo [dist/main.js]. Está codificado en ES5 y ya no necesita las herramientas de transpilación de código de ES6 a código ES5;

Debemos agregar dos dependencias al proyecto:

  • [core-js]: contiene «polyfills» para ECMAScript 2019. Un polyfill permite ejecutar código reciente, como ECMAScript 2019 (septiembre de 2019), en navegadores antiguos;
  • [regenerator-runtime]: según el sitio web de la biblioteca --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];

Estas dos dependencias sustituyen, a partir de Babel 7, a la dependencia [@babel/polyfill], que antes desempeñaba esta función y que ahora (septiembre de 2019) está en desuso. Se instalan de la siguiente manera:

Image

El archivo [package.json] cambia entonces de la siguiente manera:


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "regenerator-runtime": "^0.13.3"
  }
}

El uso de las dependencias [core-js, regenerator-runtime] requiere incluir los siguientes [imports] (líneas 3-4) en el script principal [src/main.js]:


// importaciones
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// importaciones
import Dao from './Dao2';
import Métier from './Métier';

14.4.3. Configuración de [webpack]

[webpack] es la herramienta que se encargará de:

  • la transpilación de ES6 → ES5 de todos los archivos de JavaScript del proyecto;
  • la combinación de los archivos generados en un único archivo;

Esta herramienta se controla mediante un archivo de configuración [webpack.config.js] que se puede generar gracias a una dependencia llamada [@webpack-cli/init] (septiembre de 2019). Esta se instaló junto con las demás en el párrafo del enlace.

Ejecutamos el comando [npx webpack-cli init] en un terminal [VSCode]:

Image

Después de responder a las diferentes preguntas (en las que se pueden aceptar la mayoría de las respuestas propuestas por defecto), se genera un archivo [webpack.config.js] en la raíz del proyecto [4]:

El archivo [webpack.config.js] tiene el siguiente aspecto:


/* eslint-disable */

const path = require('path');
const webpack = require('webpack');

/*
 * SplitChunksPlugin is enabled by default and replaced
 * deprecated CommonsChunkPlugin. It automatically identifies modules which
 * should be splitted of chunk by heuristics using module duplication count and
 * module category (i. e. node_modules). And splits the chunks…
 *
 * It is safe to remove "splitChunks" from the generated configuration
 * and was added as an educational example.
 *
* https://webpack.js.org/plugins/split-chunks-plugin/
 *
 */

const HtmlWebpackPlugin = require('html-webpack-plugin');

/*
 * We've enabled HtmlWebpackPlugin for you! This generates a html
 * page for you when you compile webpack, which will make you start
 * developing and prototyping faster.
 *
* https://github.com/jantimon/html-webpack-plugin
 *
 */

module.exports = {
    mode: 'development',
    entry: './src/index.js',

    output: {
        filename: '[name].[chunkhash].js',
        path: path.resolve(__dirname, 'dist')
    },

    plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],

    module: {
        rules: [
            {
                test: /.(js|jsx)$/,
                include: [path.resolve(__dirname, 'src')],
                loader: 'babel-loader',

                options: {
                    plugins: ['syntax-dynamic-import'],

                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                modules: false
                            }
                        ]
                    ]
                }
            }
        ]
    },

    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    priority: -10,
                    test: /[\\/]node_modules[\\/]/
                }
            },

            chunks: 'async',
            minChunks: 1,
            minSize: 30000,
            name: true
        }
    },

    devServer: {
        open: true
    }
};

No entiendo todos los detalles de este archivo, pero se pueden observar algunos puntos:

  • línea 1: el archivo no contiene código ES6. Por lo tanto, [Eslint] reporta errores que se remontan hasta la raíz del proyecto [javascript]. Esto es molesto. Para evitar que ESLint analice un archivo, basta con comentar la línea 1;
  • línea 31: trabajamos en modo [développement];
  • línea 32: el script de entrada aquí es [src/index.js]. Tendremos que cambiar esto;
  • línea 36: la carpeta donde se guardarán los productos de [webpack] será la carpeta [dist];
  • línea 46: vemos que [webpack] utiliza [babel-loader], una de las dependencias que hemos instalado;
  • línea 54: vemos que [webpack] utiliza [@babel-preset/env], una de las dependencias que hemos instalado;

La inicialización de [webpack] ha modificado el archivo [package.json] (solicita autorización):


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "regenerator-runtime": "^0.13.3"
  }
}
  • línea 4: ha sido modificada;
  • líneas 8-9, 18-19: se han agregado;
  • línea 8: la tarea [npm], que permite compilar el proyecto;
  • línea 9: la tarea [npm], que permite ejecutarlo;
  • línea 18: ?
  • línea 19: permite generar un archivo [dist/index.html] que incorpora automáticamente el script [dist/main.js] generado por [webpack], y es este el que se utiliza cuando se ejecuta el proyecto;

Por último, la configuración de [webpack] generó un archivo [src/index.js]:

Image

El contenido de [index.js] es el siguiente (septiembre de 2019):


console.log("Hello World from your main file!");

14.4.4. Compilación y ejecución del proyecto

El archivo [package.json] tiene tres tareas [npm]:


"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
},

Estas tareas están incluidas en [VSCode], que las propone para su ejecución:

Image

  • en [1-3], se compila el proyecto;
  • en [4]: el proyecto se compila en [dist/main.hash.js] y se crea una página [dist/index.html];

La página [index.html] generada es la siguiente:


<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="main.87afc226fd6d648e7dea.js"></script></body>
</html>

Por lo tanto, esta página simplemente encapsula el archivo [main.hash.js] generado por [webpack].

El proyecto se ejecuta mediante la tarea [start]:

Image

La página [dist/index.html] se carga entonces en un servidor, que forma parte de la suite [webpack], que opera en el puerto 8080 de la máquina local y se muestra en el navegador predeterminado de la máquina:

Image

  • en [2], el puerto de servicio del servidor web de [webpack];
  • en [3], el cuerpo de la página [dist/index.html] está vacío;
  • en [4], la pestaña [console] de las herramientas de desarrollo del navegador, en este caso Firefox (F12);
  • en [5], el resultado de la ejecución del archivo [src/index.js]. Recordemos que el contenido de este era el siguiente:
console.log("Hello World from your main file!");

Ahora, cambiemos ese contenido por la siguiente línea:

console.log("Bonjour le monde");

Automáticamente (sin necesidad de recompilar), se generan nuevos archivos [main.js, index.html] y el nuevo archivo [index.html] se carga en el navegador:

Image

No es necesario ejecutar la tarea [build] antes de la tarea [start]: esta última compila primero el proyecto. No almacena los resultados de esta compilación en la carpeta [dist]. Para darse cuenta de ello, basta con eliminar esa carpeta. Entonces se verá que la tarea [start] compila y ejecuta el proyecto sin crear la carpeta [dist]. Parece que almacena sus productos [index.html, main.hash.js] en una carpeta específica de [webpackdev-server]. Este comportamiento es suficiente para nuestras pruebas.

Cuando se inicia el servidor de desarrollo, cualquier modificación guardada en uno de los archivos del proyecto provoca una recompilación. Por esta razón, desactivamos el modo [Auto Save] de [VSCode]. De hecho, no queremos que se recompile cada vez que se escriben caracteres en uno de los archivos del proyecto. Solo queremos que se recompile al guardar los cambios:

Image

  • en [2], la opción [Auto Save] no debe estar marcada;

14.4.5. Pruebas del cliente JavaScript del servidor de cálculo de impuestos

Para probar el cliente de JavaScript del servidor de cálculo de impuestos, hay que designar [main.js] [1] como el punto de entrada del proyecto en el archivo [webpack.config.js] [2-3]:

Image

No olvidemos que el script [main.js] debe incluir dos importaciones adicionales con respecto a su versión en [Client http 2]:

Image

Además, hemos modificado ligeramente el código para manejar los errores que pueda enviar el servidor:


// importaciones
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// importaciones
import Dao from './Dao2';
import Métier from './Métier';

// función asíncrona [main]
async function main() {
  // configuración de axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/versión-14';
  // instanciación de la capa [dao]
  const dao = new Dao(axios);
  // solicitudes HTTP
  let taxAdminData;
  try {
    // inicialización de sesión
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    if (response.état != 700) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // autenticación
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    if (response.état != 200) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // datos fiscales
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    if (response.état != 1000) {
      throw new Error(JSON.stringify(response.réponse));
    }
    taxAdminData = response.réponse;
  } catch (error) {
    // se registra el error
    console.log("erreur=", error.message);
    // fin
    return;
  }

  // instanciación de la capa [métier]
  const métier = new Métier(taxAdminData);

  // cálculos de impuestos
  log("-----------calculer-impot x 3");
  const simulations = [];
  simulations.push(métier.calculerImpot("oui", 2, 45000));
  simulations.push(métier.calculerImpot("non", 2, 45000));
  simulations.push(métier.calculerImpot("non", 1, 30000));
  // lista de simulaciones
  log("-----------liste-des-simulations");
  log(simulations);
  // eliminación de una simulación
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

// registro jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// ejecución
main();

Comentarios

  • En las líneas [24-26], [31-33] y [38-40], se verifica el código [response.état] enviado en la respuesta jSON del servidor. Si este código indica un error, se lanza una excepción con el mensaje de error «jSON» de la respuesta del servidor [response.réponse];

Una vez hecho esto, ejecutamos el proyecto [5-6].

A continuación, se genera la página [index.html] y se carga en el navegador:

Image

  • en [7], vemos que la acción [init-session] no pudo completarse debido a un problema [CORS] (Cross-Origin Resource Sharing);

El problema CORS se debe a la relación cliente/servidor:

  • nuestro cliente de JavaScript se descargó en la máquina [http://localhost:8080];
  • el servidor de cálculo de impuestos se ejecuta en la máquina [http://localhost:80];
  • por lo tanto, el cliente y el servidor no se encuentran en los mismos dominios (mismo equipo, pero no el mismo puerto);
  • el navegador que ejecuta el cliente JavaScript cargado desde la máquina [http://localhost:8080] bloquea cualquier solicitud que no tenga como destino a [http://localhost:80]. Se trata de una medida de seguridad. Por lo tanto, bloquea la solicitud del cliente al servidor que opera en la máquina [http://localhost:80];

De hecho, el navegador no bloquea la solicitud por completo. En realidad, espera a que el servidor le «indique» que acepta solicitudes entre dominios. Si obtiene esta autorización, el navegador transmitirá entonces la solicitud entre dominios.

El servidor otorga su autorización enviando encabezados HTTP específicos:

1
2
3
4
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Headers: Accept, Content-Type
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Credentials: true
  • línea 1: el cliente de JavaScript opera en el dominio [http://localhost:8080]. El servidor debe responder explícitamente que acepta este dominio;
  • línea 2: el cliente de JavaScript utilizará en sus solicitudes los encabezados HTTP y [Accept, Content-Type]:
    • [Accept]: este encabezado se envía en todas las solicitudes;
    • [Content-Type]: este encabezado se utiliza en las operaciones POST para indicar el tipo de los parámetros de POST;

El servidor debe aceptar explícitamente estos dos encabezados HTTP;

  • línea 3: el cliente de JavaScript utilizará las solicitudes GET y POST. El servidor debe aceptar explícitamente estos dos tipos de solicitudes;
  • línea 4: el cliente de JavaScript enviará cookies de sesión. El servidor las acepta con el encabezado de la línea 4;

Por lo tanto, debemos modificar el servidor. Lo hacemos en [Netbeans]. El problema de CORS es un problema que solo se presenta en modo de desarrollo. En producción, el cliente y el servidor funcionarán en el mismo dominio [http://localhost:80] y no habrá ningún problema CORS. Por lo tanto, necesitamos una forma de autorizar o no las solicitudes CORS mediante la configuración del servidor.

Image

Los cambios en el servidor se realizan en tres lugares:

  • [1, 4]: en el archivo de configuración [config.json] para incluir un valor booleano que controle si se aceptan o no las solicitudes entre dominios;
  • [2]: en la clase [ParentResponse] que envía la respuesta al cliente JavaScript. Es esta clase la que enviará los encabezados CORS que espera el navegador del cliente;
  • [3]: en las clases [HtmlResponse, JsonResponse, XmlResponse] que generan las respuestas para las sesiones [html, json, xml], respectivamente. Estas clases deben pasar a su clase padre [2] el valor booleano [corsAllowed] que se encuentra en [4]. Esto se realiza en [5], pasando la matriz de imágenes del archivo jSON a [2];

La clase [ParentResponse] [2] evoluciona de la siguiente manera:


<?php

namespace Application;

// dependencias de Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ParentResponse {

  // int $statusCode: el código HTTP del estado de la respuesta
  // cadena $content: el cuerpo de la respuesta que se enviará
  // según el caso, es una cadena JSON, XML, HTML
  // matriz $headers: los encabezados HTTP que se deben agregar a la respuesta

  public function sendResponse(
    Request $request,
    int $statusCode,
    string $content,
    array $headers,
    array $config): void {

    // preparación de la respuesta de texto del servidor
    $response = new Response();
    $response->setCharset("utf-8");
    // código de estado
    $response->setStatusCode($statusCode);
    // encabezados para las solicitudes entre dominios
    if ($config['corsAllowed']) {
      $origin = $request->headers->get("origin");
      if (strpos($origin, "http://localhost") === 0) {
        $headers = array_merge($headers,
          ["Access-Control-Allow-Origin" => $origin,
            "Access-Control-Allow-Headers" => "Accept, Content-Type",
            "Access-Control-Allow-Methods" => "GET, POST",
            "Access-Control-Allow-Credentials" => "true"
        ]);
      }
    }
    foreach ($headers as $text => $value) {
      $response->headers->set($text, $value);
    }
    // caso particular del método [OPTIONS]
    // en este caso, solo son importantes los encabezados
    $method = strtolower($request->getMethod());
    if ($method === "options") {
      $content = "";
      $response->setStatusCode(Response::HTTP_OK);
    }
    // se envía la respuesta
    $response->setContent($content);
    $response->send();
  }

}
  • línea 29: se verifica si se deben gestionar las solicitudes entre dominios. En caso afirmativo, se generarán los encabezados HTTP y CORS (líneas 33-37), incluso si la solicitud actual no es una solicitud entre dominios. En este último caso, los encabezados CORS serán innecesarios y el cliente no los utilizará;
  • línea 30: en una solicitud entre dominios, el navegador del cliente que consulta al servidor envía un encabezado HTTP [Origin: http://localhost:8080] (en el caso específico de nuestro cliente de JavaScript). Línea 30: se recupera este encabezado HTTP en la solicitud [$request];
  • línea 31: solo se aceptarán solicitudes entre dominios que provengan de la máquina [http://localhost]. Recordamos que estas solicitudes solo se realizan en el modo de desarrollo del proyecto;
  • líneas 32-36: se agregan los encabezados CORS a los encabezados ya presentes en la tabla [$headers];
  • líneas 45-49: la forma en que el navegador del cliente solicita las autorizaciones CORS puede variar según el cliente que se ejecute. En ocasiones, el navegador del cliente solicita estas autorizaciones con un comando HTTP [OPTIONS]. Esto es algo nuevo para nuestro servidor, que fue diseñado para atender únicamente los comandos [GET, POST]. En el caso de un comando [OPTIONS], el servidor genera actualmente una respuesta de error. En las líneas 46 a 49, corregimos esto en el último momento: si en la línea 46 observamos que el comando actual es un comando [OPTIONS], entonces generamos para el cliente:
    • líneas 47 y 51: una respuesta [$content] vacía;
    • línea 48: un código de estado 200 que indica que el comando se ejecutó correctamente. Lo único importante para este comando es el envío de los encabezados CORS de las líneas 33 a 36. Eso es lo que espera el navegador del cliente;

Una vez corregido el servidor de esta manera, el cliente de JavaScript funciona mejor, pero aparece un nuevo error:

Image

  • en [1], la sesión jSON se inicializa correctamente;
  • en [2], la acción [authentifier-utilisateur] falla: el servidor indica que no hay una sesión activa. Esto significa que el cliente de JavaScript no le devolvió correctamente la cookie de sesión que había enviado durante la acción [init-session];

Analicemos los intercambios de red que tuvieron lugar:

Image

  • en [4], la solicitud [init-session]. Esta se llevó a cabo correctamente con un código 200 como estado de la respuesta;
  • en [5], la solicitud [authentifier-utilisateur]. Esta falla con un código 400 (Bad Request) [6] como estado de la respuesta;

Si se examinan los encabezados HTTP y [7] de la solicitud [5], se puede observar que el cliente de JavaScript no envió los encabezados HTTP y [Cookie], lo que le habría permitido devolver la cookie de sesión enviada inicialmente por el servidor. Por eso, este último indica que no hay sesión.

Para que el cliente envíe la cookie de sesión, es necesario agregar una configuración al objeto [axios]:


// importaciones
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// importaciones
import Dao from './Dao2';
import Métier from './Métier';

// función asíncrona [main]
async function main() {
  // configuración de Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/versión-14';
  axios.defaults.withCredentials = true;
  // instanciación de la capa [dao]
  const dao = new Dao(axios);
  // solicitudes HTTP
  let taxAdminData;
...

La línea 15 indica que las cookies se incluyan en los encabezados HTTP de la solicitud [axios]. Cabe señalar que esto no había sido necesario en el entorno [node.js]. Por lo tanto, existen diferencias de código entre los dos entornos.

Una vez corregido este error, el cliente de JavaScript funciona con normalidad:

Image

Image

14.5. Mejora del cliente HTTP 3

Cuando la clase [Dao2] anterior se ejecuta en un navegador, no es necesario gestionar la cookie de sesión. De hecho, es el navegador que aloja la capa [dao] el que gestiona la cookie de sesión: devuelve automáticamente cualquier cookie que el servidor le envíe. Por lo tanto, la clase [Dao2] puede reescribirse como la siguiente clase [Dao3]:


"use strict";

// importaciones
import qs from "qs";

class Dao3 {
  // constructor
  constructor(axios) {
    this.axios = axios;
  }

  // inicialización de sesión
  async initSession() {
    // opciones de la solicitud HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parámetros de la consulta URL
      params: {
        action: "init-session",
        type: "json"
      }
    };
    // ejecución de la consulta HTTP
    return await this.getRemoteData(options);
  }

  async authentifierUtilisateur(user, password) {
    // opciones de la consulta HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        "Content-type": "application/x-www-form-urlencoded"
      },
      // cuerpo de POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parámetros del URL
      params: {
        action: "authentifier-utilisateur"
      }
    };
    // ejecución de la consulta HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // opciones de la consulta HHTP  [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // parámetros de la consulta URL
      params: {
        action: "get-admindata"
      }
    };
    // ejecución de la consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  async getRemoteData(options) {
    // ejecución de la consulta HTTP
    let response;
    try {
      // consulta asíncrona
      response = await this.axios.request("main.php", options);
    } catch (error) {
      // el parámetro [error] es una instancia de excepción; puede presentar diversas formas
      if (error.response) {
        // la respuesta del servidor se encuentra en [error.response]
        response = error.response;
      } else {
        // se vuelve a generar el error
        throw error;
      }
    }
    // la respuesta es el conjunto completo de la respuesta HTTP del servidor (encabezados HTTP + la respuesta en sí)
    // la respuesta del servidor se encuentra en [response.data]
    return response.data;
  }
}

// exportación de la clase
export default Dao3;

Todo lo relacionado con la gestión de la cookie de administración ha desaparecido.

Modificamos el proyecto anterior de la siguiente manera:

Image

En la carpeta [src], hemos agregado dos archivos:

  • la clase [Dao3] que acabamos de presentar;
  • el archivo [main3], encargado de iniciar la nueva versión;

El archivo [main3] sigue siendo idéntico al archivo [main] de la versión anterior, pero ahora utiliza la clase [Dao3]:


// importaciones
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";

// importaciones
import Dao from "./Dao3";
import Métier from "./Métier";

// función asíncrona [main]
async function main() {
  // configuración de Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL =
    "http://localhost/php7/scripts-web/impots/version-14";
  axios.defaults.withCredentials = true;
  // instanciación de la capa [dao]
  const dao = new Dao(axios);
  // solicitudes HTTP
  ...
}

// registro jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// ejecución
main();

El archivo [webpack.config] se ha modificado para que ahora ejecute el script [main3]:


/* eslint-disable */

const path = require("path");
const webpack = require("webpack");

/*
 * SplitChunksPlugin is enabled by default and replaced
 * deprecated CommonsChunkPlugin. It automatically identifies modules which
 * should be splitted of chunk by heuristics using module duplication count and
 * module category (i. e. node_modules). And splits the chunks…
 *
 * It is safe to remove "splitChunks" from the generated configuration
 * and was added as an educational example.
 *
* https://webpack.js.org/plugins/split-chunks-plugin/
 *
 */

const HtmlWebpackPlugin = require("html-webpack-plugin");

/*
 * We've enabled HtmlWebpackPlugin for you! This generates a html
 * page for you when you compile webpack, which will make you start
 * developing and prototyping faster.
 *
* https://github.com/jantimon/html-webpack-plugin
 *
 */

module.exports = {
  mode: "development",
  //entrada: "./src/mainjs",
  entry: "./src/main3.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },

  plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};

Una vez hecho esto, se ejecuta el proyecto después de iniciar el servidor de cálculo de impuestos:

Image

Los resultados obtenidos en la consola del navegador son idénticos a los de la versión anterior.

14.6. Conclusion

Ahora contamos con todas las herramientas necesarias para desarrollar el código JavaScript de una aplicación web. Podemos:

  • utilizar el código ECMAScript más reciente;
  • probar elementos aislados de este código en un entorno [node.js] más sencillo para la depuración y las pruebas;
  • llevar luego este código a un navegador gracias a las herramientas [babel] y [webpack];