Skip to content

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

14.1. Introduction

Propomos aqui escrever um cliente [node.js] da versão 14 do serviço de cálculo de impostos. A arquitetura cliente/servidor será a seguinte:

Image

Analisaremos 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á a estrutura [main, métier, dao]. A camada [métier] do servidor será transferida para o cliente:

Image

14.2. Cliente HTTP 1

Image

Como já mencionamos, o cliente HTTP 1 implementa a seguinte arquitetura cliente/servidor:

Image

Implementaremos:

  • a camada [dao] na forma de uma classe;
  • a camada [main] na forma de um script que utiliza essa classe;

14.2.1. A camada [dao]

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


'use strict';

// importações
import qs from 'qs'

class Dao1 {

  // construtor
  constructor(axios) {
    // biblioteca axios para fazer as solicitações HTTP
    this.axios = axios;
    // cookie de sessão
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // inicializar sessão
  async  initSession() {
    // opções da consulta HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parâmetros da consulta URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // execução da consulta HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // opções da consulta HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // corpo do POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parâmetros do URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // execução da consulta HTTP
    return await this.getRemoteData(options);
  }

  // cálculo do imposto
  async  calculerImpot(marié, enfants, salaire) {
    // opções da consulta HHTP [post /main.php?action=calculer-impot]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // corpo do POST [marié, enfants, salaire]
      data: qs.stringify({
        marié: marié,
        enfants: enfants,
        salaire: salaire
      }),
      // parâmetros do URL
      params: {
        action: 'calculer-impot'
      }
    };
    // execução da consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  // lista de simulações
  async  listeSimulations() {
    // opções da consulta HHTP [get /main.php?action=lister-simulations]
    const options = {
      method: "GET",
      // parâmetros da consulta URL
      params: {
        action: 'lister-simulations'
      },
    };
    // execução da consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  // lista de simulações
  async  supprimerSimulation(index) {
    // opções da consulta HHTP  [get /main.php?action=supprimer-simulation&numéro=index]
    const options = {
      method: "GET",
      // parâmetros da consulta URL
      params: {
        action: 'supprimer-simulation',
        numéro: index
      },
    };
    // execução da consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  async  getRemoteData(options) {
    // para o cookie de sessão
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // execução da consulta HTTP
    let response;
    try {
      // solicitação assíncrona
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // o parâmetro [error] é uma instância de exceção — pode assumir diversas formas
      if (error.response) {
        // a resposta do servidor está em [error.response]
        response = error.response;
      } else {
        // o erro é reenviado
        throw error;
      }
    }
    // a resposta é o conjunto completo da resposta HTTP do servidor (cabeçalhos HTTP + a própria resposta)
    // recupera-se o cookie de sessão, caso exista
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie é uma matriz
      // procura-se o cookie de sessão nessa matriz
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // procura-se o cookie de sessão
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // armazenamos o cookie de sessão
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // encontrado
          trouvé = true;
        } else {
          // próximo elemento
          i++;
        }
      }
    }
    // a resposta do servidor está em [response.data]
    return response.data;
  }
}

// exportação da classe
export default Dao1;
  • Aqui, utilizamos o que aprendemos no parágrafo “link”, onde apresentamos a biblioteca [axios], que permite realizar consultas HTTP tanto no [node.js] quanto em um navegador. Analisaremos, em particular, o script do parágrafo “link”;
  • linhas 9-15: o construtor da classe. Ela terá três propriedades:
    • [axios]: o objeto [axios] que permite realizar as consultas HTTP. Esse objeto é passado pelo código chamador;
    • [sessionCookieName]: dependendo dos servidores, o cookie de sessão pode ter 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 [calculerImpot] realiza a solicitação [post /main.php?action=calculer-impot] enviando os parâmetros [marié, enfants, salaire]. Ela retorna a string jSON transmitida pelo servidor na forma de um objeto JavaScript;
  • linhas 79-92: a função assíncrona [listeSimulations] realiza a solicitação [get /main.php?action=lister-simulations. Ela retorna a string jSON enviada pelo servidor na forma de um objeto JavaScript;
  • linhas 95-109: a função assíncrona [supprimerSimulation] realiza a solicitação [get /main.php?action=supprimer-simulation&numéro=index]. Ela retorna a string jSON enviada pelo servidor na forma de um objeto JavaScript;
  • linha 121: utiliza-se a notação [this.axios] porque, neste caso, o objeto [axios] transmitido 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] realiza 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;
  • exclui uma delas;

O código é o seguinte:


// importação do axios
import axios from 'axios';
// importação da classe Dao1
import Dao from './Dao1';

// função assíncrona [main]
async function main() {
  // configuração do axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impostos/versão-14';
  // instanciação da camada [dao]
  const dao = new Dao(axios);
  // uso da camada [dao]
  try {
    // inicialização da sessão
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // autenticação
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // cálculos de impostos
    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 simulações
    log("-----------liste-des-simulations");
    response = await dao.listeSimulations();
    log(response);
    // exclusão de uma simulação
    log("-----------suppression simulation n° 1");
    response = await dao.supprimerSimulation(1);
    log(response);
  } catch (error) {
    // registro do erro
    console.log("erreur=", error.message);
  }
}

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

// execução
main();

Comentários

  • linha 2: importa-se a biblioteca [axios];
  • linha 4: importa-se a classe [Dao];
  • linha 7: a função [main], que se comunica com o servidor, é assíncrona;
  • linhas 9-10: configuração padrão das solicitações HTTP que serão enviadas ao servidor:
    • linha 9: [timeout] com duração de 2 segundos;
    • linha 10: todas as URL têm como prefixo a URL, base da versão 14 do servidor de cálculo de impostos;
  • linha 12: a camada [Dao] é criada. Agora ela pode ser utilizada;
  • linhas 46-48: a função [log] tem como objetivo exibir a string jSON de um objeto JavaScript de forma formatada: na forma vertical 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: três cálculos de imposto são solicitados em paralelo. Graças ao [await Promise.all], a execução fica bloqueada até que todos os três resultados tenham sido obtidos;
  • linhas 31-34: lista de simulações;
  • linhas 35-38: exclusão de uma simulação;
  • linhas 39-42: tratamento de uma eventual exceção;

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

A camada [métier] foi transferida do servidor para o cliente JavaScript. Ao contrário do que fizemos no curso PHP7, a camada [main] não precisará, neste caso, passar pela camada [métier] para chegar à camada [dao]. Utilizaremos essas duas camadas como centros de competência:

  • a camada [main] passa pela camada [dao] assim que precisa de dados que estão no servidor;
  • a camada [main] solicita à camada [métier] que realize os cálculos do imposto;
  • a camada [métier] é independente da camada [dao] e nunca recorre a ela;

14.3.1. A classe JavaScript [Métier]

A essência da classe [Métier] em PHP foi descrita no artigo (link). Trata-se de um código bastante complexo que relembramos aqui, não para explicá-lo, mas para poder traduzi-lo para JavaScript:


<?php

// espaço de nomes
namespace Application;

class Metier implements InterfaceMetier {
  // camada DAO
  private $dao;
  // dados da administração fiscal
  private $taxAdminData;

  //---------------------------------------------
  // setter da camada [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceDao $dao) {
    // armazena-se uma referência na camada [dao]
    $this->dao = $dao;
    // recuperam-se os dados que permitem o cálculo do imposto
    // o método [getTaxAdminData] pode lançar uma exceção ExceptionImpots
    // em seguida, permite que a exceção seja repassada ao código chamador
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// cálculo do imposto
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié: sim, não
    // $enfants: número de filhos
    // $salaire: salário anual
    // $this->taxAdminData: dados da administração fiscal
    //
    // verifica-se se os dados da administração fiscal estão corretos
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // cálculo do imposto com filhos
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // cálculo do imposto sem filhos
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // aplicação do limite máximo do quociente familiar
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos, o dobro para os seguintes
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // aplica-se a alíquota mais alta
    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 uma eventual redução
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // cálculo de uma eventual redução de impostos
    $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é: sim, não
    // $enfants: número de filhos
    // $salaire: salário anual
    // $this->taxAdminData: dados da administração fiscal
    //
    // número de cotas
    $marié = strtolower($marié);
    if ($marié === "oui") {
      $nbParts = $enfants / 2 + 2;
    } else {
      $nbParts = $enfants / 2 + 1;
    }
    // 1 parcela por filho a partir do terceiro
    if ($enfants >= 3) {
      // meia parcela adicional para cada filho a partir do terceiro
      $nbParts += 0.5 * ($enfants - 2);
    }
    // renda tributável
    $revenuImposable = $this->getRevenuImposable($salaire);
    // sobretaxa
    $surcôte = floor($revenuImposable - 0.9 * $salaire);
    // para problemas de arredondamento
    if ($surcôte < 0) {
      $surcôte = 0;
    }
    // quociente familiar
    $quotient = $revenuImposable / $nbParts;
    // cálculo do imposto
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // é colocado no final da tabela de limites para interromper o ciclo a seguir
    $limites[count($limites) - 1] = $quotient;
    // busca da alíquota de imposto
    $i = 0;
    while ($quotient > $limites[$i]) {
      $i++;
    }
    // devido ao fato de termos colocado $quotient no final da tabela $limites, o loop anterior
    // não pode ultrapassar os limites da tabela $limites
    // agora podemos calcular o imposto
    $impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
    // resultado
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable = salárioAnual - dedução
  // a dedução tem um valor mínimo e um valor máximo
  private function getRevenuImposable(float $salaire): float {
    // abatimento de 10% do salário
    $abattement = 0.1 * $salaire;
    // essa dedução não pode ultrapassar $this->taxAdminData->getAbattementDixPourCentMax()
    if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMax();
    }
    // a dedução não pode ser inferior a $this->taxAdminData->getAbattementDixPourcentMin()
    if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMin();
    }
    // renda tributável
    $revenuImposable = $salaire - $abattement;
    // resultado
    return floor($revenuImposable);
  }

// calcula uma eventual redução
  private function getDecôte(string $marié, float $salaire, float $impots): float {
    // inicialmente, uma redução nula
    $décôte = 0;
    // valor máximo do imposto para que haja redução
    $plafondImpôtPourDécôte = $marié === "oui" ?
      $this->taxAdminData->getPlafondImpotCouplePourDecote() :
      $this->taxAdminData->getPlafondImpotCelibatairePourDecote();
    if ($impots < $plafondImpôtPourDécôte) {
      // valor máximo do desconto
      $plafondDécôte = $marié === "oui" ?
        $this->taxAdminData->getPlafondDecoteCouple() :
        $this->taxAdminData->getPlafondDecoteCelibataire();
      // abatimento teórico
      $décôte = $plafondDécôte - 0.75 * $impots;
      // a redução não pode exceder o valor do imposto
      if ($décôte > $impots) {
        $décôte = $impots;
      }
      // não há abatimento <0
      if ($décôte < 0) {
        $décôte = 0;
      }
    }
    // resultado
    return ceil($décôte);
  }

// calcula uma eventual redução
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    // o limite máximo de renda para ter direito à redução de 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();
    }
    // renda tributável
    $revenuImposable = $this->getRevenuImposable($salaire);
    // redução
    $réduction = 0;
    if ($revenuImposable < $plafondRevenuPourRéduction) {
      // redução de 20%
      $réduction = 0.2 * $impots;
    }
    // resultado
    return ceil($réduction);
  }

  // cálculo de impostos em modo batch
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // permite que as exceções provenientes da camada [dao] sejam reportadas
    // recuperação dos dados dos contribuintes
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabela de resultados
    $results = [];
    // analisamos os resultados
    foreach ($taxPayersData as $taxPayerData) {
      // calcula-se o imposto
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // preenche-se [$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"]);
      // insere-se o resultado na tabela de resultados
      $results [] = $taxPayerData;
    }
    // gravação dos resultados
    $this->dao->saveResults($resultsFileName, $results);
  }

}
  • linhas 19-26: o construtor da classe PHP. Como mencionamos que estaríamos construindo uma camada [métier] independente da camada [dao], faremos duas modificações nesse construtor em JavaScript:
    • ele não receberá uma instância da camada [dao] (já não precisa mais dela);
    • ele não solicitará os dados fiscais da administração [taxAdminData] à camada [dao]: será o código chamador que transmitirá esses dados ao construtor;
  • linhas 197-122: não implementaremos o método [executeBatchImpots], cujo objetivo final era gravar os resultados das simulações em um arquivo de texto. Queremos um código que funcione tanto no [node.js] quanto em um navegador. No entanto, não é possível salvar dados no sistema de arquivos da máquina que executa o navegador cliente;

Com essas restrições, o código da classe JavaScript [Métier] é o seguinte:


'use strict';

// classe Métier
class Métier {

  // construtor
  constructor(taxAdmindata) {
    // this.taxAdminData: dados da administração fiscal
    this.taxAdminData = taxAdmindata;
  }

  // cálculo do imposto
  // --------------------------------------------------------------------------
  calculerImpot(marié, enfants, salaire) {
    // casado: sim, não
    // filhos: número de filhos
    // salário: salário anual
    // this.taxAdminData: dados da administração tributária
    //
    // cálculo do imposto com filhos
    const result1 = this.calculerImpot2(marié, enfants, salaire);
    const impot1 = result1["impôt"];
    // cálculo do imposto sem filhos
    let result2, impot2, plafondDemiPart;
    if (enfants !== 0) {
      result2 = this.calculerImpot2(marié, 0, salaire);
      impot2 = result2["impôt"];
      // aplicação do limite máximo do quociente familiar
      plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
      if (enfants < 3) {
        // PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos
        impot2 = impot2 - enfants * plafondDemiPart;
      } else {
        // PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos, o dobro para os seguintes
        impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
      }
    } else {
      // sem recálculo do imposto
      impot2 = impot1;
      result2 = result1;
    }
    // considera-se o imposto mais alto em [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 uma eventual redução
    const décôte = this.getDecôte(marié, impot);
    impot -= décôte;
    // cálculo de uma eventual redução de impostos
    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: sim, não
    // filhos: número de filhos
    // salário: salário anual
    // this->taxAdminData: dados da administração fiscal
    //
    // número de cotas
    marié = marié.toLowerCase();
    let nbParts;
    if (marié === "oui") {
      nbParts = enfants / 2 + 2;
    } else {
      nbParts = enfants / 2 + 1;
    }
    // 1 parcela por filho a partir do terceiro
    if (enfants >= 3) {
      // meia parcela adicional para cada filho a partir do terceiro
      nbParts += 0.5 * (enfants - 2);
    }
    // renda tributável
    const revenuImposable = this.getRevenuImposable(salaire);
    // sobretaxa
    let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
    // para problemas de arredondamento
    if (surcôte < 0) {
      surcôte = 0;
    }
    // quociente familiar
    const quotient = revenuImposable / nbParts;
    // cálculo do imposto
    const limites = this.taxAdminData.limites;
    const coeffR = this.taxAdminData.coeffR;
    const coeffN = this.taxAdminData.coeffN;
    // é colocado no final da tabela de limites para interromper o ciclo a seguir
    limites[limites.length - 1] = quotient;
    // busca da alíquota de imposto
    let i = 0;
    while (quotient > limites[i]) {
      i++;
    }
    // como o quociente familiar foi colocado no final da tabela de limites, o loop anterior
    // não pode ultrapassar os limites da tabela
    // agora é possível calcular o imposto
    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 = salárioAnual - dedução
  // a dedução tem um valor mínimo e um valor máximo
  getRevenuImposable(salaire) {
    // abatimento de 10% do salário
    let abattement = 0.1 * salaire;
    // essa dedução não pode exceder taxAdminData.getAbattementDixPourCentMax()
    if (abattement > this.taxAdminData.abattementDixPourCentMax) {
      abattement = this.taxAdminData.abattementDixPourcentMax;
    }
    // a dedução não pode ser inferior a taxAdminData.getAbattementDixPourcentMin()
    if (abattement < this.taxAdminData.abattementDixPourcentMin) {
      abattement = this.taxAdminData.abattementDixPourcentMin;
    }
    // renda tributável
    const revenuImposable = salaire - abattement;
    // resultado
    return Math.floor(revenuImposable);
  }

  // calcula uma eventual redução
  getDecôte(marié, impots) {
    // inicialmente, uma redução nula
    let décôte = 0;
    // valor máximo do imposto para que haja redução
    let plafondImpôtPourDécôte = marié === "oui" ?
      this.taxAdminData.plafondImpotCouplePourDecote :
      this.taxAdminData.plafondImpotCelibatairePourDecote;
    let plafondDécôte;
    if (impots < plafondImpôtPourDécôte) {
      // valor máximo do desconto
      plafondDécôte = marié === "oui" ?
        this.taxAdminData.plafondDecoteCouple :
        this.taxAdminData.plafondDecoteCelibataire;
      // abatimento teórico
      décôte = plafondDécôte - 0.75 * impots;
      // a redução não pode exceder o valor do imposto
      if (décôte > impots) {
        décôte = impots;
      }
      // não há abatimento <0
      if (décôte < 0) {
        décôte = 0;
      }
    }
    // resultado
    return Math.ceil(décôte);
  }

  // calcula uma eventual redução
  getRéduction(marié, salaire, enfants, impots) {
    // o limite máximo de renda para ter direito à redução de 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;
    }
    // renda tributável
    const revenuImposable = this.getRevenuImposable(salaire);
    // redução
    let réduction = 0;
    if (revenuImposable < plafondRevenuPourRéduction) {
      // redução de 20%
      réduction = 0.2 * impots;
    }
    // resultado
    return Math.ceil(réduction);
  }
}

// exportação da classe
export default Métier;
  • o código JavaScript segue rigorosamente o código PHP;
  • a classe [Métier] é exportada, linha 187;

14.3.2. A classe JavaScript [Dao2]

Image

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


'use strict';

// importações
import qs from 'qs'

class Dao2 {

  // construtor
  constructor(axios) {
    this.axios = axios;
    // cookie de sessão
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // inicialização da sessão
  async  initSession() {
    // opções da solicitação HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parâmetros da consulta URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // execução da consulta HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // opções da consulta HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // corpo do POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parâmetros do URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // execução da consulta HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // opções da consulta HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // parâmetros da consulta URL
      params: {
        action: 'get-admindata'
      }
    };
    // execução da consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  async  getRemoteData(options) {
    // para o cookie de sessão
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // execução da consulta HTTP
    let response;
    try {
      // solicitação assíncrona
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // o parâmetro [error] é uma instância de exceção — pode assumir diversas formas
      if (error.response) {
        // a resposta do servidor está em [error.response]
        response = error.response;
      } else {
        // o erro é reenviado
        throw error;
      }
    }
    // a resposta é o conjunto completo da resposta HTTP do servidor (cabeçalhos HTTP + a própria resposta)
    // recupera-se o cookie de sessão, caso exista
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie é uma matriz
      // procura-se o cookie de sessão nessa matriz
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // procura-se o cookie de sessão
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // armazenamos o cookie de sessão
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // encontramos
          trouvé = true;
        } else {
          // próximo elemento
          i++;
        }
      }
    }
    // a resposta do servidor está em [response.data]
    return response.data;
  }
}

// exportação da classe
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;
    • [authentifier-utilisateur] (linhas 31-50): para autenticar-se;
    • [get-admindata] (linhas 52-65): para obter os dados da administração fiscal que permitirão realizar os cálculos do imposto, no lado do cliente;
  • linhas 52-65: introduzimos uma nova ação [get-admindata] para o servidor. Essa ação ainda não havia sido implementada. Fazemos isso 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 possui as seguintes características:

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

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

A modificação será feita no NetBeans:

Image

Em [2], modificamos o arquivo [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 esse controlador na lista de classes a serem carregadas pelo aplicativo PHP;

A próxima etapa é implementar o controlador [AdminDataController] [3]:


<?php

namespace Application;

// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias da camada [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

class AdminDataController implements InterfaceController {

  // $config é a configuração do aplicativo
  // processamento de uma solicitação Request
  // utiliza a sessão Session e pode modificá-la
  // $infos são informações adicionais específicas de cada controlador
  // retorna um array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // deve haver um único parâmetro GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      // observa-se o erro
      $message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
      $état = 1001;
      // retorno do resultado ao controlador principal
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }

    // é possível continuar trabalhando
    // Redis
    \Predis\Autoloader::register();
    try {
      // cliente [predis]
      $redis = new \Predis\Client();
      // conectando-se ao servidor para verificar se ele está disponível
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // deu errado
      // resultado devolvido com erro ao controlador principal
      $état = 1050;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }

    // recuperação dos dados da administração fiscal
    // primeiro se verifica no cache [redis]
    if (!$redis->get("taxAdminData")) {
      try {
        // os dados fiscais são buscados no banco de dados
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // taxAdminData
        $taxAdminData = $dao->getTaxAdminData();
        // colocamos os dados recuperados no Redis
        $redis->set("taxAdminData", $taxAdminData);
      } catch (\RuntimeException $ex) {
        // ocorreu um erro
        // retorno do resultado com erro para o controlador principal
        $état = 1041;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // os dados fiscais são obtidos da memória [redis], de escopo [application]
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      // instancia-se um objeto [TaxAdminData] a partir da tabela de atributos anterior
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
    }

    // retorna o resultado ao controlador principal
    $état = 1000;
    return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
  }

}

Comentários

  • linha 12: assim como os outros controladores do servidor, o [AdminDataController] implementa a interface [InterfaceController], constituída pelo método [execute] das linhas 19 a 79;
  • linha 78: assim como nos outros controladores do servidor, o método [AdminDataController.execute] retorna um array [$status, $état, [‘réponse’=>$response]] com:
    • [$status]: o código de status da resposta HTTP;
    • [$état]: um código interno do aplicativo que representa o estado em que o servidor se encontra após a execução da solicitação do cliente;
    • [$response]: um array que encapsula a resposta a ser enviada ao cliente. Aqui, esse array será posteriormente transformado na string jSON;
  • linhas 25-34: verifica-se se a ação [get-admindata] do cliente está sintaticamente correta;
  • linhas 37-74: recupera-se um objeto [TaxAdminData] encontrado:
    • linhas 56-59: no banco de dados, caso não tenha sido encontrado no cache [redis];
    • linhas 70-73: no cache [redis];

Esse código é idêntico ao do controlador [CalculerImpotController] explicado no artigo (link). De fato, esse controlador também deveria recuperar o objeto [TaxAdminData] que encapsula os dados da administração fiscal.

Durante os testes do cliente JavaScript, a forma jSON do [TaxAdminData] causou problemas quando esse objeto foi encontrado no cache [redis]. Para entender isso, vamos examinar de que forma esse objeto é armazenado em [redis]:

Image

Image

  • No [5-7], percebe-se que valores numéricos foram armazenados na forma de cadeias de caracteres. O PHP aceitou isso porque o operador + nos cálculos entre números e cadeias de caracteres provoca implicitamente uma conversão de tipo da cadeia de caracteres para um número. Mas o JavaScript faz o contrário: o operador + em cálculos entre números e cadeias de caracteres provoca implicitamente uma mudança de tipo do número para uma cadeia de caracteres. Os cálculos da classe JavaScript [Métier] estão, portanto, incorretos;

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


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // faixas de imposto
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // constantes de cálculo do imposto
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // inicialização
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // pai
    parent::setFromJsonFile($taxAdminDataFilename);
    // verifica-se os valores dos atributos
    $this->checkAttributes();
    // retorna o objeto
    return $this;
  }

  protected function check($value): \stdClass {
    // $value é uma matriz de elementos do tipo string ou um único elemento
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // transforma-se a matriz de strings em uma matriz de números reais
    $newTableau = [];
    $result = new \stdClass();
    // os elementos da matriz devem ser números decimais positivos ou nulos
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // coloca-se o float em newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // registra-se o erro
        $result->erreur = TRUE;
        // sai-se
        return $result;
      }
    }
    // retornamos o resultado
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // um único valor
      $result->value = $newTableau[0];
    } else {
      // uma lista de valores
      $result->value = $newTableau;
    }
    return $result;
  }

  // inicialização por meio de uma matriz de atributos
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // pai
    parent::setFromArrayOfAttributes($arrayOfAttributes);
    // verifica-se os valores dos atributos
    $this->checkAttributes();
    // retorna o objeto
    return $this;
  }

  // verificação dos valores dos atributos
  protected function checkAttributes() {
    // verifica-se se os valores dos atributos são números reais >=0
    foreach ($this as $key => $value) {
      if (is_string($value)) {
        // $value deve ser um número real >=0 ou uma matriz de números reais >=0
        $result = $this->check($value);
        // erro?
        if ($result->erreur) {
          // lança-se uma exceção
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // registramos o valor
          $this->$key = $result->value;
        }
      }
    }

    // retornamos o objeto
    return $this;
  }

  // getters e setters
  ...

}

Comentários

  • linha 5: a classe [TaxAdminData] estende a classe [BaseEntity], que já possui o método [setFromArrayOfAttributes]. Como esse método não é adequado, nós o redefinimos nas linhas 67-75;
  • linha 70: o método [setFromArrayOfAttributes] da classe pai é utilizado inicialmente para inicializar os atributos da classe;
  • linha 72: o método [checkAttributes] verifica se os valores associados são realmente números. Se forem cadeias de caracteres, elas são convertidas em números;
  • linha 74: o objeto [$this] gerado é, então, um objeto com atributos cujos valores são numéricos;
  • linhas 78-93: o método [checkAttributes] verifica se os valores associados aos atributos do objeto são realmente numéricos;
  • linha 80: percorre-se a lista de atributos;
  • linha 81: se o valor de um atributo for do tipo [string];
  • linha 83: verifica-se se essa sequência de caracteres representa um número;
  • linha 90: se for o caso, a sequência de caracteres é convertida em número e atribuída ao atributo testado;
  • linhas 85-86: se não for o caso, é lançada uma exceção;
  • linhas 32-65: a função [check] faz um pouco mais do que o necessário. Ela processa tanto matrizes quanto valores únicos. No entanto, aqui ela é chamada apenas para verificar um valor do tipo [string]. Ela retorna um objeto com as propriedades [erreur, value], em que:
    • [erreur] é um valor booleano que indica se há ou não um erro;
    • [value] é o parâmetro [value] da linha 32, transformado em número ou matriz de números, conforme o caso;

A classe [BaseEntity], que poderia ter um atributo chamado [arrayOfAttributes], foi modificada para não possuir mais esse atributo: ele, de fato, contamina a cadeia jSON com [TaxAdminData]. A classe é reescrita da seguinte forma:


<?php

namespace Application;

class BaseEntity {
  
  // inicialização a partir de um arquivo JSON
  public function setFromJsonFile(string $jsonFilename) {
    // recupera-se o conteúdo do arquivo de dados fiscais
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // erro?
    if (!$fileContents) {
      // registrando o erro
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // recupera-se o código JSON do arquivo de configuração em uma tabela associativa
      $arrayOfAttributes = \json_decode($fileContents, true);
      // erro?
      if ($arrayOfAttributes === FALSE) {
        // registra-se o erro
        $erreur = TRUE;
        $message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // erro?
    if ($erreur) {
      // lança-se uma exceção
      throw new ExceptionImpots($message);
    }
    // inicialização dos atributos da classe
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // verifica-se a presença de todos os atributos
    $this->checkForAllAttributes($arrayOfAttributes);
    // retornando o objeto
    return $this;
  }

  public function checkForAllAttributes($arrayOfAttributes) {
    // verifica-se se todas as chaves foram inicializadas
    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) {
    // inicializa-se alguns atributos da classe (não necessariamente todos)
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // retorna-se o objeto
    return $this;
  }

  // toString
  public function __toString() {
    // atributos do objeto
    $arrayOfAttributes = \get_object_vars($this);
    // cadeia de caracteres jSON do objeto
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }

}

Comentários

  • linha 20: o atributo [$this→arrayOfAttributes] foi transformado em uma 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 essa alteração no [BaseEntity], a classe [Database] também deve ser ligeiramente modificada:


<?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
  // inicialização
  public function setFromJsonFile(string $jsonFilename) {
    // pai
    parent::setFromJsonFile($jsonFilename);
    // retorna o objeto
    return $this;
  }

  // getters e setters
  ...
}

Comentários

  • no código original, após a linha 30, era chamado o método [parent::checkForAllAttributes]. Isso não precisa mais ser feito, pois agora é tratado automaticamente pelo método [parent::setFromJsonFile($jsonFilename)];

14.3.4. Testes do método [Postman] no servidor

O [Postman] foi apresentado no artigo (link).

Utilizamos os seguintes testes do Postman:

Image

Image

Image

O resultado jSON dessa última solicitação é o seguinte:

Image

  • em [5-8], é possível observar que os atributos da string jSON possuem, de fato, valores numéricos (e não strings de caracteres). Esse resultado permitirá que a classe JavaScript [Métier] seja executada normalmente;

14.3.5. O script principal [main]

Image

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


// importações
import axios from 'axios';

// importações
import Dao from './Dao2';
import Métier from './Métier';

// função assíncrona [main]
async function main() {
  // configuração do Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instanciação da camada [dao]
  const dao = new Dao(axios);
  // solicitações HTTP
  let taxAdminData;
  try {
    // inicialização de sessão
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // autenticação
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // dados fiscais
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response.réponse;
  } catch (error) {
    // registro do erro
    console.log("erreur=", error.message);
    // fim
    return;
  }

  // instanciação da camada [métier]
  const métier = new Métier(taxAdminData);

  // cálculos de imposto
  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 simulações
  log("-----------liste-des-simulations");
  log(simulations);
  // exclusão de uma simulação
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

// execução
main();

Comentários

  • linhas 5-6: importação das classes [Dao] e [Métier];
  • linha 9: a função assíncrona [main], que organizará a comunicação com o servidor por meio da classe [Dao] e solicitará à classe [Métier] que realize os cálculos de impostos;
  • linhas 10-36: o script chama sucessivamente e de forma bloqueante os métodos [initSession, authentifierUtilisateur, getAdminData] da camada [dao];
  • linha 38: a camada [dao] já não é necessária. Temos todos os elementos para que a camada [métier] do cliente JavaScript funcione;
  • linhas 41-46: realizamos três cálculos de imposto, cujos resultados são acumulados em uma tabela [simulations];
  • linha 49: exibimos a tabela de simulações;
  • linha 52: excluímos 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 seção, abrimos o aplicativo [Client HTTP 2] em um navegador de acordo com a seguinte arquitetura:

Image

A portabilidade não ocorre imediatamente. Embora o [node.js] seja capaz de executar JavaScript (ES6), isso geralmente não é o caso dos navegadores. É necessário, portanto, utilizar ferramentas que convertam o código ES6 em código ES5 compreendido pelos navegadores mais recentes. Felizmente, essas ferramentas são ao mesmo tempo poderosas e bastante fáceis de usar.

Seguimos aqui o artigo [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].

Na pasta [client HTTP 3/src], colocamos os elementos [main.js, Métier.js, Dao2.js] do aplicativo [Client Http 2] que acabamos de desenvolver.

14.4.1. Inicialização do projeto

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

Image

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

Image

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

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

Vamos 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 em navegadores recentes e mais antigos;
  • [@babel/preset-env]: faz parte do conjunto de ferramentas Babel. É executado antes da transpilagem de ES6 para ES5;
  • [babel-loader]: essa dependência permite que a ferramenta [webpack] utilize a ferramenta [Babel];
  • [webpack]: o “maestro”. É a ferramenta [webpack] que utiliza o Babel para realizar a transpilagem dos códigos ES6 → ES5 e, em seguida, é ela mesma que agrupa todos os arquivos resultantes em um único arquivo;
  • [webpack-cli]: necessário para o [webpack];
  • [@webpack-cli/init]: usado para configurar o [webpack];
  • [webpack-dev-server]: fornece um servidor web de desenvolvimento que opera, por padrão, na porta 8080. Quando os arquivos-fonte são alterados, ele recarrega automaticamente o aplicativo web;

As dependências do projeto são instaladas da seguinte maneira em um terminal do [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 arquivo [package.json] sofreu as seguintes alterações:


{
  "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]: elas são necessárias durante a fase de desenvolvimento, mas não na fase de produção. De fato, em produção, é o arquivo [dist/main.js] que é utilizado. Ele está codificado em ES5 e não precisa mais das ferramentas de transpilagem de código de ES6 para ES5;

Precisamos adicionar duas dependências ao projeto:

  • [core-js]: contém “polyfills” para o ECMAScript 2019. Um polyfill permite executar um código mais recente, como o ECMAScript 2019 (setembro de 2019), em navegadores mais antigos;
  • [regenerator-runtime]: de acordo com o site da biblioteca --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];

Essas duas dependências substituem, a partir do Babel 7, a dependência [@babel/polyfill], que antes desempenhava essa função e que agora (setembro de 2019) está obsoleta. Elas são instaladas da seguinte maneira:

Image

O arquivo [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"
  }
}

O uso das dependências [core-js, regenerator-runtime] exige que os seguintes [imports] (linhas 3-4) sejam incluídos no script principal [src/main.js]:


// importações
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// importações
import Dao from './Dao2';
import Métier from './Métier';

14.4.3. Configuração do [webpack]

[webpack] é a ferramenta que irá controlar:

  • a transpilagem de ES6 para ES5 de todos os arquivos JavaScript do projeto;
  • a unificação dos arquivos gerados em um único arquivo;

Essa ferramenta é controlada por um arquivo de configuração [webpack.config.js], que pode ser gerado por meio de uma dependência chamada [@webpack-cli/init] (setembro de 2019). Essa dependência foi instalada junto com as outras, conforme indicado no parágrafo com o link.

Executamos o comando [npx webpack-cli init] em um terminal [VSCode]:

Image

Após responder às diversas perguntas (nas quais podemos aceitar a maioria das respostas propostas por padrão), um arquivo [webpack.config.js] é gerado na raiz do projeto [4]:

O arquivo [webpack.config.js] tem a seguinte aparência:


/* 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 desse arquivo, mas é possível observar alguns pontos:

  • linha 1: o arquivo não contém o código ES6. O [Eslint], então, relata erros que remontam até a raiz do projeto [javascript]. Isso é incômodo. Para impedir que o Eslint analise um arquivo, basta colocar a linha 1 entre comentários;
  • linha 31: estamos trabalhando no modo [développement];
  • linha 32: o script de entrada aqui é [src/index.js]. Teremos que alterar isso;
  • linha 36: a pasta onde serão armazenados os produtos do [webpack] será a pasta [dist];
  • linha 46: vemos que [webpack] utiliza [babel-loader], uma das dependências que instalamos;
  • linha 54: vemos que o [webpack] utiliza o [@babel-preset/env], uma das dependências que instalamos;

A inicialização do [webpack] alterou o arquivo [package.json] (ele solicita autorizaçã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 alterada;
  • linhas 8-9, 18-19: foram adicionadas;
  • linha 8: a tarefa [npm], que permite compilar o projeto;
  • linha 9: a tarefa [npm], que permite executá-lo;
  • linha 18: ?
  • linha 19: permite a geração de um arquivo [dist/index.html] que incorpora automaticamente o script [dist/main.js] gerado por [webpack], e é este script que é executado quando o projeto é executado;

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

Image

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


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

14.4.4. Compilação e execução do projeto

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


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

Essas tarefas são incluídas pelo [VSCode], que as propõe para execução:

Image

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

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>

Portanto, essa página se limita a encapsular o arquivo [main.hash.js] gerado por [webpack].

O projeto é executado pela tarefa [start]:

Image

A página [dist/index.html] é então carregada em um servidor, pertencente à suíte [webpack], operando na porta 8080 da máquina local e exibida pelo navegador padrão da máquina:

Image

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

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

console.log("Bonjour le monde");

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

Image

Não é necessário executar a tarefa [build] antes da tarefa [start]: esta última realiza primeiro a compilação do projeto. Ela não armazena os resultados dessa compilação na pasta [dist]. Para perceber isso, basta excluir essa pasta. Ver-se-á então que a tarefa [start] compila e executa o projeto sem criar a pasta [dist]. Ela parece armazenar seus produtos [index.html, main.hash.js] em uma pasta específica da tarefa [webpackdev-server]. Esse comportamento é suficiente para nossos testes.

Quando o servidor de desenvolvimento é iniciado, qualquer alteração salva em um dos arquivos do projeto provoca uma recompilação. Por esse motivo, desativamos o modo [Auto Save] do [VSCode]. De fato, não queremos que haja recompilação sempre que digitarmos caracteres em um dos arquivos do projeto. Só queremos que a recompilação ocorra no momento em que as alterações forem salvas:

Image

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

14.4.5. Testes do cliente JavaScript do servidor de cálculo de impostos

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

Image

Não se esqueça de que o script [main.js] deve incluir duas importações adicionais em relação à sua versão no [Client http 2]:

Image

Além disso, modificamos ligeiramente o código para lidar com os erros que o servidor pode enviar:


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

// importações
import Dao from './Dao2';
import Métier from './Métier';

// função assíncrona [main]
async function main() {
  // configuração do axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instanciação da camada [dao]
  const dao = new Dao(axios);
  // solicitações HTTP
  let taxAdminData;
  try {
    // inicialização de sessão
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    if (response.état != 700) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // autenticação
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    if (response.état != 200) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // dados fiscais
    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) {
    // registro do erro
    console.log("erreur=", error.message);
    // fim
    return;
  }

  // instanciação da camada [métier]
  const métier = new Métier(taxAdminData);

  // cálculos de imposto
  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 simulações
  log("-----------liste-des-simulations");
  log(simulations);
  // exclusão de uma simulação
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

// execução
main();

Comentários

  • nas linhas [24-26], [31-33] e [38-40], verificamos o código [response.état] enviado na resposta jSON do servidor. Se esse código indicar um erro, é lançada uma exceção com a mensagem de erro “jSON”, proveniente da resposta do servidor [response.réponse];

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 [CORS] (Cross-Origin Resource Sharing);

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

  • nosso cliente JavaScript foi baixado na máquina [http://localhost:8080];
  • o servidor de cálculo de impostos está sendo executado na máquina [http://localhost:80];
  • o cliente e o servidor não estão, portanto, nos mesmos domínios (mesma máquina, mas porta diferente);
  • o navegador que executa o cliente JavaScript carregado da máquina [http://localhost:8080] bloqueia qualquer solicitação que não tenha como destino [http://localhost:80]. Trata-se de uma medida de segurança. Por isso, ele também bloqueia a solicitação do cliente para o servidor que opera na máquina [http://localhost:80];

Na verdade, o navegador não bloqueia totalmente a solicitação. Ele, na verdade, aguarda que o servidor “informe” que aceita solicitações entre domínios. Se obtiver essa autorização, o navegador transmitirá a solicitação entre domínios.

O servidor concede 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 esse domínio;
  • linha 2: o cliente JavaScript utilizará em suas solicitações os cabeçalhos HTTP e [Accept, Content-Type]:
    • [Accept]: esse cabeçalho é enviado em todas as solicitações;
    • [Content-Type]: esse cabeçalho é utilizado nas operações POST para indicar o tipo dos parâmetros do POST;

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

  • linha 3: o cliente JavaScript utilizará as solicitações GET e POST. O servidor deve aceitar explicitamente esses dois tipos de solicitações;
  • linha 4: o cliente JavaScript enviará cookies de sessão. O servidor os aceita com o cabeçalho da linha 4;

Portanto, precisamos modificar o servidor. Fazemos isso em [Netbeans]. O problema com CORS é um problema encontrado 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á problema CORS. Portanto, precisamos de uma maneira de autorizar ou não as solicitações CORS por meio da configuração do servidor.

Image

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

  • [1, 4]: no arquivo de configuração [config.json], para inserir um valor booleano que controlará se as solicitações entre domínios serão aceitas ou não;
  • [2]: na classe [ParentResponse], que envia a resposta ao cliente JavaScript. É ela que enviará os cabeçalhos CORS esperados pelo navegador do cliente;
  • [3]: nas classes [HtmlResponse, JsonResponse, XmlResponse] que geram as respostas para as sessões [html, json, xml], respectivamente. Essas classes devem passar para sua classe pai [2] o valor booleano [corsAllowed] encontrado em [4]. Isso é feito em [5], passando a matriz de imagens do arquivo jSON para [2];

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


<?php

namespace Application;

// dependências do Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ParentResponse {

  // int $statusCode: o código HTTP do status da resposta
  // string $content: o corpo da resposta a ser enviada
  // dependendo do caso, é uma string JSON, XML, HTML
  // matriz $headers: os cabeçalhos HTTP a serem adicionados à resposta

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

    // preparação da resposta de texto do servidor
    $response = new Response();
    $response->setCharset("utf-8");
    // código de status
    $response->setStatusCode($statusCode);
    // cabeçalhos para solicitações entre domínios
    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 específico do método [OPTIONS]
    // neste caso, apenas os cabeçalhos são importantes
    $method = strtolower($request->getMethod());
    if ($method === "options") {
      $content = "";
      $response->setStatusCode(Response::HTTP_OK);
    }
    // envia-se a resposta
    $response->setContent($content);
    $response->send();
  }

}
  • linha 29: verifica-se se é necessário lidar com solicitações entre domínios. Se for o caso, geram-se os cabeçalhos HTTP e CORS (linhas 33-37), mesmo que a solicitação atual não seja uma solicitação entre domínios. Nesse último caso, os cabeçalhos CORS serão desnecessários e não serão utilizados pelo cliente;
  • linha 30: em uma 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 esse cabeçalho HTTP na solicitação [$request];
  • linha 31: só serão aceitas solicitações entre domínios provenientes exclusivamente da máquina [http://localhost]. Vale lembrar que essas solicitações ocorrem apenas no modo de desenvolvimento do projeto;
  • linhas 32-36: adicionam-se os cabeçalhos CORS aos cabeçalhos já presentes na tabela [$headers];
  • linhas 45-49: a forma como o navegador do cliente solicita as autorizações CORS pode variar de acordo com o cliente em execução. Às vezes, o navegador do cliente solicita essas autorizações com um comando HTTP [OPTIONS]. Essa é uma novidade para o nosso servidor, que foi desenvolvido para atender exclusivamente aos comandos [GET, POST]. No caso de um comando [OPTIONS], o servidor atualmente gera uma resposta de erro. Nas linhas 46 a 49, corrigimos isso no último momento: se, na linha 46, constatarmos que o comando atual é um comando [OPTIONS], então geramos para o cliente:
    • linhas 47 e 51: uma resposta [$content] vazia;
    • linha 48: um código de status 200 indicando que o comando foi bem-sucedido. A única coisa importante para esse comando é o envio dos cabeçalhos CORS das linhas 33 a 36. É isso que o navegador do cliente espera;

Depois que o servidor é corrigido dessa 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 [authentifier-utilisateur] falha: o servidor indica que não há nenhuma sessão em andamento. Isso significa que o cliente JavaScript não reenviou corretamente o cookie de sessão que havia enviado durante a ação [init-session];

Vamos examinar as trocas de dados de rede que ocorreram:

Image

  • em [4], a solicitação [init-session]. Ela foi bem-sucedida, com um código 200 como status da resposta;
  • em [5], a solicitação [authentifier-utilisateur]. Esta falhou com um código 400 (Bad Request) [6] como status da resposta;

Se examinarmos os cabeçalhos HTTP e [7] da solicitação [5], percebe-se que o cliente JavaScript não enviou os cabeçalhos HTTP e [Cookie], que lhe permitiriam reenviar o cookie de sessão enviado inicialmente pelo servidor. É por isso que o servidor declara 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]:


// importações
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// importações
import Dao from './Dao2';
import Métier from './Métier';

// função assíncrona [main]
async function main() {
  // configuração do Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  axios.defaults.withCredentials = true;
  // instanciação da camada [dao]
  const dao = new Dao(axios);
  // solicitações HTTP
  let taxAdminData;
...

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

Depois que esse erro for corrigido, o cliente JavaScript funciona normalmente:

Image

Image

14.5. Melhoria no cliente HTTP 3

Quando a classe [Dao2] anterior é executada em um navegador, o gerenciamento do cookie de sessão torna-se desnecessário. De fato, é o navegador que hospeda a camada [dao] que gerencia o cookie de sessão: ele reenvia automaticamente qualquer cookie que o servidor lhe envie. Assim, a classe [Dao2] pode ser reescrita na seguinte classe [Dao3]:


"use strict";

// importações
import qs from "qs";

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

  // inicialização da sessão
  async initSession() {
    // opções da solicitação HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parâmetros da consulta URL
      params: {
        action: "init-session",
        type: "json"
      }
    };
    // execução da consulta HTTP
    return await this.getRemoteData(options);
  }

  async authentifierUtilisateur(user, password) {
    // opções da consulta HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        "Content-type": "application/x-www-form-urlencoded"
      },
      // corpo da consulta POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parâmetros do URL
      params: {
        action: "authentifier-utilisateur"
      }
    };
    // execução da consulta HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // opções da consulta HHTP  [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // parâmetros da consulta URL
      params: {
        action: "get-admindata"
      }
    };
    // execução da consulta HTTP
    const data = await this.getRemoteData(options);
    // resultado
    return data;
  }

  async getRemoteData(options) {
    // execução da consulta HTTP
    let response;
    try {
      // solicitação assíncrona
      response = await this.axios.request("main.php", options);
    } catch (error) {
      // o parâmetro [error] é uma instância de exceção — pode assumir diversas formas
      if (error.response) {
        // a resposta do servidor está em [error.response]
        response = error.response;
      } else {
        // o erro é reenviado
        throw error;
      }
    }
    // a resposta é o conjunto completo da resposta HTTP do servidor (cabeçalhos HTTP + a própria resposta)
    // a resposta do servidor está em [response.data]
    return response.data;
  }
}

// exportação da classe
export default Dao3;

Tudo o que se referia ao gerenciamento do cookie de gerenciamento desapareceu.

Modificamos o projeto anterior da seguinte maneira:

Image

Na pasta [src], adicionamos dois arquivos:

  • a classe [Dao3] que acabamos de apresentar;
  • o arquivo [main3], responsável por iniciar a nova versão;

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


// importações
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";

// importações
import Dao from "./Dao3";
import Métier from "./Métier";

// função assíncrona [main]
async function main() {
  // configuração do Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL =
    "http://localhost/php7/scripts-web/impots/version-14";
  axios.defaults.withCredentials = true;
  // instanciação da camada [dao]
  const dao = new Dao(axios);
  // solicitações HTTP
  ...
}

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

// execução
main();

O arquivo [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",
  //entrada: "./src/mainjs",
  entry: "./src/main3.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },

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

Feito isso, executa-se o projeto após iniciar o servidor de cálculo de impostos:

Image

Os resultados exibidos no console do navegador são idênticos aos da versão anterior.

14.6. Conclusion

Agora já temos todas as ferramentas necessárias para desenvolver o código JavaScript de um aplicativo web. Podemos:

  • utilizar o código ECMAScript mais recente;
  • testar elementos isolados desse código em um ambiente [node.js] mais simples para depuração e testes;
  • em seguida, portar esse código para um navegador usando as ferramentas [babel] e [webpack];