Skip to content

14. Clienti HTTP Javascript del servizio di calcolo delle imposte

14.1. Introduction

In questa sede ci proponiamo di scrivere un client [node.js] della versione 14 del servizio di calcolo delle imposte. L’architettura client/server sarà la seguente:

Image

Esamineremo due versioni del client:

  • la versione 1 del client avrà la seguente struttura a livelli [main, dao]:

Image

  • la versione 2 del client avrà una struttura [main, métier, dao]. Il livello [métier] del server sarà trasferito sul client:

Image

14.2. Client HTTP 1

Image

Come già detto, il client HTTP 1 implementa la seguente architettura client/server:

Image

Implementeremo:

  • il livello [dao] sotto forma di una classe;
  • il livello [main] sotto forma di uno script che utilizza tale classe;

14.2.1. Il livello [dao]

Il livello [dao] sarà implementato dalla seguente classe [Dao1.js]:


'use strict';

// importazioni
import qs from 'qs'

class Dao1 {

  // costruttore
  constructor(axios) {
    // libreria axios per effettuare le richieste HTTP
    this.axios = axios;
    // cookie di sessione
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // avvio sessione
  async  initSession() {
    // opzioni della richiesta HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parametri di URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // esecuzione della query HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // opzioni della richiesta HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // corpo del POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parametri dell'URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // esecuzione della richiesta HTTP
    return await this.getRemoteData(options);
  }

  // calcolo dell'imposta
  async  calculerImpot(marié, enfants, salaire) {
    // opzioni della richiesta HHTP [post /main.php?action=calculer-impot]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // corpo del POST [marié, enfants, salaire]
      data: qs.stringify({
        marié: marié,
        enfants: enfants,
        salaire: salaire
      }),
      // parametri di URL
      params: {
        action: 'calculer-impot'
      }
    };
    // esecuzione della richiesta HTTP
    const data = await this.getRemoteData(options);
    // risultato
    return data;
  }

  // elenco delle simulazioni
  async  listeSimulations() {
    // opzioni della query HHTP [get /main.php?action=lister-simulations]
    const options = {
      method: "GET",
      // parametri di URL
      params: {
        action: 'lister-simulations'
      },
    };
    // esecuzione della richiesta HTTP
    const data = await this.getRemoteData(options);
    // risultato
    return data;
  }

  // elenco delle simulazioni
  async  supprimerSimulation(index) {
    // opzioni della richiesta HHTP  [get /main.php?action=supprimer-simulation&numéro=index]
    const options = {
      method: "GET",
      // parametri di URL
      params: {
        action: 'supprimer-simulation',
        numéro: index
      },
    };
    // esecuzione della richiesta HTTP
    const data = await this.getRemoteData(options);
    // risultato
    return data;
  }

  async  getRemoteData(options) {
    // per il cookie di sessione
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // esecuzione della richiesta HTTP
    let response;
    try {
      // richiesta asincrona
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // il parametro [error] è un'istanza di eccezione - può assumere diverse forme
      if (error.response) {
        // la risposta del server è contenuta in [error.response]
        response = error.response;
      } else {
        // l'errore viene generato nuovamente
        throw error;
      }
    }
    // la risposta è l'insieme della risposta HTTP del server (intestazioni HTTP + la risposta stessa)
    // si recupera il cookie di sessione, se presente
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie è un array
      // si cerca il cookie di sessione in questo array
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // si cerca il cookie di sessione
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // si memorizza il cookie di sessione
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // trovato
          trouvé = true;
        } else {
          // elemento successivo
          i++;
        }
      }
    }
    // la risposta del server è in [response.data]
    return response.data;
  }
}

// esportazione della classe
export default Dao1;
  • qui utilizziamo quanto appreso nel paragrafo "link", dove abbiamo presentato la libreria [axios] che consente di effettuare richieste HTTP sia in [node.js] che in un browser. Esamineremo in particolare lo script del paragrafo "link";
  • righe 9-15: il costruttore della classe. Questa avrà tre proprietà:
    • [axios]: l’oggetto [axios] che consente di effettuare le richieste HTTP. Questo viene trasmesso dal codice chiamante;
    • [sessionCookieName]: a seconda dei server, il cookie di sessione ha nomi diversi. In questo caso è [PHPSESSID];
    • [sessionCookie]: il cookie di sessione inviato dal server e memorizzato dal client;
  • righe 53-76: la funzione asincrona [calculerImpot] effettua la richiesta [post /main.php?action=calculer-impot] inviando i parametri [marié, enfants, salaire]. Restituisce la stringa jSON trasmessa dal server sotto forma di oggetto JavaScript;
  • righe 79-92: la funzione asincrona [listeSimulations] esegue la richiesta [get /main.php?action=lister-simulations. Restituisce la stringa jSON inviata dal server sotto forma di oggetto JavaScript;
  • righe 95-109: la funzione asincrona [supprimerSimulation] effettua la richiesta [get /main.php?action=supprimer-simulation&numéro=index]. Restituisce la stringa jSON trasmessa dal server sotto forma di oggetto JavaScript;
  • riga 121: si utilizza la notazione [this.axios] poiché, in questo caso, l’oggetto [axios] trasmesso al costruttore è stato memorizzato nella proprietà [this.axios];
  • riga 161: la classe [Dao1] viene esportata per poter essere utilizzata;

14.2.2. Lo script [main1.js]

Lo script [main1.js] effettua una serie di chiamate al server utilizzando la classe [Dao1]:

  • inizializzazione di una sessione jSON;
  • autenticazione con [admin, admin];
  • richiede tre calcoli delle imposte;
  • richiede l'elenco delle simulazioni;
  • elimina una di esse;

Il codice è il seguente:


// importazione di axios
import axios from 'axios';
// importazione della classe Dao1
import Dao from './Dao1';

// funzione asincrona [main]
async function main() {
  // configurazione di Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // istanziazione del livello [dao]
  const dao = new Dao(axios);
  // utilizzo del livello [dao]
  try {
    // inizializzazione della sessione
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // autenticazione
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // calcoli fiscali
    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);
    // elenco delle simulazioni
    log("-----------liste-des-simulations");
    response = await dao.listeSimulations();
    log(response);
    // eliminazione di una simulazione
    log("-----------suppression simulation n° 1");
    response = await dao.supprimerSimulation(1);
    log(response);
  } catch (error) {
    // registrazione dell'errore
    console.log("erreur=", error.message);
  }
}

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

// esecuzione
main();

Commenti

  • riga 2: si importa la libreria [axios];
  • riga 4: si importa la classe [Dao];
  • riga 7: la funzione [main] che comunica con il server è asincrona;
  • righe 9-10: configurazione predefinita delle richieste HTTP che verranno inviate al server:
    • riga 9: [timeout] di 2 secondi;
    • riga 10: tutte le URL hanno come prefisso la URL di base della versione 14 del server di calcolo delle imposte;
  • riga 12: viene creato il livello [Dao]. Ora è possibile utilizzarlo;
  • righe 46-48: la funzione [log] ha lo scopo di visualizzare la stringa jSON di un oggetto JavaScript in una forma ottimizzata: in formato verticale con un'indentazione di due spazi (terzo parametro);
  • righe 15-18: inizializzazione della sessione jSON;
  • righe 19-22: autenticazione;
  • righe 23-30: vengono richiesti tre calcoli fiscali in parallelo. Grazie a [await Promise.all], l’esecuzione rimane bloccata finché non vengono ottenuti tutti e tre i risultati;
  • righe 31-34: elenco delle simulazioni;
  • righe 35-38: eliminazione di una simulazione;
  • righe 39-42: gestione di eventuali eccezioni;

I risultati dell’esecuzione sono i seguenti:


[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

L'architettura del client HTTP2 è la seguente:

Image

Il livello [métier] è stato spostato dal server al client JavaScript. A differenza di quanto abbiamo fatto nel corso PHP7, il livello [main] non dovrà qui passare attraverso il livello [métier] per raggiungere il livello [dao]. Utilizzeremo questi due livelli come centri di competenza:

  • il livello [main] passa attraverso il livello [dao] non appena ha bisogno di dati presenti sul server;
  • il livello [main] richiede al livello [métier] di eseguire i calcoli fiscali;
  • il livello [métier] è indipendente dal livello [dao] e non lo chiama mai;

14.3.1. La classe JavaScript [Métier]

L’essenza della classe [Métier] in PHP è stata descritta nell’articolo al link. Si tratta di un codice piuttosto complesso che riportiamo qui, non per spiegarlo, ma per poterlo tradurre in JavaScript:


<?php

// spazio dei nomi
namespace Application;

class Metier implements InterfaceMetier {
  // livello DAO
  private $dao;
  // dati dell’amministrazione fiscale
  private $taxAdminData;

  //---------------------------------------------
  // impostazione del livello [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceDao $dao) {
    // si memorizza un riferimento sul livello [dao]
    $this->dao = $dao;
    // si recuperano i dati necessari per il calcolo dell'imposta
    // il metodo [getTaxAdminData] può generare un'eccezione ExceptionImpots
    // si lascia quindi che l'eccezione venga propagata al codice chiamante
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// calcolo dell'imposta
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié: sì, no
    // $enfants: numero di figli
    // $salaire: stipendio annuo
    // $this->taxAdminData: dati dell'amministrazione fiscale
    //
    // si verifica che siano presenti i dati dell'amministrazione fiscale
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // calcolo dell'imposta con figli
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // calcolo dell'imposta senza figli
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // applicazione del limite massimo del quoziente familiare
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euro per i primi 2 figli
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euro per i primi 2 figli, il doppio per i successivi
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // si applica l'aliquota più 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"];
    }
    // calcolo di un'eventuale riduzione
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // calcolo di un'eventuale riduzione delle imposte
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // risultato
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

// --------------------------------------------------------------------------
  private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
    // $marié: sì, no
    // $enfants: numero di figli
    // $salaire: stipendio annuo
    // $this->taxAdminData: dati dell'amministrazione fiscale
    //
    // numero di quote
    $marié = strtolower($marié);
    if ($marié === "oui") {
      $nbParts = $enfants / 2 + 2;
    } else {
      $nbParts = $enfants / 2 + 1;
    }
    // 1 quota per ogni figlio a partire dal terzo
    if ($enfants >= 3) {
      // mezza quota in più per ogni figlio a partire dal terzo
      $nbParts += 0.5 * ($enfants - 2);
    }
    // reddito imponibile
    $revenuImposable = $this->getRevenuImposable($salaire);
    // maggiorazione
    $surcôte = floor($revenuImposable - 0.9 * $salaire);
    // per problemi di arrotondamento
    if ($surcôte < 0) {
      $surcôte = 0;
    }
    // quoziente familiare
    $quotient = $revenuImposable / $nbParts;
    // calcolo dell'imposta
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // viene inserito alla fine della tabella dei limiti per interrompere il ciclo successivo
    $limites[count($limites) - 1] = $quotient;
    // ricerca dell'aliquota fiscale
    $i = 0;
    while ($quotient > $limites[$i]) {
      $i++;
    }
    // poiché $quotient è stato inserito alla fine della tabella $limites, il ciclo precedente
    // non può andare oltre i limiti della tabella $limites
    // ora è possibile calcolare l’imposta
    $impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
    // risultato
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=stipendioAnnuale-detrazione
  // la detrazione ha un valore minimo e uno massimo
  private function getRevenuImposable(float $salaire): float {
    // detrazione pari al 10% dello stipendio
    $abattement = 0.1 * $salaire;
    // questa detrazione non può superare $this->taxAdminData->getAbattementDixPourCentMax()
    if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMax();
    }
    // la detrazione non può essere inferiore a $this->taxAdminData->getAbattementDixPourcentMin()
    if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMin();
    }
    // reddito imponibile
    $revenuImposable = $salaire - $abattement;
    // risultato
    return floor($revenuImposable);
  }

// calcola un eventuale sconto
  private function getDecôte(string $marié, float $salaire, float $impots): float {
    // inizialmente, uno sconto pari a zero
    $décôte = 0;
    // importo massimo dell'imposta per ottenere la riduzione
    $plafondImpôtPourDécôte = $marié === "oui" ?
      $this->taxAdminData->getPlafondImpotCouplePourDecote() :
      $this->taxAdminData->getPlafondImpotCelibatairePourDecote();
    if ($impots < $plafondImpôtPourDécôte) {
      // importo massimo della riduzione
      $plafondDécôte = $marié === "oui" ?
        $this->taxAdminData->getPlafondDecoteCouple() :
        $this->taxAdminData->getPlafondDecoteCelibataire();
      // sconto teorico
      $décôte = $plafondDécôte - 0.75 * $impots;
      // la riduzione non può superare l'importo dell'imposta
      if ($décôte > $impots) {
        $décôte = $impots;
      }
      // nessuna riduzione <0
      if ($décôte < 0) {
        $décôte = 0;
      }
    }
    // risultato
    return ceil($décôte);
  }

// calcola un'eventuale riduzione
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    // il limite massimo di reddito per avere diritto alla riduzione del 20%
    $plafondRevenuPourRéduction = $marié === "oui" ?
      $this->taxAdminData->getPlafondRevenusCouplePourReduction() :
      $this->taxAdminData->getPlafondRevenusCelibatairePourReduction();
    $plafondRevenuPourRéduction += $enfants * $this->taxAdminData->getValeurReducDemiPart();
    if ($enfants > 2) {
      $plafondRevenuPourRéduction += ($enfants - 2) * $this->taxAdminData->getValeurReducDemiPart();
    }
    // reddito imponibile
    $revenuImposable = $this->getRevenuImposable($salaire);
    // riduzione
    $réduction = 0;
    if ($revenuImposable < $plafondRevenuPourRéduction) {
      // riduzione del 20%
      $réduction = 0.2 * $impots;
    }
    // risultato
    return ceil($réduction);
  }

  // calcolo delle imposte in modalità batch
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // si lasciano risalire le eccezioni provenienti dal livello [dao]
    // si recuperano i dati dei contribuenti
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabella dei risultati
    $results = [];
    // si analizzano
    foreach ($taxPayersData as $taxPayerData) {
      // si calcola l'imposta
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // si completa [$taxPayerData]
      $taxPayerData->setMontant($result["impôt"]);
      $taxPayerData->setDécôte($result["décôte"]);
      $taxPayerData->setSurCôte($result["surcôte"]);
      $taxPayerData->setTaux($result["taux"]);
      $taxPayerData->setRéduction($result["réduction"]);
      // si inserisce il risultato nella tabella dei risultati
      $results [] = $taxPayerData;
    }
    // registrazione dei risultati
    $this->dao->saveResults($resultsFileName, $results);
  }

}
  • righe 19-26: il costruttore della classe PHP. Poiché abbiamo detto che stavamo costruendo un livello [métier] indipendente dal livello [dao], apporteremo in JavaScript due modifiche a questo costruttore:
    • non riceverà un’istanza del livello [dao] (non ne ha più bisogno);
    • non richiederà i dati fiscali dell’amministrazione [taxAdminData] al livello [dao]: sarà il codice chiamante a trasmettere tali dati al costruttore;
  • righe 197-122: non implementeremo il metodo [executeBatchImpots], il cui scopo finale era quello di salvare i risultati delle simulazioni in un file di testo. Vogliamo un codice che funzioni sia su [node.js] che in un browser. Tuttavia, non è possibile salvare dati sul file system del computer su cui è in esecuzione il browser client;

Tenendo conto di queste restrizioni, il codice della classe JavaScript [Métier] è il seguente:


'use strict';

// classe Métier
class Métier {

  // costruttore
  constructor(taxAdmindata) {
    // this.taxAdminData: dati dell'amministrazione fiscale
    this.taxAdminData = taxAdmindata;
  }

  // calcolo dell'imposta
  // --------------------------------------------------------------------------
  calculerImpot(marié, enfants, salaire) {
    // stato civile: sì, no
    // figli: numero di figli
    // stipendio: stipendio annuo
    // this.taxAdminData: dati dell'amministrazione fiscale
    //
    // calcolo dell'imposta con figli
    const result1 = this.calculerImpot2(marié, enfants, salaire);
    const impot1 = result1["impôt"];
    // calcolo dell'imposta senza figli
    let result2, impot2, plafondDemiPart;
    if (enfants !== 0) {
      result2 = this.calculerImpot2(marié, 0, salaire);
      impot2 = result2["impôt"];
      // applicazione del limite massimo del quoziente familiare
      plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
      if (enfants < 3) {
        // PLAFOND_QF_DEMI_PART euro per i primi 2 figli
        impot2 = impot2 - enfants * plafondDemiPart;
      } else {
        // PLAFOND_QF_DEMI_PART euro per i primi 2 figli, il doppio per i successivi
        impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
      }
    } else {
      // nessun ricalcolo dell’imposta
      impot2 = impot1;
      result2 = result1;
    }
    // si prende l’imposta più alta 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"];
    }
    // calcolo di un'eventuale riduzione
    const décôte = this.getDecôte(marié, impot);
    impot -= décôte;
    // calcolo di un'eventuale riduzione delle imposte
    const réduction = this.getRéduction(marié, salaire, enfants, impot);
    impot -= réduction;
    // risultato
    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) {
    // coniugato: sì, no
    // figli: numero di figli
    // stipendio: stipendio annuo
    // this->taxAdminData: dati dell'amministrazione fiscale
    //
    // numero di quote
    marié = marié.toLowerCase();
    let nbParts;
    if (marié === "oui") {
      nbParts = enfants / 2 + 2;
    } else {
      nbParts = enfants / 2 + 1;
    }
    // 1 quota per ogni figlio a partire dal terzo
    if (enfants >= 3) {
      // mezza quota in più per ogni figlio a partire dal terzo
      nbParts += 0.5 * (enfants - 2);
    }
    // reddito imponibile
    const revenuImposable = this.getRevenuImposable(salaire);
    // maggiorazione
    let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
    // per problemi di arrotondamento
    if (surcôte < 0) {
      surcôte = 0;
    }
    // quoziente familiare
    const quotient = revenuImposable / nbParts;
    // calcolo dell'imposta
    const limites = this.taxAdminData.limites;
    const coeffR = this.taxAdminData.coeffR;
    const coeffN = this.taxAdminData.coeffN;
    // viene inserito alla fine della tabella dei limiti per interrompere il ciclo successivo
    limites[limites.length - 1] = quotient;
    // ricerca dell'aliquota fiscale
    let i = 0;
    while (quotient > limites[i]) {
      i++;
    }
    // dato che il quoziente è stato inserito alla fine della tabella dei limiti, il ciclo precedente
    // non può andare oltre i limiti dell'array
    // ora è possibile calcolare l’imposta
    const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
    // risultato
    return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
  }

  // revenuImposable = stipendio annuo - detrazione
  // la detrazione ha un valore minimo e uno massimo
  getRevenuImposable(salaire) {
    // detrazione pari al 10% dello stipendio
    let abattement = 0.1 * salaire;
    // questa detrazione non può superare taxAdminData.getAbattementDixPourCentMax()
    if (abattement > this.taxAdminData.abattementDixPourCentMax) {
      abattement = this.taxAdminData.abattementDixPourcentMax;
    }
    // la detrazione non può essere inferiore a taxAdminData.getAbattementDixPourcentMin()
    if (abattement < this.taxAdminData.abattementDixPourcentMin) {
      abattement = this.taxAdminData.abattementDixPourcentMin;
    }
    // reddito imponibile
    const revenuImposable = salaire - abattement;
    // risultato
    return Math.floor(revenuImposable);
  }

  // calcola un'eventuale riduzione
  getDecôte(marié, impots) {
    // inizialmente, uno sconto pari a zero
    let décôte = 0;
    // importo massimo dell'imposta per ottenere la riduzione
    let plafondImpôtPourDécôte = marié === "oui" ?
      this.taxAdminData.plafondImpotCouplePourDecote :
      this.taxAdminData.plafondImpotCelibatairePourDecote;
    let plafondDécôte;
    if (impots < plafondImpôtPourDécôte) {
      // importo massimo della riduzione
      plafondDécôte = marié === "oui" ?
        this.taxAdminData.plafondDecoteCouple :
        this.taxAdminData.plafondDecoteCelibataire;
      // sconto teorico
      décôte = plafondDécôte - 0.75 * impots;
      // la riduzione non può superare l'importo dell'imposta
      if (décôte > impots) {
        décôte = impots;
      }
      // nessuna riduzione <0
      if (décôte < 0) {
        décôte = 0;
      }
    }
    // risultato
    return Math.ceil(décôte);
  }

  // calcola un'eventuale riduzione
  getRéduction(marié, salaire, enfants, impots) {
    // il limite massimo di reddito per avere diritto alla riduzione del 20%
    let plafondRevenuPourRéduction = marié === "oui" ?
      this.taxAdminData.plafondRevenusCouplePourReduction :
      this.taxAdminData.plafondRevenusCelibatairePourReduction;
    plafondRevenuPourRéduction += enfants * this.taxAdminData.valeurReducDemiPart;
    if (enfants > 2) {
      plafondRevenuPourRéduction += (enfants - 2) * this.taxAdminData.valeurReducDemiPart;
    }
    // reddito imponibile
    const revenuImposable = this.getRevenuImposable(salaire);
    // sconto
    let réduction = 0;
    if (revenuImposable < plafondRevenuPourRéduction) {
      // riduzione del 20%
      réduction = 0.2 * impots;
    }
    // risultato
    return Math.ceil(réduction);
  }
}

// esportazione della classe
export default Métier;
  • il codice JavaScript segue scrupolosamente il codice PHP;
  • la classe [Métier] viene esportata, riga 187;

14.3.2. La classe JavaScript [Dao2]

Image

La classe [Dao2] implementa il livello [dao] del client JavaScript sopra indicato nel modo seguente:


'use strict';

// importazioni
import qs from 'qs'

class Dao2 {

  // costruttore
  constructor(axios) {
    this.axios = axios;
    // cookie di sessione
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // inizializzazione della sessione
  async  initSession() {
    // opzioni della richiesta HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parametri di URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // esecuzione della query HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // opzioni della richiesta HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // corpo del POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parametri di URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // esecuzione della richiesta HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // opzioni della richiesta HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // parametri della query URL
      params: {
        action: 'get-admindata'
      }
    };
    // esecuzione della query HTTP
    const data = await this.getRemoteData(options);
    // risultato
    return data;
  }

  async  getRemoteData(options) {
    // per il cookie di sessione
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // esecuzione della richiesta HTTP
    let response;
    try {
      // richiesta asincrona
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // il parametro [error] è un'istanza di eccezione - può assumere diverse forme
      if (error.response) {
        // la risposta del server è contenuta in [error.response]
        response = error.response;
      } else {
        // l'errore viene generato nuovamente
        throw error;
      }
    }
    // la risposta è l'insieme della risposta HTTP del server (intestazioni HTTP + la risposta stessa)
    // si recupera il cookie di sessione, se presente
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie è un array
      // si cerca il cookie di sessione in questo array
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // si cerca il cookie di sessione
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // si memorizza il cookie di sessione
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // trovato
          trouvé = true;
        } else {
          // elemento successivo
          i++;
        }
      }
    }
    // la risposta del server è in [response.data]
    return response.data;
  }
}

// esportazione della classe
export default Dao2;

Commenti

  • la classe [Dao2] implementa solo tre delle possibili richieste al server di calcolo delle imposte:
    • [init-session] (righe 17-29): per inizializzare la sessione jSON;
    • [authentifier-utilisateur] (righe 31-50): per l'autenticazione;
    • [get-admindata] (righe 52-65): per ottenere i dati dell’amministrazione fiscale che consentiranno di effettuare i calcoli delle imposte sul lato client;
  • righe 52-65: introduciamo una nuova azione [get-admindata] verso il server. Questa azione non era stata implementata fino a quel momento. Lo facciamo ora.

14.3.3. Modifica del server di calcolo dell’imposta

Il server di calcolo dell’imposta deve implementare una nuova azione. Lo faremo sulla versione 14 del server. L’azione da implementare presenta le seguenti caratteristiche:

  • viene richiesta da un’operazione [get /main.php?action=get-admindata];
  • restituisce la stringa jSON di un oggetto che incapsula i dati dell’amministrazione fiscale;

Vedremo ora come aggiungere un'azione al nostro server.

La modifica verrà effettuata in NetBeans:

Image

In [2], modifichiamo il file [config.json] per aggiungere la nuova azione:


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

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

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

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

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

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

La modifica consiste nel:

  • riga 67: aggiungere l’azione [get-admindata] e associarla a un controller;
  • riga 36: dichiarare questo controller nell'elenco delle classi da caricare dall'applicazione PHP;

La fase successiva consiste nell’implementare il controller [AdminDataController] [3]:


<?php

namespace Application;

// dipendenze Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias del livello [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

class AdminDataController implements InterfaceController {

  // $config è la configurazione dell'applicazione
  // elaborazione di una richiesta Request
  // utilizza la sessione Session e può modificarla
  // $infos sono informazioni aggiuntive specifiche per ciascun controller
  // restituisce un array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // deve esserci un unico parametro GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      // si rileva l'errore
      $message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
      $état = 1001;
      // invio del risultato al controller principale
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }

    // si può procedere
    // Redis
    \Predis\Autoloader::register();
    try {
      // client [predis]
      $redis = new \Predis\Client();
      // ci si connette al server per verificare se è presente
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // c'è stato un problema
      // risultato restituito con errore al controller principale
      $état = 1050;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }

    // recupero dei dati dall'amministrazione fiscale
    // si cerca prima nella cache [redis]
    if (!$redis->get("taxAdminData")) {
      try {
        // si recuperano i dati fiscali dal database
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // taxAdminData
        $taxAdminData = $dao->getTaxAdminData();
        // si inseriscono in Redis i dati recuperati
        $redis->set("taxAdminData", $taxAdminData);
      } catch (\RuntimeException $ex) {
        // C'è stato un errore
        // si restituisce il risultato con errore al controller principale
        $état = 1041;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // i dati fiscali vengono prelevati dalla memoria [redis] con ambito [application]
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      // viene istanziato un oggetto [TaxAdminData] a partire dalla tabella degli attributi precedente
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
    }

    // restituisce il risultato al controller principale
    $état = 1000;
    return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
  }

}

Commenti

  • riga 12: come gli altri controller del server, [AdminDataController] implementa l’interfaccia [InterfaceController] costituita dal metodo [execute] delle righe 19-79;
  • riga 78: come per gli altri controller del server, il metodo [AdminDataController.execute] restituisce un array [$status, $état, [‘réponse’=>$response]] con:
    • [$status]: il codice di stato della risposta HTTP;
    • [$état]: un codice interno all’applicazione che rappresenta lo stato in cui si trova il server dopo l’esecuzione della richiesta del client;
    • [$response]: un array che incapsula la risposta da inviare al client. In questo caso, tale array verrà successivamente trasformato nella stringa jSON;
  • righe 25-34: si verifica che l’azione [get-admindata] del client sia sintatticamente corretta;
  • righe 37-74: si recupera un oggetto [TaxAdminData] trovato:
    • righe 56-59: nel database, se non è stato trovato nella cache [redis];
    • righe 70-73: nella cache [redis];

Questo codice riprende quello del controller [CalculerImpotController] spiegato nell’articolo al link. Infatti, anche questo controller doveva recuperare l’oggetto [TaxAdminData] che incapsulava i dati dell’amministrazione fiscale.

Durante i test del client JavaScript, la forma jSON di [TaxAdminData] ha causato problemi quando tale oggetto è stato trovato nella cache [redis]. Per comprenderlo, esaminiamo in quale forma tale oggetto è memorizzato in [redis]:

Image

Image

  • In [5-7] si nota che i valori numerici sono stati memorizzati sotto forma di stringhe di caratteri. PHP ha risolto il problema perché l’operatore + nei calcoli tra numeri e stringhe provoca implicitamente una conversione del tipo da stringa a numero. Ma JavaScript fa il contrario: l’operatore + nei calcoli tra numeri e stringhe provoca implicitamente una conversione del tipo da numero a stringa. I calcoli della classe JavaScript [Métier] risultano quindi errati;

Per risolvere questo problema, modifichiamo il metodo [TaxAdminData.setFromArrayOfAttributes] utilizzato alla riga 71 del controller per istanziare un oggetto [TaxAdminData] (cfr. articolo) a partire dalla stringa jSON presente nella cache [redis]:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // scaglioni fiscali
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // costanti di calcolo dell'imposta
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // inizializzazione
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // genitore
    parent::setFromJsonFile($taxAdminDataFilename);
    // si verificano i valori degli attributi
    $this->checkAttributes();
    // si restituisce l'oggetto
    return $this;
  }

  protected function check($value): \stdClass {
    // $value è un array di elementi di tipo stringa o un singolo elemento
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // si trasforma l'array di stringhe in un array di numeri reali
    $newTableau = [];
    $result = new \stdClass();
    // gli elementi dell'array devono essere numeri decimali positivi o pari a zero
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // si inserisce il numero in virgola mobile in newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // si annota l'errore
        $result->erreur = TRUE;
        // si esce
        return $result;
      }
    }
    // si restituisce il risultato
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // un unico valore
      $result->value = $newTableau[0];
    } else {
      // un elenco di valori
      $result->value = $newTableau;
    }
    return $result;
  }

  // inizializzazione tramite un array di attributi
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // genitore
    parent::setFromArrayOfAttributes($arrayOfAttributes);
    // si verificano i valori degli attributi
    $this->checkAttributes();
    // si restituisce l'oggetto
    return $this;
  }

  // verifica dei valori degli attributi
  protected function checkAttributes() {
    // si verifica che i valori degli attributi siano numeri reali >=0
    foreach ($this as $key => $value) {
      if (is_string($value)) {
        // $value deve essere un numero reale >=0 o un array di numeri reali >=0
        $result = $this->check($value);
        // errore?
        if ($result->erreur) {
          // viene generata un'eccezione
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // si registra il valore
          $this->$key = $result->value;
        }
      }
    }

    // si restituisce l'oggetto
    return $this;
  }

  // getter e setter
  ...

}

Commenti

  • riga 5: la classe [TaxAdminData] estende la classe [BaseEntity], che dispone già del metodo [setFromArrayOfAttributes]. Poiché quest'ultimo non è adatto, lo ridefiniamo alle righe 67-75;
  • riga 70: il metodo [setFromArrayOfAttributes] della classe padre viene utilizzato innanzitutto per inizializzare gli attributi della classe;
  • riga 72: il metodo [checkAttributes] verifica che i valori associati siano effettivamente numeri. Se si tratta di stringhe, queste vengono convertite in numeri;
  • riga 74: l’oggetto [$this] così generato è quindi un oggetto con attributi dai valori numerici;
  • righe 78-93: il metodo [checkAttributes] verifica che i valori associati agli attributi dell’oggetto siano effettivamente numerici;
  • riga 80: si scorre l’elenco degli attributi;
  • riga 81: se il valore di un attributo è di tipo [string];
  • riga 83: allora si verifica che tale stringa rappresenti un numero;
  • riga 90: in tal caso, la stringa viene convertita in un numero e assegnata all’attributo verificato;
  • righe 85-86: in caso contrario, viene generata un'eccezione;
  • righe 32-65: la funzione [check] fa un po’ più del necessario. Gestisce sia gli array che i valori singoli. In questo caso, però, viene chiamata solo per verificare un valore di tipo [string]. Restituisce un oggetto con le proprietà [erreur, value] dove:
    • [erreur] è un valore booleano che indica la presenza o meno di un errore;
    • [value] è il parametro [value] della riga 32, trasformato in un numero o in un array di numeri a seconda dei casi;

La classe [BaseEntity], che poteva avere un attributo denominato [arrayOfAttributes], viene modificata in modo da non avere più tale attributo: esso infatti contamina la stringa jSON con [TaxAdminData]. La classe viene riscritta come segue:


<?php

namespace Application;

class BaseEntity {
  
  // inizializzazione da un file JSON
  public function setFromJsonFile(string $jsonFilename) {
    // si recupera il contenuto del file dei dati fiscali
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // errore?
    if (!$fileContents) {
      // si registra l'errore
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // si recupera il codice JSON dal file di configurazione in un array associativo
      $arrayOfAttributes = \json_decode($fileContents, true);
      // errore?
      if ($arrayOfAttributes === FALSE) {
        // si registra l'errore
        $erreur = TRUE;
        $message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // errore?
    if ($erreur) {
      // si genera un'eccezione
      throw new ExceptionImpots($message);
    }
    // inizializzazione degli attributi della classe
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // si verifica la presenza di tutti gli attributi
    $this->checkForAllAttributes($arrayOfAttributes);
    // si restituisce l'oggetto
    return $this;
  }

  public function checkForAllAttributes($arrayOfAttributes) {
    // si verifica che tutte le chiavi siano state inizializzate
    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) {
    // si inizializzano alcuni attributi della classe (non necessariamente tutti)
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // si restituisce l'oggetto
    return $this;
  }

  // toString
  public function __toString() {
    // attributi dell'oggetto
    $arrayOfAttributes = \get_object_vars($this);
    // stringa jSON dell'oggetto
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }

}

Commenti

  • riga 20: l’attributo [$this→arrayOfAttributes] è stato trasformato in una variabile che d’ora in poi dovrà essere passata al metodo [checkForAllAttributes], riga 38, che in precedenza operava sull’attributo [$this→arrayOfAttributes];

A causa di questa modifica su [BaseEntity], anche la classe [Database] deve essere leggermente modificata:


<?php

namespace Application;

class Database extends BaseEntity {
  // attributi
  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
  // inizializzazione
  public function setFromJsonFile(string $jsonFilename) {
    // genitore
    parent::setFromJsonFile($jsonFilename);
    // viene restituito l'oggetto
    return $this;
  }

  // getters e setters
  ...
}

Commenti

  • nel codice originale, dopo la riga 30, veniva chiamato il metodo [parent::checkForAllAttributes]. Ciò non è più necessario poiché ora viene gestito automaticamente dal metodo [parent::setFromJsonFile($jsonFilename)];

14.3.4. Test del metodo [Postman] sul server

[Postman] è stato presentato nell’articolo link.

Utilizziamo i seguenti test Postman:

Image

Image

Image

Il risultato jSON di quest'ultima richiesta è il seguente:

Image

  • in [5-8], si può notare che gli attributi della stringa jSON hanno effettivamente valori numerici (e non stringhe di caratteri). Questo risultato consentirà alla classe JavaScript [Métier] di eseguirsi normalmente;

14.3.5. Lo script principale [main]

Image

Lo script principale [main] del client JavaScript è il seguente:


// importazioni
import axios from 'axios';

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

// funzione asincrona [main]
async function main() {
  // configurazione di Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // istanziazione del livello [dao]
  const dao = new Dao(axios);
  // richieste HTTP
  let taxAdminData;
  try {
    // inizializzazione sessione
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // autenticazione
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // dati fiscali
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response.réponse;
  } catch (error) {
    // registrazione dell'errore
    console.log("erreur=", error.message);
    // fine
    return;
  }

  // istanziazione del livello [métier]
  const métier = new Métier(taxAdminData);

  // calcoli fiscali
  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));
  // elenco delle simulazioni
  log("-----------liste-des-simulations");
  log(simulations);
  // eliminazione di una simulazione
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

// esecuzione
main();

Commenti

  • righe 5-6: importazione delle classi [Dao] e [Métier];
  • riga 9: la funzione asincrona [main] che gestirà la comunicazione con il server tramite la classe [Dao] e richiederà alla classe [Métier] di eseguire i calcoli fiscali;
  • righe 10-36: lo script chiama in successione e in modo bloccante i metodi [initSession, authentifierUtilisateur, getAdminData] del livello [dao];
  • riga 38: non è più necessario il livello [dao]. Si dispone di tutti gli elementi per far funzionare il livello [métier] del client JavaScript;
  • righe 41-46: si effettuano tre calcoli delle imposte, i cui risultati vengono sommati in una tabella [simulations];
  • riga 49: si visualizza la tabella delle simulazioni;
  • riga 52: se ne elimina una;

I risultati dell’esecuzione dello script principale sono i seguenti:


[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

In questa sezione, apriamo l’applicazione [Client HTTP 2] in un browser secondo la seguente struttura:

Image

Il porting non è immediato. Sebbene [node.js] sia in grado di eseguire il JavaScript ES6, ciò non vale in generale per i browser. È quindi necessario utilizzare strumenti che traducano il codice ES6 in codice ES5 comprensibile dai browser recenti. Fortunatamente questi strumenti sono potenti e piuttosto semplici da usare.

In questo caso abbiamo seguito l’articolo [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].

Nella cartella [client HTTP 3/src] abbiamo inserito gli elementi [main.js, Métier.js, Dao2.js] dell’applicazione [Client Http 2] che abbiamo appena sviluppato.

14.4.1. Inizializzazione del progetto

Lavoreremo nella cartella [client http 3]. Apriamo un terminale in [VSCode] e ci posizioniamo su questa cartella:

Image

Inizializziamo questo progetto con il comando [npm init] e accettiamo le risposte predefinite alle domande poste:

Image

  • in [4-5], il file di configurazione del progetto [package.json] generato in base alle diverse risposte fornite;

14.4.2. Installazione delle dipendenze del progetto

Installeremo le seguenti dipendenze:

  • [@babel/core]: il nucleo dello strumento [Babel] [https://babeljs.io] che trasforma il codice ES 2015+ in codice eseguibile sui browser recenti e meno recenti;
  • [@babel/preset-env]: fa parte della suite di strumenti Babel. Interviene prima della transpilazione da ES6 a ES5;
  • [babel-loader]: questa dipendenza consente allo strumento [webpack] di richiamare lo strumento [Babel];
  • [webpack]: coordinatore. È [webpack] che richiama Babel per eseguire la transpilazione dei codici da ES6 a ES5, per poi assemblare tutti i file risultanti in un unico file;
  • [webpack-cli]: necessario per [webpack];
  • [@webpack-cli/init]: utilizzato per configurare [webpack];
  • [webpack-dev-server]: fornisce un server web di sviluppo che opera di default sulla porta 8080. Quando i file sorgente vengono modificati, ricarica automaticamente l'applicazione web;

Le dipendenze del progetto vengono installate come segue in un terminale di [VSCode]:

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

Image

Dopo l’installazione delle dipendenze, il file [package.json] è stato modificato come segue:


{
  "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"
  }
}
  • righe 12-19: le dipendenze del progetto sono [devDependencies]: sono necessarie durante la fase di sviluppo, ma non più in quella di produzione. Infatti, in produzione viene utilizzato il file [dist/main.js]. È codificato in ES5 e non necessita più degli strumenti di transpilazione da codice ES6 a codice ES5;

Dobbiamo aggiungere due dipendenze al progetto:

  • [core-js]: contiene dei «polyfill» per ECMAScript 2019. Un polyfill consente di eseguire un codice recente, come ECMAScript 2019 (settembre 2019), su browser meno recenti;
  • [regenerator-runtime]: secondo il sito della libreria --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];

Queste due dipendenze sostituiscono, a partire da Babel 7, la dipendenza [@babel/polyfill] che in precedenza svolgeva questo ruolo e che ora (settembre 2019) è deprecata. Si installano nel modo seguente:

Image

Il file [package.json] viene quindi modificato come segue:


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

L’utilizzo delle dipendenze [core-js, regenerator-runtime] richiede di inserire i seguenti [imports] (righe 3-4) nello script principale [src/main.js]:


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

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

14.4.3. Configurazione di [webpack]

[webpack] è lo strumento che gestirà:

  • la transpilazione da ES6 a ES5 di tutti i file JavaScript del progetto;
  • l’unione dei file generati in un unico file;

Questo strumento è gestito da un file di configurazione [webpack.config.js] che può essere generato tramite una dipendenza denominata [@webpack-cli/init] (settembre 2019). Quest’ultima è stata installata insieme alle altre indicate nel paragrafo "link".

Eseguiamo il comando [npx webpack-cli init] in un terminale [VSCode]:

Image

Dopo aver risposto alle varie domande (per le quali è possibile accettare la maggior parte delle risposte predefinite), viene generato un file [webpack.config.js] nella directory principale del progetto [4]:

Il file [webpack.config.js] ha il seguente aspetto:


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

Non comprendo tutti i dettagli di questo file, ma si possono notare alcuni punti:

  • riga 1: il file non contiene il codice ES6. [Eslint] segnala quindi degli errori che risalgono fino alla radice del progetto [javascript]. È fastidioso. Per impedire a ESLint di analizzare un file, è sufficiente commentare la riga 1;
  • riga 31: si lavora in modalità [développement];
  • riga 32: lo script di input è qui [src/index.js]. Dovremo modificarlo;
  • riga 36: la cartella in cui verranno salvati i prodotti di [webpack] sarà la cartella [dist];
  • riga 46: si nota che [webpack] utilizza [babel-loader], una delle dipendenze che abbiamo installato;
  • riga 54: si vede che [webpack] utilizza [@babel-preset/env], una delle dipendenze che abbiamo installato;

L’inizializzazione di [webpack] ha modificato il file [package.json] (richiede l’autorizzazione):


{
  "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"
  }
}
  • riga 4: è stata modificata;
  • righe 8-9, 18-19: sono state aggiunte;
  • riga 8: l'attività [npm] che consente di compilare il progetto;
  • riga 9: l'attività [npm] che consente di eseguirlo;
  • riga 18: ?
  • riga 19: consente la generazione di un file [dist/index.html] che incorpora automaticamente lo script [dist/main.js] generato da [webpack] ed è proprio quest'ultimo che viene utilizzato quando il progetto viene eseguito;

Infine, la configurazione di [webpack] ha generato un file [src/index.js]:

Image

Il contenuto di [index.js] è il seguente (settembre 2019):


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

14.4.4. Compilazione ed esecuzione del progetto

Il file [package.json] contiene tre attività [npm]:


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

Queste attività sono incluse in [VSCode], che le propone per l’esecuzione:

Image

  • in [1-3], si compila il progetto;
  • in [4]: il progetto viene compilato in [dist/main.hash.js] e viene creata una pagina [dist/index.html];

La pagina [index.html] generata è la seguente:


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

Questa pagina si limita quindi a incapsulare il file [main.hash.js] generato da [webpack].

Il progetto viene eseguito dall’attività [start]:

Image

La pagina [dist/index.html] viene quindi caricata su un server, appartenente alla suite [webpack], operante sulla porta 8080 del computer locale e visualizzata dal browser predefinito del computer:

Image

  • in [2], la porta di servizio del server web di [webpack];
  • in [3], il corpo della pagina [dist/index.html] è vuoto;
  • in [4], la scheda [console] degli strumenti di sviluppo del browser, in questo caso Firefox (F12);
  • in [5], il risultato dell'esecuzione del file [src/index.js]. Si ricorda che il contenuto di quest'ultimo era il seguente:
console.log("Hello World from your main file!");

Ora modifichiamo tale contenuto con la seguente riga:

console.log("Bonjour le monde");

Automaticamente (senza ricompilare), vengono generati nuovi file [main.js, index.html] e il nuovo file [index.html] viene caricato nel browser:

Image

Non è necessario eseguire l’attività [build] prima dell’attività [start]: quest’ultima compila infatti prima il progetto. Non memorizza i risultati di tale compilazione nella cartella [dist]. Per rendersene conto, è sufficiente eliminare tale cartella. Si noterà quindi che l’attività [start] compila ed esegue il progetto senza creare la cartella [dist]. Sembra che i suoi output [index.html, main.hash.js] vengano memorizzati in una cartella specifica di [webpackdev-server]. Questo comportamento è sufficiente per i nostri test.

Quando il server di sviluppo è in esecuzione, qualsiasi modifica salvata in uno dei file del progetto provoca una ricompilazione. Per questo motivo, disabilitiamo la modalità [Auto Save] di [VSCode]. Infatti, non vogliamo che avvenga una ricompilazione ogni volta che si digitano caratteri in uno dei file del progetto. Vogliamo che la ricompilazione avvenga solo al momento del salvataggio delle modifiche:

Image

  • in [2], l’opzione [Auto Save] non deve essere selezionata;

14.4.5. Test del client JavaScript del server di calcolo delle imposte

Per testare il client JavaScript del server di calcolo delle imposte, è necessario indicare [main.js] [1] come punto di ingresso del progetto nel file [webpack.config.js] [2-3]:

Image

Non dimentichiamo che lo script [main.js] deve includere due importazioni aggiuntive rispetto alla sua versione in [Client http 2]:

Image

Inoltre, abbiamo leggermente modificato il codice per gestire gli errori che il server potrebbe inviare:


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

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

// funzione asincrona [main]
async function main() {
  // configurazione di axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // istanziazione del livello [dao]
  const dao = new Dao(axios);
  // richieste HTTP
  let taxAdminData;
  try {
    // inizializzazione sessione
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    if (response.état != 700) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // autenticazione
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    if (response.état != 200) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // dati fiscali
    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) {
    // registrazione dell'errore
    console.log("erreur=", error.message);
    // fine
    return;
  }

  // istanziazione del livello [métier]
  const métier = new Métier(taxAdminData);

  // calcoli fiscali
  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));
  // elenco delle simulazioni
  log("-----------liste-des-simulations");
  log(simulations);
  // eliminazione di una simulazione
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

// esecuzione
main();

Commenti

  • nelle righe [24-26], [31-33], [38-40], viene verificato il codice [response.état] inviato nella risposta jSON del server. Se tale codice indica un errore, viene generata un’eccezione con come messaggio di errore la stringa jSON contenuta nella risposta del server [response.réponse];

Fatto ciò, si esegue il progetto [5-6].

Viene quindi generata la pagina [index.html] e caricata nel browser:

Image

  • in [7], si nota che l’azione [init-session] non è riuscita a completarsi a causa di un problema [CORS] (Cross-Origin Resource Sharing);

Il problema CORS deriva dalla relazione client/server:

  • il nostro client JavaScript è stato caricato sul computer [http://localhost:8080];
  • il server di calcolo delle imposte è in esecuzione sul computer [http://localhost:80];
  • il client e il server non si trovano quindi negli stessi domini (stesso computer ma porta diversa);
  • il browser che esegue il client JavaScript caricato dalla macchina [http://localhost:8080] blocca qualsiasi richiesta che non abbia come destinazione [http://localhost:80]. Si tratta di una misura di sicurezza. Pertanto, blocca anche la richiesta del client verso il server che opera sulla macchina [http://localhost:80];

In realtà, il browser non blocca completamente la richiesta. Attende infatti che il server gli «comunichi» di accettare le richieste interdominio. Se ottiene tale autorizzazione, il browser trasmetterà quindi la richiesta interdominio.

Il server concede l’autorizzazione inviando particolari intestazioni HTTP:

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
  • riga 1: il client JavaScript opera sul dominio [http://localhost:8080]. Il server deve rispondere esplicitamente che accetta questo dominio;
  • riga 2: il client JavaScript utilizzerà nelle sue richieste le intestazioni HTTP e [Accept, Content-Type]:
    • [Accept]: questa intestazione viene inviata in ogni richiesta;
    • [Content-Type]: questa intestazione viene utilizzata nelle operazioni POST per indicare il tipo dei parametri di POST;

Il server deve accettare esplicitamente queste due intestazioni HTTP;

  • riga 3: il client JavaScript utilizzerà le richieste GET e POST. Il server deve accettare esplicitamente questi due tipi di richieste;
  • riga 4: il client JavaScript invierà cookie di sessione. Il server li accetta con l’intestazione della riga 4;

Dobbiamo quindi modificare il server. Lo facciamo in [Netbeans]. Il problema relativo a CORS si verifica esclusivamente in modalità di sviluppo. In produzione, il client e il server opereranno nello stesso dominio [http://localhost:80] e non si verificherà alcun problema CORS. Dobbiamo quindi trovare un modo per autorizzare o meno le richieste CORS tramite la configurazione del server.

Image

Le modifiche al server vanno effettuate in tre punti:

  • [1, 4]: nel file di configurazione [config.json] per inserire un valore booleano che determinerà se accettare o meno le richieste interdominio;
  • [2]: nella classe [ParentResponse] che invia la risposta al client JavaScript. È questa classe che invierà le intestazioni CORS attese dal browser client;
  • [3]: nelle classi [HtmlResponse, JsonResponse, XmlResponse] che generano le risposte rispettivamente per le sessioni [html, json, xml]. Queste classi devono passare alla loro classe padre [2] il valore booleano [corsAllowed] presente in [4]. Ciò avviene in [5], passando l’array delle immagini dal file jSON a [2];

La classe [ParentResponse] [2] si evolve come segue:


<?php

namespace Application;

// dipendenze Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ParentResponse {

  // int $statusCode: codice di stato della risposta HTTP
  // stringa $content: il corpo della risposta da inviare
  // a seconda dei casi, è una stringa JSON, XML, HTML
  // array $headers: le intestazioni HTTP da aggiungere alla risposta

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

    // preparazione della risposta testuale del server
    $response = new Response();
    $response->setCharset("utf-8");
    // codice di stato
    $response->setStatusCode($statusCode);
    // intestazioni per le richieste interdominio
    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 particolare del metodo [OPTIONS]
    // in questo caso sono importanti solo le intestazioni
    $method = strtolower($request->getMethod());
    if ($method === "options") {
      $content = "";
      $response->setStatusCode(Response::HTTP_OK);
    }
    // si invia la risposta
    $response->setContent($content);
    $response->send();
  }

}
  • riga 29: si verifica se è necessario gestire le richieste interdominio. In caso affermativo, si generano le intestazioni HTTP CORS (righe 33-37) anche se la richiesta corrente non è una richiesta interdominio. In quest’ultimo caso, le intestazioni CORS saranno superflue e non verranno utilizzate dal client;
  • riga 30: in una richiesta inter-dominio, il browser client che interroga il server invia un'intestazione HTTP [Origin: http://localhost:8080] (nel caso specifico del nostro client JavaScript). Riga 30: si recupera questa intestazione HTTP nella richiesta [$request];
  • riga 31: si accetteranno richieste interdominio provenienti esclusivamente dal computer [http://localhost]. Si ricorda che queste richieste avvengono solo in modalità di sviluppo del progetto;
  • righe 32-36: si aggiungono le intestazioni CORS alle intestazioni già presenti nella tabella [$headers];
  • righe 45-49: il modo in cui il browser client richiede le autorizzazioni CORS può variare a seconda del client in esecuzione. A volte capita che il browser client richieda tali autorizzazioni con un comando HTTP [OPTIONS]. Si tratta di una novità per il nostro server, che è stato progettato per gestire esclusivamente i comandi [GET, POST]. Nel caso di un comando [OPTIONS], il server attualmente genera una risposta di errore. Alle righe 46-49, correggiamo questo comportamento all’ultimo momento: se alla riga 46 constatiamo che il comando corrente è un comando [OPTIONS], allora generiamo per il client:
    • righe 47, 51: una risposta [$content] vuota;
    • riga 48: un codice di stato 200 che indica che l’ordine è andato a buon fine. L’unica cosa importante per questo comando è l’invio delle intestazioni CORS delle righe 33-36. È ciò che si aspetta il browser del cliente;

Una volta corretto il server in questo modo, il client JavaScript funziona meglio ma genera un nuovo errore:

Image

  • in [1], la sessione jSON viene inizializzata correttamente;
  • in [2], l’azione [authentifier-utilisateur] fallisce: il server segnala che non c’è alcuna sessione in corso. Ciò significa che il client JavaScript non gli ha restituito correttamente il cookie di sessione che aveva inviato durante l’azione [init-session];

Esaminiamo gli scambi di rete che hanno avuto luogo:

Image

  • da , la richiesta [init-session]. Si è svolta correttamente con un codice 200 come stato della risposta;
  • in [5], la richiesta [authentifier-utilisateur]. Quest’ultima fallisce con un codice 400 (Bad Request) [6] come stato della risposta;

Se si esaminano le intestazioni HTTP [7] della richiesta [5], si può notare che il client JavaScript non ha inviato l’intestazione HTTP [Cookie] che gli avrebbe permesso di rinviare il cookie di sessione inviato inizialmente dal server. Questo è il motivo per cui il server dichiara che non esiste alcuna sessione.

Affinché il client invii il cookie di sessione, è necessario aggiungere una configurazione all’oggetto [axios]:


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

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

// funzione asincrona [main]
async function main() {
  // configurazione di Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  axios.defaults.withCredentials = true;
  // istanziazione del livello [dao]
  const dao = new Dao(axios);
  // richieste HTTP
  let taxAdminData;
...

La riga 15 richiede che i cookie siano inclusi nelle intestazioni HTTP della richiesta [axios]. Si noti che ciò non era stato necessario nell’ambiente [node.js]. Esistono quindi differenze di codice tra i due ambienti.

Una volta corretto questo errore, il client JavaScript funziona normalmente:

Image

Image

14.5. Miglioramento del client HTTP 3

Quando la classe precedente [Dao2] viene eseguita all’interno di un browser, la gestione del cookie di sessione non è necessaria. Infatti, è il browser che ospita il livello [dao] a gestire il cookie di sessione: esso rinvia automaticamente qualsiasi cookie che il server gli invia. Di conseguenza, la classe [Dao2] può essere riscritta nella seguente classe [Dao3]:


"use strict";

// importazioni
import qs from "qs";

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

  // inizializzazione sessione
  async initSession() {
    // opzioni della richiesta HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parametri di URL
      params: {
        action: "init-session",
        type: "json"
      }
    };
    // esecuzione della query HTTP
    return await this.getRemoteData(options);
  }

  async authentifierUtilisateur(user, password) {
    // opzioni della query HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        "Content-type": "application/x-www-form-urlencoded"
      },
      // corpo del POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parametri di URL
      params: {
        action: "authentifier-utilisateur"
      }
    };
    // esecuzione della query HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // opzioni della richiesta HHTP  [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // parametri di URL
      params: {
        action: "get-admindata"
      }
    };
    // esecuzione della query HTTP
    const data = await this.getRemoteData(options);
    // risultato
    return data;
  }

  async getRemoteData(options) {
    // esecuzione della richiesta HTTP
    let response;
    try {
      // richiesta asincrona
      response = await this.axios.request("main.php", options);
    } catch (error) {
      // il parametro [error] è un'istanza di eccezione - può assumere diverse forme
      if (error.response) {
        // la risposta del server si trova in [error.response]
        response = error.response;
      } else {
        // l'errore viene generato nuovamente
        throw error;
      }
    }
    // la risposta è l'insieme della risposta HTTP del server (intestazioni HTTP + la risposta stessa)
    // la risposta del server si trova in [response.data]
    return response.data;
  }
}

// esportazione della classe
export default Dao3;

Tutto ciò che riguardava la gestione del cookie di gestione è scomparso.

Modifichiamo il progetto precedente nel modo seguente:

Image

Nella cartella [src] abbiamo aggiunto due file:

  • la classe [Dao3] che abbiamo appena presentato;
  • il file [main3] incaricato di avviare la nuova versione;

Il file [main3] rimane identico al file [main] della versione precedente, ma ora utilizza la classe [Dao3]:


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

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

// funzione asincrona [main]
async function main() {
  // configurazione di Axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL =
    "http://localhost/php7/scripts-web/impots/version-14";
  axios.defaults.withCredentials = true;
  // istanziazione del livello [dao]
  const dao = new Dao(axios);
  // richieste HTTP
  ...
}

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

// esecuzione
main();

Il file [webpack.config] è stato modificato per eseguire ora lo 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()],
...
};

Fatto ciò, si esegue il progetto dopo aver avviato il server di calcolo delle imposte:

Image

I risultati ottenuti nella console del browser sono identici a quelli della versione precedente.

14.6. Conclusion

Ora disponiamo di tutti gli strumenti necessari per sviluppare il codice JavaScript di un'applicazione web. Possiamo:

  • utilizzare il codice ECMAScript più recente;
  • testare singoli elementi di questo codice in un ambiente [node.js] più semplice per il debug e i test;
  • trasferire successivamente questo codice in un browser grazie agli strumenti [babel] e [webpack];