Skip to content

14. Clientes HTTP JavaScript do serviço de cálculo de impostos

14.1. Introdução

Aqui, propomos escrever um cliente [Node.js] para a versão 14 do serviço de cálculo de impostos. A arquitetura cliente/servidor será a seguinte:

Image

Iremos analisar duas versões do cliente:

  • A versão 1 do cliente terá a seguinte estrutura em camadas [main, dao]:

Image

  • A versão 2 do cliente terá uma estrutura [main, business logic, DAO]. A camada [business logic] do servidor será transferida para o cliente:

Image

14.2. Cliente HTTP 1

Image

Como mencionado, o cliente HTTP 1 implementa a seguinte arquitetura cliente/servidor:

Image

Iremos implementar:

  • a camada [DAO] como uma classe;
  • a camada [main] como um script que utiliza esta classe;

14.2.1. A camada [dao]

A camada [dao] será implementada pela seguinte classe [Dao1.js]:


'use strict';
 
// imports
import qs from 'qs'
 
class Dao1 {
 
  // manufacturer
  constructor(axios) {
    // axios library for queries HTTP
    this.axios = axios;
    // session cookie
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }
 
  // init session
  async  initSession() {
    // query options HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // URL parameters
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // execute query HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // query options HHTP [post /main.php?action=authenticate-user]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // body of POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // URL parameters
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // execute query HTTP
    return await this.getRemoteData(options);
  }
 
  // tAX CALCULATION
  async  calculerImpot(marié, enfants, salaire) {
    // query options HHTP [post /main.php?action=calculate-tax]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // body of POST [married, children, salary]
      data: qs.stringify({
        marié: marié,
        enfants: enfants,
        salaire: salaire
      }),
      // URL parameters
      params: {
        action: 'calculer-impot'
      }
    };
    // execute query HTTP
    const data = await this.getRemoteData(options);
    // result
    return data;
  }
 
  // list of simulations
  async  listeSimulations() {
    // query options HHTP [get /main.php?action=lister-simulations]
    const options = {
      method: "GET",
      // URL parameters
      params: {
        action: 'lister-simulations'
      },
    };
    // execute query HTTP
    const data = await this.getRemoteData(options);
    // result
    return data;
  }
 
  // list of simulations
  async  supprimerSimulation(index) {
    // query options HHTP [get /main.php?action=suppress-simulation&number=index]
    const options = {
      method: "GET",
      // URL parameters
      params: {
        action: 'supprimer-simulation',
        numéro: index
      },
    };
    // execute query HTTP
    const data = await this.getRemoteData(options);
    // result
    return data;
  }
 
  async  getRemoteData(options) {
    // for the session cookie
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // execute query HTTP
    let response;
    try {
      // asynchronous request
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // the [error] parameter is an exception instance - it can take various forms
      if (error.response) {
        // the server response is in [error.response]
        response = error.response;
      } else {
        // error restart
        throw error;
      }
    }
    // response is the entire HTTP response from the server (HTTP headers + response itself)
    // retrieve the session cookie if it exists
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie is an array
      // look for the session cookie in this table
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // look for the session cookie
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // the session cookie is stored
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // we found
          trouvé = true;
        } else {
          // next item
          i++;
        }
      }
    }
    // the server response is in [response.data]
    return response.data;
  }
}
 
// class export
export default Dao1;
  • Aqui estamos a utilizar o que aprendemos na secção em destaque, onde apresentámos a biblioteca [axios], que nos permite fazer pedidos HTTP tanto no [node.js] como num navegador. Iremos analisar especificamente o script na secção em destaque;
  • linhas 9–15: o construtor da classe. Esta classe terá três propriedades:
    • [axios]: o objeto [axios] utilizado para efetuar pedidos HTTP. Este é passado pelo código de chamada;
    • [sessionCookieName]: dependendo do servidor, o cookie de sessão tem nomes diferentes. Aqui, é [PHPSESSID];
    • [sessionCookie]: o cookie de sessão enviado pelo servidor e armazenado pelo cliente;
  • linhas 53–76: a função assíncrona [calculateTax] faz a solicitação [post /main.php?action=calculate-tax] enviando os parâmetros [married, children, salary]. Ela retorna a string JSON enviada pelo servidor como um objeto JavaScript;
  • linhas 79–92: a função assíncrona [listSimulations] faz a solicitação [get /main.php?action=list-simulations]. Ela retorna a string JSON enviada pelo servidor como um objeto JavaScript;
  • linhas 95–109: A função assíncrona [deleteSimulation] faz a solicitação [get /main.php?action=delete-simulation&number=index]. Ela retorna a string JSON enviada pelo servidor como um objeto JavaScript;
  • linha 121: a notação [this.axios] é utilizada porque, neste caso, o objeto [axios] passado ao construtor foi armazenado na propriedade [this.axios];
  • linha 161: a classe [Dao1] é exportada para que possa ser utilizada;

14.2.2. O script [main1.js]

O script [main1.js] efetua uma série de chamadas ao servidor utilizando a classe [Dao1]:

  • inicialização de uma sessão JSON;
  • autenticação com [admin, admin];
  • solicita três cálculos de impostos;
  • solicita a lista de simulações;
  • elimina uma delas;

O código é o seguinte:


// import axios
import axios from 'axios';
// dao1 class import
import Dao from './Dao1';
 
// asynchronous function [main]
async function main() {
  // axios configuration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // layer instantiation [dao]
  const dao = new Dao(axios);
  // using the [dao] layer
  try {
    // init session
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // authentication
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // tax calculations
    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);
    // list of simulations
    log("-----------liste-des-simulations");
    response = await dao.listeSimulations();
    log(response);
    // deleting a simulation
    log("-----------suppression simulation n° 1");
    response = await dao.supprimerSimulation(1);
    log(response);
  } catch (error) {
    // we log the error
    console.log("erreur=", error.message);
  }
}
 
// log jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}
 
// execution
main();

Comentários

  • linha 2: importar a biblioteca [axios];
  • linha 4: importar a classe [Dao];
  • linha 7: a função [main] que comunica com o servidor é assíncrona;
  • linhas 9-10: configuração padrão para os pedidos HTTP a enviar para o servidor:
    • linha 9: [timeout] de 2 segundos;
    • linha 10: todas as URLs têm como prefixo a URL base da versão 14 do servidor de cálculo de impostos;
  • linha 12: a camada [Dao] está construída. Agora pode ser utilizada;
  • linhas 46–48: a função [log] é utilizada para apresentar a cadeia JSON de um objeto JavaScript de forma formatada: verticalmente com recuo de dois espaços (3.º parâmetro);
  • linhas 15–18: inicialização da sessão JSON;
  • linhas 19–22: autenticação;
  • linhas 23–30: são solicitados três cálculos de impostos em paralelo. Graças a [await Promise.all], a execução fica bloqueada até que os três resultados tenham sido obtidos;
  • linhas 31–34: lista de simulações;
  • linhas 35–38: eliminação de uma simulação;
  • linhas 39–42: tratamento de eventuais exceções;

Os resultados da execução são os seguintes:


[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

A arquitetura do cliente HTTP2 é a seguinte:

Image

Transferimos a camada [business] do servidor para o cliente JavaScript. Ao contrário do que fizemos no curso de PHP7, a camada [main] não precisará passar pela camada [business] para chegar à camada [DAO]. Utilizaremos estas duas camadas como componentes especializados:

  • a camada [main] passa pela camada [DAO] sempre que precisa de dados que se encontram no servidor;
  • a camada [main] solicita à camada [business] que execute os cálculos de impostos;
  • a camada [business] é independente da camada [DAO] e nunca a invoca;

14.3.1. A classe [Business] em JavaScript

A essência da classe [Business] em PHP foi descrita no artigo em link. Trata-se de um trecho de código bastante complexo que estamos a recordar aqui, não para o explicar, mas para o podermos traduzir para JavaScript:


<?php
 
// namespace
namespace Application;
 
class Metier implements InterfaceMetier {
  // dao layer
  private $dao;
  // tax administration data
  private $taxAdminData;
 
  //---------------------------------------------
  // setter couche [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }
 
  public function __construct(InterfaceDao $dao) {
    // a reference is stored on the [dao] layer
    $this->dao = $dao;
    // recover data for tax calculation
    // method [getTaxAdminData] may throw a ExceptionImpots exception
    // we then let it go back to the calling code
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }
 
// tAX CALCULATION
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié : yes, no
    // $enfants : number of children
    // $salaire: annual salary
    // $this->taxAdminData: tax administration data
    //
    // we check that we have the correct data from the tax authorities
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // tax calculation with children
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // tax calculation without children
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // application of the family allowance ceiling
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euros for the first 2 children
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euros for the first 2 children, double for subsequent children
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // we take the highest tax
    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"];
    }
    // calculation of any discount
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // calculation of any tax reduction
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // result
    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é : yes, no
    // $enfants : number of children
    // $salaire: annual salary
    // $this->taxAdminData: tax administration data
    //
    // number of shares
    $marié = strtolower($marié);
    if ($marié === "oui") {
      $nbParts = $enfants / 2 + 2;
    } else {
      $nbParts = $enfants / 2 + 1;
    }
    // 1 part per child from the 3rd
    if ($enfants >= 3) {
      // an additional half share for each child from the 3rd onwards
      $nbParts += 0.5 * ($enfants - 2);
    }
    // taxable income
    $revenuImposable = $this->getRevenuImposable($salaire);
    // surcharge
    $surcôte = floor($revenuImposable - 0.9 * $salaire);
    // for rounding problems
    if ($surcôte < 0) {
      $surcôte = 0;
    }
    // family quotient
    $quotient = $revenuImposable / $nbParts;
    // tAX CALCULATION
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // is set at the end of the limit array to stop the following loop
    $limites[count($limites) - 1] = $quotient;
    // tax rate search
    $i = 0;
    while ($quotient > $limites[$i]) {
      $i++;
    }
    // because $quotient has been placed at the end of the $limites array, the previous loop
    // cannot exceed the table $limites
    // now we can calculate the tax
    $impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
    // result
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }
 
  // revenuImposable=annualwage-discount
  // the allowance has a minimum and a maximum
  private function getRevenuImposable(float $salaire): float {
    // 10% salary deduction
    $abattement = 0.1 * $salaire;
    // this allowance cannot exceed $this->taxAdminData->getAbattementDixPourCentMax()
    if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMax();
    }
    // the allowance cannot be less than $this->taxAdminData->getAbattementDixPourcentMin()
    if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMin();
    }
    // taxable income
    $revenuImposable = $salaire - $abattement;
    // result
    return floor($revenuImposable);
  }
 
// calculates any discount
  private function getDecôte(string $marié, float $salaire, float $impots): float {
    // at the outset, a zero discount
    $décôte = 0;
    // maximum tax amount to qualify for discount
    $plafondImpôtPourDécôte = $marié === "oui" ?
      $this->taxAdminData->getPlafondImpotCouplePourDecote() :
      $this->taxAdminData->getPlafondImpotCelibatairePourDecote();
    if ($impots < $plafondImpôtPourDécôte) {
      // maximum discount
      $plafondDécôte = $marié === "oui" ?
        $this->taxAdminData->getPlafondDecoteCouple() :
        $this->taxAdminData->getPlafondDecoteCelibataire();
      // theoretical discount
      $décôte = $plafondDécôte - 0.75 * $impots;
      // the discount cannot exceed the amount of tax due
      if ($décôte > $impots) {
        $décôte = $impots;
      }
      // no discount <0
      if ($décôte < 0) {
        $décôte = 0;
      }
    }
    // result
    return ceil($décôte);
  }
 
// calculates any reduction
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    // the income ceiling to qualify for the 20% reduction
    $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();
    }
    // taxable income
    $revenuImposable = $this->getRevenuImposable($salaire);
    // reduction
    $réduction = 0;
    if ($revenuImposable < $plafondRevenuPourRéduction) {
      // 20% discount
      $réduction = 0.2 * $impots;
    }
    // result
    return ceil($réduction);
  }
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // we let the exceptions coming from the [dao] layer flow upwards
    // retrieve taxpayer data
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // results table
    $results = [];
    // we exploit them
    foreach ($taxPayersData as $taxPayerData) {
      // tax calculation
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // complete [$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"]);
      // put the result in the results table
      $results [] = $taxPayerData;
    }
    // recording results
    $this->dao->saveResults($resultsFileName, $results);
  }
 
}
  • linhas 19–26: o construtor da classe PHP. Como dissemos que estávamos a construir uma camada [de negócios] independente da camada [DAO], faremos duas alterações a este construtor em JavaScript:
    • não receberá uma instância da camada [DAO] (já não precisa de uma);
    • não solicitará dados fiscais da administração [taxAdminData] da camada [dao]: o código de chamada passará esses dados para o construtor;
  • Linhas 197–122: Não iremos implementar o método [executeBatchImpots], cujo objetivo final era guardar os resultados da simulação num ficheiro de texto. Queremos código que funcione tanto em [node.js] como num navegador. No entanto, não é possível guardar dados no sistema de ficheiros da máquina que executa o navegador do cliente;

Dadas estas restrições, o código para a classe [Métier] em JavaScript é o seguinte:


'use strict';
 
// job class
class Métier {
 
  // manufacturer
  constructor(taxAdmindata) {
    // this.taxAdminData: tax administration data
    this.taxAdminData = taxAdmindata;
  }
 
  // tAX CALCULATION
  // --------------------------------------------------------------------------
  calculerImpot(marié, enfants, salaire) {
    // married: yes, no
    // children: number of children
    // salary: annual salary
    // this.taxAdminData: tax administration data
    //
    // tax calculation with children
    const result1 = this.calculerImpot2(marié, enfants, salaire);
    const impot1 = result1["impôt"];
    // tax calculation without children
    let result2, impot2, plafondDemiPart;
    if (enfants !== 0) {
      result2 = this.calculerImpot2(marié, 0, salaire);
      impot2 = result2["impôt"];
      // application of the family allowance ceiling
      plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
      if (enfants < 3) {
        // PLAFOND_QF_DEMI_PART euros for the first 2 children
        impot2 = impot2 - enfants * plafondDemiPart;
      } else {
        // PLAFOND_QF_DEMI_PART euros for the first 2 children, double for subsequent children
        impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
      }
    } else {
      // no tax recalculation
      impot2 = impot1;
      result2 = result1;
    }
    // we take the highest tax in [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"];
    }
    // calculation of any discount
    const décôte = this.getDecôte(marié, impot);
    impot -= décôte;
    // calculation of any tax reduction
    const réduction = this.getRéduction(marié, salaire, enfants, impot);
    impot -= réduction;
    // result
    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) {
    // married: yes, no
    // children: number of children
    // salary: annual salary
    // this->taxAdminData: tax administration data
    //
    // number of shares
    marié = marié.toLowerCase();
    let nbParts;
    if (marié === "oui") {
      nbParts = enfants / 2 + 2;
    } else {
      nbParts = enfants / 2 + 1;
    }
    // 1 part per child from the 3rd
    if (enfants >= 3) {
      // an additional half share for each child from the 3rd onwards
      nbParts += 0.5 * (enfants - 2);
    }
    // taxable income
    const revenuImposable = this.getRevenuImposable(salaire);
    // surcharge
    let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
    // for rounding problems
    if (surcôte < 0) {
      surcôte = 0;
    }
    // family quotient
    const quotient = revenuImposable / nbParts;
    // tAX CALCULATION
    const limites = this.taxAdminData.limites;
    const coeffR = this.taxAdminData.coeffR;
    const coeffN = this.taxAdminData.coeffN;
    // is set at the end of the limit table to stop the following loop
    limites[limites.length - 1] = quotient;
    // tax rate search
    let i = 0;
    while (quotient > limites[i]) {
      i++;
    }
    // because we've placed quotient at the end of the limit array, the previous loop
    // cannot go beyond the limit table
    // now we can calculate the tax
    const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
    // result
    return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
  }
 
  // revenuImposable=annualwage-discount
  // the allowance has a minimum and a maximum
  getRevenuImposable(salaire) {
    // 10% salary deduction
    let abattement = 0.1 * salaire;
    // this allowance cannot exceed taxAdminData.getAbattementDixPourCentMax()
    if (abattement > this.taxAdminData.abattementDixPourCentMax) {
      abattement = this.taxAdminData.abattementDixPourcentMax;
    }
    // the allowance cannot be less than taxAdminData.getAbattementDixPourcentMin()
    if (abattement < this.taxAdminData.abattementDixPourcentMin) {
      abattement = this.taxAdminData.abattementDixPourcentMin;
    }
    // taxable income
    const revenuImposable = salaire - abattement;
    // result
    return Math.floor(revenuImposable);
  }
 
  // calculates any discount
  getDecôte(marié, impots) {
    // at the outset, a zero discount
    let décôte = 0;
    // maximum tax amount to qualify for discount
    let plafondImpôtPourDécôte = marié === "oui" ?
      this.taxAdminData.plafondImpotCouplePourDecote :
      this.taxAdminData.plafondImpotCelibatairePourDecote;
    let plafondDécôte;
    if (impots < plafondImpôtPourDécôte) {
      // maximum discount
      plafondDécôte = marié === "oui" ?
        this.taxAdminData.plafondDecoteCouple :
        this.taxAdminData.plafondDecoteCelibataire;
      // theoretical discount
      décôte = plafondDécôte - 0.75 * impots;
      // the discount cannot exceed the amount of tax due
      if (décôte > impots) {
        décôte = impots;
      }
      // no discount <0
      if (décôte < 0) {
        décôte = 0;
      }
    }
    // result
    return Math.ceil(décôte);
  }
 
  // calculates any reduction
  getRéduction(marié, salaire, enfants, impots) {
    // the income ceiling to qualify for the 20% reduction
    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;
    }
    // taxable income
    const revenuImposable = this.getRevenuImposable(salaire);
    // reduction
    let réduction = 0;
    if (revenuImposable < plafondRevenuPourRéduction) {
      // 20% discount
      réduction = 0.2 * impots;
    }
    // result
    return Math.ceil(réduction);
  }
}
 
// class export
export default Métier;
  • O código JavaScript segue de perto o código PHP;
  • A classe [Business] é exportada, linha 187;

14.3.2. A classe JavaScript [Dao2]

Image

A classe [Dao2] implementa a camada [dao] do cliente JavaScript acima da seguinte forma:


'use strict';

// imports
import qs from 'qs'
 
class Dao2 {
 
  // manufacturer
  constructor(axios) {
    this.axios = axios;
    // session cookie
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }
 
  // init session
  async  initSession() {
    // query options HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // URL parameters
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // execute query HTTP
    return await this.getRemoteData(options);
  }
 
  async  authentifierUtilisateur(user, password) {
    // query options HHTP [post /main.php?action=authenticate-user]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // body of POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // URL parameters
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // execute query HTTP
    return await this.getRemoteData(options);
  }
 
  async getAdminData() {
    // query options HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // URL parameters
      params: {
        action: 'get-admindata'
      }
    };
    // execute query HTTP
    const data = await this.getRemoteData(options);
    // result
    return data;
  }
 
  async  getRemoteData(options) {
    // for the session cookie
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // execute query HTTP
    let response;
    try {
      // asynchronous request
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // the [error] parameter is an exception instance - it can take various forms
      if (error.response) {
        // the server response is in [error.response]
        response = error.response;
      } else {
        // error restart
        throw error;
      }
    }
    // response is the entire HTTP response from the server (HTTP headers + response itself)
    // retrieve the session cookie if it exists
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie is an array
      // look for the session cookie in this table
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // look for the session cookie
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // the session cookie is stored
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // we found
          trouvé = true;
        } else {
          // next item
          i++;
        }
      }
    }
    // the server response is in [response.data]
    return response.data;
  }
}
 
// class export
export default Dao2;

Comentários

  • A classe [Dao2] implementa apenas três das possíveis solicitações ao servidor de cálculo de impostos:
    • [init-session] (linhas 17–29): para inicializar a sessão JSON;
    • [authenticate-user] (linhas 31–50): para autenticar;
    • [get-admindata] (linhas 52–65): para recuperar os dados de administração fiscal necessários para realizar cálculos de impostos no lado do cliente;
  • linhas 52–65: introduzimos uma nova ação [get-admindata] no servidor. Esta ação não tinha sido implementada até agora. Estamos a fazê-lo agora.

14.3.3. Modificação do servidor de cálculo de impostos

O servidor de cálculo de impostos deve implementar uma nova ação. Faremos isso na versão 14 do servidor. A ação a ser implementada tem as seguintes características:

  • é solicitada por uma operação [get /main.php?action=get-admindata];
  • retorna a cadeia JSON de um objeto que encapsula os dados de administração fiscal;

Iremos rever como adicionar uma ação ao nosso servidor.

A modificação será feita no NetBeans:

Image

Em [2], modificamos o ficheiro [config.json] para adicionar a nova ação:


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

A modificação consiste em:

  • linha 67: adicionar a ação [get-admindata] e associá-la a um controlador;
  • linha 36: declarar este controlador na lista de classes a serem carregadas pela aplicação PHP;

O próximo passo é implementar o controlador [AdminDataController] [3]:


<?php
 
namespace Application;
 
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// layer alias [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
 
class AdminDataController implements InterfaceController {
 
  // $config is the application configuration
  // traitement d'une requête Request
  // session and can modify it
  // $infos is additional information specific to each controller
  // renders an array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {
 
    // you must have a single parameter GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      // we note the error
      $message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
      $état = 1001;
      // return result to main controller
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
 
    // we can work
    // Redis
    \Predis\Autoloader::register();
    try {
      // customer [predis]
      $redis = new \Predis\Client();
      // connect to the server to see if it's there
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // it didn't go well
      // return result with error to main controller
      $état = 1050;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }
 
    // data recovery from tax authorities
    // first search the cache [redis]
    if (!$redis->get("taxAdminData")) {
      try {
        // retrieve tax data from the database
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // taxAdminData
        $taxAdminData = $dao->getTaxAdminData();
        // put the recovered data into redis
        $redis->set("taxAdminData", $taxAdminData);
      } catch (\RuntimeException $ex) {
        // it didn't go well
        // return result with error to main controller
        $état = 1041;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // tax data are taken from the [redis] memory of the [application] scope
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      // we instantiate an object [TaxAdminData] from the previous attribute array
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
    }
 
    // return result to main controller
    $état = 1000;
    return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
  }
 
}

Comentários

  • linha 12: tal como os outros controladores do servidor, o [AdminDataController] implementa a interface [InterfaceController], que consiste no método [execute] nas linhas 19–79;
  • linha 78: tal como acontece com os outros controladores do servidor, o método [AdminDataController.execute] devolve uma matriz [$status, $status, [‘response’=>$response]] com:
    • [$status]: o código de estado da resposta HTTP;
    • [$état]: um código interno da aplicação que representa o estado do servidor após a execução do pedido do cliente;
    • [$response]: uma matriz que encapsula a resposta a ser enviada ao cliente. Aqui, esta matriz será posteriormente convertida numa cadeia JSON;
  • linhas 25–34: verificamos se a ação [get-admindata] do cliente está sintaticamente correta;
  • linhas 37–74: recuperamos um objeto [TaxAdminData] encontrado:
    • linhas 56–59: da base de dados, caso não tenha sido encontrado no cache [redis];
    • linhas 70–73: no cache [redis];

Este código foi retirado do controlador [CalculerImpotController] explicado no artigo em link. Na verdade, este controlador também precisava de recuperar o objeto [TaxAdminData] que encapsula os dados da administração fiscal.

Durante os testes do cliente JavaScript, o formato JSON de [TaxAdminData] causou problemas quando este objeto foi encontrado no cache [redis]. Para compreender porquê, vamos examinar como este objeto é armazenado no [redis]:

Image

Image

  • Em [5-7], vemos que os valores numéricos foram armazenados como cadeias de caracteres. O PHP lidou com isto porque o operador + em cálculos que envolvem números e cadeias de caracteres provoca implicitamente uma conversão de tipo de cadeia de caracteres para número. Mas o JavaScript faz o oposto: o operador + em cálculos que envolvem números e cadeias de caracteres provoca implicitamente uma conversão de tipo de número para cadeia de caracteres. Os cálculos na classe [Métier] do JavaScript estão, portanto, incorretos;

Para resolver este problema, modificamos o método [TaxAdminData.setFromArrayOfAttributes] utilizado na linha 71 do controlador para instanciar um objeto [TaxAdminData] (ver artigo) a partir da cadeia JSON encontrada no cache [redis]:


<?php
 
namespace Application;
 
class TaxAdminData extends BaseEntity {
  // tax brackets
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // tax calculation constants
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;
 
  // initialization
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // parent
    parent::setFromJsonFile($taxAdminDataFilename);
    // check attribute values
    $this->checkAttributes();
    // we return the object
    return $this;
  }
 
  protected function check($value): \stdClass {
    // $value is an array of string elements or a single element
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // transform the array of strings into an array of reals
    $newTableau = [];
    $result = new \stdClass();
    // table elements must be positive or zero decimal numbers
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // put the float in newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // we note the error
        $result->erreur = TRUE;
        // we leave
        return $result;
      }
    }
    // we return the result
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // a single value
      $result->value = $newTableau[0];
    } else {
      // a list of values
      $result->value = $newTableau;
    }
    return $result;
  }
 
  // initialization by an array of attributes
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // parent
    parent::setFromArrayOfAttributes($arrayOfAttributes);
    // check attribute values
    $this->checkAttributes();
    // we return the object
    return $this;
  }
 
  // checking attribute values
  protected function checkAttributes() {
    // check that attribute values are real >=0
    foreach ($this as $key => $value) {
      if (is_string($value)) {
        // $value must be a real number >=0 or an array of reals >=0
        $result = $this->check($value);
        // mistake?
        if ($result->erreur) {
          // throw an exception
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // we note the value
          $this->$key = $result->value;
        }
      }
    }
 
    // we return the object
    return $this;
  }
 
  // getters and setters
  ...
 
}

Comentários

  • linha 5: a classe [TaxAdminData] estende a classe [BaseEntity], que já possui o método [setFromArrayOfAttributes]. Como este método não é adequado, redefinimo-lo nas linhas 67–75;
  • linha 70: o método [setFromArrayOfAttributes] da classe pai é utilizado primeiro para inicializar os atributos da classe;
  • linha 72: o método [checkAttributes] verifica se os valores associados são, de facto, números. Se forem cadeias de caracteres, são convertidos em números;
  • linha 74: o objeto [$this] resultante é, então, um objeto com atributos que possuem valores numéricos;
  • linhas 78–93: o método [checkAttributes] verifica se os valores associados aos atributos do objeto são efetivamente numéricos;
  • linha 80: a lista de atributos é percorrida;
  • linha 81: se o valor de um atributo for do tipo [string];
  • linha 83: então verificamos se essa cadeia de caracteres representa um número;
  • linha 90: se for o caso, a string é convertida num número e atribuída ao atributo que está a ser testado;
  • linhas 85–86: caso contrário, é lançada uma exceção;
  • linhas 32–65: a função [check] faz um pouco mais do que o necessário. Ela lida tanto com matrizes como com valores únicos. No entanto, aqui é chamada apenas para verificar um valor do tipo [string]. Ela retorna um objeto com as propriedades [error, value], onde:
    • [error] é um booleano que indica se ocorreu ou não um erro;
    • [value] é o parâmetro [value] da linha 32, convertido num número ou numa matriz de números, conforme apropriado;

A classe [BaseEntity], que anteriormente tinha um atributo chamado [arrayOfAttributes], foi modificada para remover este atributo: estava a causar problemas com a cadeia JSON [TaxAdminData]. A classe foi reescrita da seguinte forma:


<?php
 
namespace Application;
 
class BaseEntity {
 
  // initialization from a JSON file
  public function setFromJsonFile(string $jsonFilename) {
    // retrieve the contents of the tax data file
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // mistake?
    if (!$fileContents) {
      // we note the error
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // retrieve the JSON code from the configuration file in an associative array
      $arrayOfAttributes = \json_decode($fileContents, true);
      // mistake?
      if ($arrayOfAttributes === FALSE) {
        // we note the error
        $erreur = TRUE;
        $message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // mistake?
    if ($erreur) {
      // throw an exception
      throw new ExceptionImpots($message);
    }
    // initialization of class attributes
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // we check the presence of all attributes
    $this->checkForAllAttributes($arrayOfAttributes);
    // we return the object
    return $this;
  }
 
  public function checkForAllAttributes($arrayOfAttributes) {
    // check that all keys have been initialized
    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) {
    // initialize certain class attributes (not necessarily all)
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // object is returned
    return $this;
  }
 
  // toString
  public function __toString() {
    // object attributes
    $arrayOfAttributes = \get_object_vars($this);
    // string jSON of object
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }
 
}

Comentários

  • linha 20: o atributo [$this→arrayOfAttributes] foi convertido numa variável que agora deve ser passada para o método [checkForAllAttributes] na linha 38, que anteriormente operava sobre o atributo [$this→arrayOfAttributes];

Devido a esta alteração em [BaseEntity], a classe [Database] também deve ser ligeiramente modificada:


<?php
 
namespace Application;
 
class Database extends BaseEntity {
  // attributes
  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
  // initialization
  public function setFromJsonFile(string $jsonFilename) {
    // parent
    parent::setFromJsonFile($jsonFilename);
    // object is returned
    return $this;
  }
 
  // getters and setters
  ...
}

Comentários

  • No código original, após a linha 30, era chamado o método [parent::checkForAllAttributes]. Isto já não é necessário, uma vez que agora é tratado automaticamente pelo método [parent::setFromJsonFile($jsonFilename)];

14.3.4. Testes de servidor [Postman]

O [Postman] foi apresentado no artigo em link.

Utilizamos os seguintes testes do Postman:

Image

Image

Image

O resultado JSON desta última solicitação é o seguinte:

Image

  • Em [5-8], pode ver-se que os atributos na cadeia JSON têm, de facto, valores numéricos (e não cadeias de caracteres). Este resultado permitirá que a classe JavaScript [Business] seja executada normalmente;

14.3.5. O script principal [main]

Image

O script principal [main] do cliente JavaScript é o seguinte:


// imports
import axios from 'axios';
 
// imports
import Dao from './Dao2';
import Métier from './Métier';
 
// asynchronous function [main]
async function main() {
  // axios configuration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // layer instantiation [dao]
  const dao = new Dao(axios);
  // requests HTTP
  let taxAdminData;
  try {
    // init session
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // authentication
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // tax information
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response.réponse;
  } catch (error) {
    // we log the error
    console.log("erreur=", error.message);
    // end
    return;
  }
 
  // instantiation layer [business]
  const métier = new Métier(taxAdminData);
 
  // tax calculations
  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));
  // list of simulations
  log("-----------liste-des-simulations");
  log(simulations);
  // deleting a simulation
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

Comentários

  • linhas 5-6: importações das classes [Dao] e [Business];
  • linha 9: a função assíncrona [main] que irá gerir a comunicação com o servidor utilizando a classe [Dao] e solicitar à classe [Business] que efetue os cálculos de impostos;
  • linhas 10-36: o script chama os métodos [initSession, authenticateUser, getAdminData] da camada [Dao] sequencialmente e de forma bloqueante;
  • linha 38: já não precisamos da camada [dao]. Temos todos os elementos necessários para executar a camada [business] do cliente JavaScript;
  • linhas 41–46: realizamos três cálculos de impostos e armazenamos os resultados numa matriz [simulations];
  • linha 49: exibimos a matriz de simulações;
  • linha 52: removemos uma delas;

Os resultados da execução do script principal são os seguintes:


[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

Nesta secção, portamos a aplicação [Cliente HTTP 2] para um navegador utilizando a seguinte arquitetura:

Image

O processo de portabilidade não é simples. Embora o [node.js] possa executar JavaScript ES6, isso geralmente não acontece nos navegadores. Por isso, temos de utilizar ferramentas que traduzam o código ES6 para código ES5, compreendido pelos navegadores modernos. Felizmente, estas ferramentas são poderosas e bastante fáceis de utilizar.

Aqui, seguimos o artigo [Como escrever código ES6 que seja seguro para executar no navegador - Web Developer's Journal].

Na pasta [client HTTP 3/src], colocámos os ficheiros [main.js, Métier.js, Dao2.js] da aplicação [Client Http 2] que acabámos de desenvolver.

14.4.1. Inicialização do projeto

Iremos trabalhar na pasta [client http 3]. Abrimos um terminal no [VSCode] e navegamos até esta pasta:

Image

Inicializamos este projeto com o comando [npm init] e aceitamos as respostas padrão às perguntas feitas:

Image

  • em [4-5], o ficheiro de configuração do projeto [package.json] gerado a partir das várias respostas fornecidas;

14.4.2. Instalação das dependências do projeto

Iremos instalar as seguintes dependências:

  • [@babel/core]: o núcleo da ferramenta [Babel] [https://babeljs.io], que transforma código ES 2015+ em código executável tanto em navegadores modernos como em navegadores mais antigos;
  • [@babel/preset-env]: faz parte do conjunto de ferramentas do Babel. É executado antes da transpilagem de ES6 para ES5;
  • [babel-loader]: esta dependência permite que a ferramenta [webpack] recorra à ferramenta [Babel];
  • [webpack]: o orquestrador. É o [webpack] que invoca o Babel para transcompilar código ES6 para ES5 e, em seguida, reúne todos os ficheiros resultantes num único ficheiro;
  • [webpack-cli]: necessário para o [webpack];
  • [@webpack-cli/init]: utilizado para configurar o [webpack];
  • [webpack-dev-server]: fornece um servidor web de desenvolvimento que funciona por predefinição na porta 8080. Quando os ficheiros de origem são modificados, recarrega automaticamente a aplicação web;

As dependências do projeto são instaladas da seguinte forma num terminal [VSCode]:

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

Image

Após a instalação das dependências, o ficheiro [package.json] ficou alterado da seguinte forma:


{
  "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"
  }
}
  • linhas 12–19: as dependências do projeto são [devDependencies]: precisamos delas durante a fase de desenvolvimento, mas não na fase de produção. Em produção, é utilizado o ficheiro [dist/main.js]. Este está escrito em ES5 e já não requer ferramentas para transpilá-lo de ES6 para ES5;

Precisamos de adicionar duas dependências ao projeto:

  • [core-js]: contém «polyfills» para o ECMAScript 2019. Um polyfill permite que código recente, como o ECMAScript 2019 (setembro de 2019), seja executado em navegadores mais antigos;
  • [regenerator-runtime]: de acordo com o site da biblioteca --> [Transformador de código-fonte que permite funções geradoras do ECMAScript 6 no JavaScript atual];

A partir do Babel 7, estas duas dependências substituem a dependência [@babel/polyfill], que anteriormente servia este propósito e está agora (setembro de 2019) obsoleta. São instaladas da seguinte forma:

Image

O ficheiro [package.json] passa então a ter a seguinte forma:


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

Para utilizar as dependências [core-js, regenerator-runtime], é necessário adicionar as seguintes [imports] (linhas 3–4) ao script principal [src/main.js]:


// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
 
// imports
import Dao from './Dao2';
import Métier from './Métier';

14.4.3. Configuração do [webpack]

[webpack] é a ferramenta que irá tratar:

  • a transpilagem de todos os ficheiros JavaScript do projeto de ES6 para ES5;
  • o agrupamento dos ficheiros gerados num único ficheiro;

Esta ferramenta é controlada por um ficheiro de configuração [webpack.config.js], que pode ser gerado utilizando uma dependência chamada [@webpack-cli/init] (setembro de 2019). Esta dependência foi instalada juntamente com as outras mencionadas na secção «Link».

Executamos o comando [npx webpack-cli init] num terminal do [VSCode]:

Image

Após responder às várias perguntas (para as quais podemos aceitar a maioria das respostas predefinidas), é gerado um ficheiro [webpack.config.js] na raiz do projeto [4]:

O ficheiro [webpack.config.js] tem o seguinte aspeto:


/* 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
    }
};

Não compreendo todos os detalhes deste ficheiro, mas há alguns aspetos que se destacam:

  • linha 1: o ficheiro não contém código ES6. O [Eslint] reporta então erros que se propagam até à raiz do projeto [javascript]. Isto é irritante. Para impedir que o Eslint analise um ficheiro, basta comentar a linha 1;
  • linha 31: estamos a trabalhar no modo [development];
  • linha 32: o script de entrada está aqui [src/index.js]. Teremos de alterar isto;
  • linha 36: a pasta onde a saída do [webpack] será colocada é a pasta [dist];
  • linha 46: podemos ver que o [webpack] usa o [babel-loader], uma das dependências que instalámos;
  • linha 54: vemos que o [webpack] usa o [@babel-preset/env], uma das dependências que instalámos;

A inicialização do [webpack] modificou o ficheiro [package.json] (pede permissão):


{
  "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"
  }
}
  • linha 4: foi modificada;
  • linhas 8–9, 18–19: foram adicionadas;
  • linha 8: a tarefa [npm] que compila o projeto;
  • linha 9: a tarefa [npm] que a executa;
  • linha 18: ?
  • linha 19: gera um ficheiro [dist/index.html] que incorpora automaticamente o script [dist/main.js] gerado pelo [webpack], e é este que é utilizado quando o projeto é executado;

Por fim, a configuração [webpack] gerou um ficheiro [src/index.js]:

Image

O conteúdo de [index.js] é o seguinte (setembro de 2019):


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

14.4.4. Compilar e executar o projeto

O ficheiro [package.json] contém três tarefas [npm]:


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

Estas tarefas são reconhecidas pelo [VSCode], que as disponibiliza para execução:

Image

  • em [1-3], o projeto é compilado;
  • em [4]: o projeto é compilado para [dist/main.hash.js] e é criada uma página [dist/index.html];

A página [index.html] gerada é a seguinte:


<!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>

Esta página simplesmente encapsula o ficheiro [main.hash.js] gerado pelo [webpack].

O projeto é executado pela tarefa [start]:

Image

A página [dist/index.html] é então carregada num servidor, parte do conjunto [webpack], a funcionar na porta 8080 da máquina local e apresentada pelo navegador predefinido da máquina:

Image

  • em [2], a porta de serviço do servidor web [webpack];
  • em [3], o corpo da página [dist/index.html] está vazio;
  • em [4], o separador [console] das ferramentas de programador do navegador, neste caso o Firefox (F12);
  • em [5], o resultado da execução do ficheiro [src/index.js]. Recorde-se que o seu conteúdo era o seguinte:
console.log("Hello World from your main file!");

Agora, vamos alterar este conteúdo para a seguinte linha:

console.log("Bonjour le monde");

Automaticamente (sem recompilar), são gerados novos ficheiros [main.js, index.html] e o novo ficheiro [index.html] é carregado no navegador:

Image

Não é necessário executar a tarefa [build] antes da tarefa [start]: esta última compila primeiro o projeto. Ela não armazena o resultado dessa compilação na pasta [dist]. Para verificar isso, basta eliminar essa pasta. Veremos então que a tarefa [start] compila e executa o projeto sem criar a pasta [dist]. Parece que armazena o seu resultado [index.html, main.hash.js] numa pasta específica do [webpackdev-server]. Este comportamento é suficiente para os nossos testes.

Quando o servidor de desenvolvimento está em execução, quaisquer alterações guardadas num ficheiro do projeto desencadeiam uma recompilação. Por este motivo, desativamos o modo [Auto Save] do [VSCode]. Não queremos que o projeto seja recompilado sempre que digitamos caracteres num ficheiro do projeto. Queremos que a recompilação ocorra apenas quando as alterações são guardadas:

Image

  • em [2], a opção [Auto Save] não deve estar marcada;

14.4.5. Testar o cliente JavaScript para o servidor de cálculo de impostos

Para testar o cliente JavaScript do servidor de cálculo de impostos, deve designar [main.js] [1] como o ponto de entrada do projeto no ficheiro [webpack.config.js] [2-3]:

Image

Lembre-se de que o script [main.js] deve incluir duas importações adicionais em comparação com a sua versão em [Cliente HTTP 2]:

Image

Além disso, modificámos ligeiramente o código para lidar com os erros que o servidor possa enviar:


// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
 
// imports
import Dao from './Dao2';
import Métier from './Métier';
 
// asynchronous function [main]
async function main() {
  // axios configuration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // layer instantiation [dao]
  const dao = new Dao(axios);
  // requests HTTP
  let taxAdminData;
  try {
    // init session
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    if (response.état != 700) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // authentication
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    if (response.état != 200) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // tax information
    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) {
    // we log the error
    console.log("erreur=", error.message);
    // end
    return;
  }
 
  // instantiation layer [business]
  const métier = new Métier(taxAdminData);
 
  // tax calculations
  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));
  // list of simulations
  log("-----------liste-des-simulations");
  log(simulations);
  // deleting a simulation
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}
 
// log jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}
 
// execution
main();

Comentários

  • Nas linhas [24-26], [31-33] e [38-40], verificamos o código [response.status] enviado na resposta JSON do servidor. Se este código indicar um erro, é lançada uma exceção com a cadeia JSON da resposta do servidor [response.response] como mensagem de erro;

Depois de feito isso, executamos o projeto [5-6].

A página [index.html] é então gerada e carregada no navegador:

Image

  • Em [7], vemos que a ação [init-session] não pôde ser concluída devido a um problema de [CORS] (Cross-Origin Resource Sharing);

O problema de CORS decorre da relação cliente/servidor:

  • o nosso cliente JavaScript foi descarregado para a máquina [http://localhost:8080];
  • o servidor de cálculo de impostos funciona na máquina [http://localhost:80];
  • o cliente e o servidor não estão, portanto, no mesmo domínio (mesma máquina, mas portas diferentes);
  • o navegador que executa o cliente JavaScript carregado a partir da máquina [http://localhost:8080] bloqueia qualquer pedido que não tenha como destino [http://localhost:80]. Trata-se de uma medida de segurança. Por conseguinte, bloqueia o pedido do cliente ao servidor que opera na máquina [http://localhost:80];

Na verdade, o navegador não bloqueia completamente a solicitação. Na realidade, ele aguarda que o servidor lhe «indique» que aceita solicitações entre domínios. Se receber essa autorização, o navegador transmitirá então a solicitação entre domínios.

O servidor concede a sua autorização enviando cabeçalhos 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
  • Linha 1: O cliente JavaScript opera no domínio [http://localhost:8080]. O servidor deve responder explicitamente que aceita este domínio;
  • Linha 2: O cliente JavaScript utilizará os cabeçalhos HTTP [Accept, Content-Type] nas suas solicitações:
    • [Accept]: este cabeçalho é enviado em todas as solicitações;
    • [Content-Type]: este cabeçalho é utilizado em operações POST para especificar o tipo dos parâmetros POST;

O servidor deve aceitar explicitamente estes dois cabeçalhos HTTP;

  • Linha 3: O cliente JavaScript utilizará pedidos GET e POST. O servidor deve aceitar explicitamente estes dois tipos de pedidos;
  • Linha 4: O cliente JavaScript enviará cookies de sessão. O servidor aceita-os com o cabeçalho na linha 4;

Por isso, precisamos de alterar a configuração do servidor. Fazemos isso no [NetBeans]. O problema relacionado com o CORS ocorre apenas no modo de desenvolvimento. Em produção, o cliente e o servidor funcionarão no mesmo domínio [http://localhost:80] e não haverá problemas relacionados com o CORS. Por isso, precisamos de uma forma de ativar ou desativar os pedidos CORS através da configuração do servidor.

Image

As modificações no servidor são feitas em três locais:

  • [1, 4]: no ficheiro de configuração [config.json] para definir um valor booleano que controla se as solicitações entre domínios são aceites ou não;
  • [2]: na classe [ParentResponse], que envia a resposta ao cliente JavaScript. Esta classe enviará os cabeçalhos CORS esperados pelo navegador do cliente;
  • [3]: nas classes [HtmlResponse, JsonResponse, XmlResponse] que geram respostas para as sessões [html, json, xml], respetivamente. Estas classes devem passar o valor booleano [corsAllowed] encontrado em [4] para a sua classe pai [2]. Isto é feito em [5], passando a matriz de imagens do ficheiro JSON [2];

A classe [ParentResponse] [2] evolui da seguinte forma:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
 
class ParentResponse {
 
  // int $statusCode: HTTP response status code
  // string $content: the body of the response to be sent
  // depending on the case, this is a JSON, XML, HTML string
  // array $headers: HTTP headers to be added to the response
 
  public function sendResponse(
    Request $request,
    int $statusCode,
    string $content,
    array $headers,
    array $config): void {
 
    // preparing the server's text response
    $response = new Response();
    $response->setCharset("utf-8");
    // status code
    $response->setStatusCode($statusCode);
    // headers for cross-domain requests
    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);
    }
    // special case of the [OPTIONS] method
    // only the headers are important in this case
    $method = strtolower($request->getMethod());
    if ($method === "options") {
      $content = "";
      $response->setStatusCode(Response::HTTP_OK);
    }
    // we send the answer
    $response->setContent($content);
    $response->send();
  }
 
}
  • linha 29: verificamos se precisamos de lidar com pedidos entre domínios. Se for o caso, geramos os cabeçalhos HTTP CORS (linhas 33–37), mesmo que o pedido atual não seja um pedido entre domínios. Neste último caso, os cabeçalhos CORS serão desnecessários e não serão utilizados pelo cliente;
  • linha 30: numa solicitação entre domínios, o navegador do cliente que consulta o servidor envia um cabeçalho HTTP [Origin: http://localhost:8080] (no caso específico do nosso cliente JavaScript). Na linha 30, recuperamos este cabeçalho HTTP da solicitação [$request];
  • linha 31: só aceitaremos pedidos entre domínios originários da máquina [http://localhost]. Note-se que estes pedidos só ocorrem no modo de desenvolvimento do projeto;
  • Linhas 32–36: Adicionamos os cabeçalhos CORS aos cabeçalhos já presentes na matriz [$headers];
  • Linhas 45–49: A forma como o navegador do cliente solicita permissões CORS pode variar dependendo do navegador utilizado. Por vezes, o navegador do cliente solicita estas permissões utilizando uma solicitação HTTP [OPTIONS]. Este é um novo cenário para o nosso servidor, que foi construído para lidar apenas com solicitações [GET] e [POST]. No caso de uma solicitação [OPTIONS], o servidor gera atualmente uma resposta de erro. Linhas 46–49: corrigimos isto no último momento: se, na linha 46, determinarmos que a solicitação atual é uma solicitação [OPTIONS], então geramos o seguinte para o cliente:
    • linhas 47, 51: uma resposta [$content] vazia;
    • linha 48: um código de estado 200 indicando que a solicitação foi bem-sucedida. A única coisa importante para esta solicitação é enviar os cabeçalhos CORS nas linhas 33–36. É isso que o navegador do cliente espera;

Depois de o servidor ter sido corrigido desta forma, o cliente JavaScript funciona melhor, mas apresenta um novo erro:

Image

  • em [1], a sessão JSON é inicializada corretamente;
  • Em [2], a ação [authenticate-user] falha: o servidor indica que não existe nenhuma sessão ativa. Isto significa que o cliente JavaScript não devolveu corretamente o cookie de sessão que enviou durante a ação [init-session];

Vamos analisar as interações que ocorreram na rede:

Image

  • em [4], o pedido [init-session]. Foi concluído com sucesso com um código de estado 200 na resposta;
  • em [5], o pedido [authenticate-user]. Este pedido falha com um código de estado 400 (Bad Request) [6] na resposta;

Se analisarmos os cabeçalhos HTTP [7] da solicitação [5], podemos ver que o cliente JavaScript não enviou o cabeçalho HTTP [Cookie], o que lhe teria permitido devolver o cookie de sessão inicialmente enviado pelo servidor. É por isso que o servidor informa que não há sessão.

Para que o cliente envie o cookie de sessão, é necessário adicionar uma configuração ao objeto [axios]:


// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
 
// imports
import Dao from './Dao2';
import Métier from './Métier';
 
// asynchronous function [main]
async function main() {
  // axios configuration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  axios.defaults.withCredentials = true;
  // layer instantiation [dao]
  const dao = new Dao(axios);
  // requests HTTP
  let taxAdminData;
...

A linha 15 solicita que os cookies sejam incluídos nos cabeçalhos HTTP da solicitação [axios]. Note que isto não era necessário no ambiente [node.js]. Existem, portanto, diferenças de código entre os dois ambientes.

Depois de corrigido este erro, o cliente JavaScript funciona normalmente:

Image

Image

14.5. Melhoria no cliente HTTP 3

Quando a classe [Dao2] anterior é executada num navegador, a gestão de cookies de sessão torna-se desnecessária. Isto porque o navegador que hospeda a camada [dao] gere o cookie de sessão: reenvia automaticamente qualquer cookie que o servidor lhe envie. Consequentemente, a classe [Dao2] pode ser reescrita como a seguinte classe [Dao3]:


"use strict";
 
// imports
import qs from "qs";
 
class Dao3 {
  // manufacturer
  constructor(axios) {
    this.axios = axios;
  }
 
  // init session
  async initSession() {
    // query options HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // URL parameters
      params: {
        action: "init-session",
        type: "json"
      }
    };
    // execute query HTTP
    return await this.getRemoteData(options);
  }
 
  async authentifierUtilisateur(user, password) {
    // query options HHTP [post /main.php?action=authenticate-user]
    const options = {
      method: "POST",
      headers: {
        "Content-type": "application/x-www-form-urlencoded"
      },
      // body of POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // URL parameters
      params: {
        action: "authentifier-utilisateur"
      }
    };
    // execute query HTTP
    return await this.getRemoteData(options);
  }
 
  async getAdminData() {
    // query options HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // URL parameters
      params: {
        action: "get-admindata"
      }
    };
    // execute query HTTP
    const data = await this.getRemoteData(options);
    // result
    return data;
  }
 
  async getRemoteData(options) {
    // execute query HTTP
    let response;
    try {
      // asynchronous request
      response = await this.axios.request("main.php", options);
    } catch (error) {
      // the [error] parameter is an exception instance - it can take various forms
      if (error.response) {
        // the server response is in [error.response]
        response = error.response;
      } else {
        // error restart
        throw error;
      }
    }
    // response is the entire HTTP response from the server (HTTP headers + response itself)
    // the server response is in [response.data]
    return response.data;
  }
}
 
// class export
export default Dao3;

Tudo o que está relacionado com a gestão do cookie de gestão desapareceu.

Modificamos o projeto anterior da seguinte forma:

Image

Na pasta [src], adicionámos dois ficheiros:

  • a classe [Dao3] que acabámos de introduzir;
  • o ficheiro [main3] responsável por iniciar a nova versão;

O ficheiro [main3] permanece idêntico ao ficheiro [main] da versão anterior, mas agora utiliza a classe [Dao3]:


// imports
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";
 
// imports
import Dao from "./Dao3";
import Métier from "./Métier";
 
// asynchronous function [main]
async function main() {
  // axios configuration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL =
    "http://localhost/php7/scripts-web/impots/version-14";
  axios.defaults.withCredentials = true;
  // layer instantiation [dao]
  const dao = new Dao(axios);
  // requests HTTP
  ...
}
 
// log jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}
 
// execution
main();

O ficheiro [webpack.config] foi modificado para agora executar o 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",
  //entry: "./src/mainjs",
  entry: "./src/main3.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },
 
  plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};

Depois de fazer isto, executamos o projeto após iniciar o servidor de cálculo de impostos:

Image

Os resultados apresentados na consola do navegador são idênticos aos da versão anterior.

14.6. Conclusão

Agora dispomos de todas as ferramentas necessárias para desenvolver código JavaScript para uma aplicação web. Podemos:

  • utilizar o código ECMAScript mais recente;
  • testar partes isoladas deste código num ambiente [node.js], o que é mais simples para depuração e testes;
  • depois portar esse código para um navegador utilizando [babel] e [webpack];