14. Clients HTTP Javascript from the tax calculation service
14.1. Introduction
Here, we propose to write a client [node.js] for version 14 of the tax calculation service. The client/server architecture will be as follows:

We will examine two versions of the client:
- Client version 1 will have the following layered structure:

- Client version 2 will have a [main, métier, dao] structure. The server’s [métier] layer will be offloaded to the client:

14.2. Client HTTP 1

As mentioned, the client HTTP 1 implements the following client/server architecture:

We will implement:
- the [dao] layer as a class;
- the [main] layer as a script using this class;
14.2.1. The [dao] layer
The [dao] layer will be implemented by the following class [Dao1.js]:
'use strict';
// imports
import qs from 'qs'
class Dao1 {
// manufacturer
constructor(axios) {
// axios library for queries HTTP
this.axios = axios;
// session cookie
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// init session
async initSession() {
// query options HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// URL parameters
params: {
action: 'init-session',
type: 'json'
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// query options HHTP [post /main.php?action=authenticate-user]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// body of POST
data: qs.stringify({
user: user,
password: password
}),
// URL parameters
params: {
action: 'authentifier-utilisateur'
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
// tAX CALCULATION
async calculerImpot(marié, enfants, salaire) {
// query options HHTP [post /main.php?action=calculate-tax]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// body of POST [married, children, salary]
data: qs.stringify({
marié: marié,
enfants: enfants,
salaire: salaire
}),
// URL parameters
params: {
action: 'calculer-impot'
}
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
// list of simulations
async listeSimulations() {
// query options HHTP [get /main.php?action=lister-simulations]
const options = {
method: "GET",
// URL parameters
params: {
action: 'lister-simulations'
},
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
// list of simulations
async supprimerSimulation(index) {
// query options HHTP [get /main.php?action=suppress-simulation&number=index]
const options = {
method: "GET",
// URL parameters
params: {
action: 'supprimer-simulation',
numéro: index
},
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
async getRemoteData(options) {
// for the session cookie
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// execute query HTTP
let response;
try {
// asynchronous request
response = await this.axios.request('main.php', options);
} catch (error) {
// parameter [error] is an exception instance - it can take various forms
if (error.response) {
// the server response is in [error.response]
response = error.response;
} else {
// error restart
throw error;
}
}
// response is the entire HTTP response from the server (HTTP headers + response itself)
// retrieve the session cookie if it exists
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie is an array
// look for the session cookie in this table
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// look for the session cookie
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// the session cookie is stored
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// we found
trouvé = true;
} else {
// next item
i++;
}
}
}
// the server response is in [response.data]
return response.data;
}
}
// class export
export default Dao1;
- Here we use what we learned in the linked section, where we introduced the [axios] library that allows you to make HTTP requests both in [node.js] and in a browser. We will look in particular at the script in the linked section;
- lines 9–15: the class constructor. This class will have three properties:
- [axios]: the [axios] object used to make HTTP requests. This is passed by the calling code;
- [sessionCookieName]: depending on the server, the session cookie has different names. Here, it is [PHPSESSID];
- [sessionCookie]: the session cookie sent by the server and stored by the client;
- lines 53–76: the asynchronous function [calculerImpot] makes the request [post /main.php?action=calculer-impot] by posting the parameters [marié, enfants, salaire]. It returns the string jSON transmitted by the server in the form of an object Javascript;
- lines 79–92: the asynchronous function [listeSimulations] makes the request [get /main.php?action=lister-simulations. It returns the string jSON transmitted by the server in the form of an object Javascript;
- Lines 95–109: The asynchronous function [supprimerSimulation] makes the request [get /main.php?action=supprimer-simulation&numéro=index]. It returns the string jSON transmitted by the server in the form of an object Javascript;
- line 121: the notation [this.axios] is used because here, the [axios] object passed to the constructor has been stored in the [this.axios] property;
- line 161: the class [Dao1] is exported so it can be used;
14.2.2. The [main1.js] script
The script [main1.js] makes a series of calls to the server using the class [Dao1]:
- initialization of a session jSON;
- authentication with [admin, admin];
- requests three tax calculations;
- requests the list of simulations;
- deletes one of them;
The code is as follows:
// import axios
import axios from 'axios';
// dao1 class import
import Dao from './Dao1';
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// instantiation layer [dao]
const dao = new Dao(axios);
// use of the [dao] layer
try {
// init session
log("-----------init-session");
let response = await dao.initSession();
log(response);
// authentication
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// tax calculations
log("-----------calculer-impot x 3");
response = await Promise.all([
dao.calculerImpot("oui", 2, 45000),
dao.calculerImpot("non", 2, 45000),
dao.calculerImpot("non", 1, 30000)
]);
log(response);
// list of simulations
log("-----------liste-des-simulations");
response = await dao.listeSimulations();
log(response);
// deleting a simulation
log("-----------suppression simulation n° 1");
response = await dao.supprimerSimulation(1);
log(response);
} catch (error) {
// we log the error
console.log("erreur=", error.message);
}
}
// log jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execution
main();
Comments
- line 2: import the [axios] library;
- line 4: import the [Dao] class;
- line 7: the [main] function that communicates with the server is asynchronous;
- lines 9-10: default configuration of the HTTP requests that will be sent to the server:
- line 9: [timeout] with a 2-second timeout;
- line 10: all URL requests are prefixed with the base URL of the tax calculation server’s version 14;
- line 12: the [Dao] layer is built. It can now be used;
- lines 46–48: the function [log] is designed to display the string jSON from an object Javascript in a formatted manner: vertically with a two-space indentation (3rd parameter);
- lines 15–18: initialization of the jSON session;
- lines 19–22: authentication;
- lines 23–30: three tax calculations are requested in parallel. Thanks to [await Promise.all], execution is blocked until all three results have been obtained;
- lines 31–34: list of simulations;
- lines 35–38: deletion of a simulation;
- lines 39–42: handling of any exceptions;
The execution results are as follows:
[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. Client HTTP 2

The architecture of the HTTP2 client is as follows:

The [métier] layer has been moved from the server to the Javascript client. Unlike what we did in the PHP7 course, the [main] layer will not have to pass through the [métier] layer to reach the [dao] layer. We will use these two layers as centers of expertise:
- the [main] layer goes through the [dao] layer whenever it needs data that is on the server;
- the [main] layer asks the [métier] layer to perform the tax calculations;
- The [métier] layer is independent of the [dao] layer and never calls upon it;
14.3.1. The class Javascript [Métier]
The essence of the class [Métier] in PHP was described in the linked article. It is a rather complex code that we recall here, not to explain it, but to be able to translate it into Javascript:
<?php
// namespace
namespace Application;
class Metier implements InterfaceMetier {
// layer Dao
private $dao;
// tax administration data
private $taxAdminData;
//---------------------------------------------
// setter layer [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// we store a reference on the [dao] layer
$this->dao = $dao;
// recover data for tax calculation
// method [getTaxAdminData] may throw a ExceptionImpots exception
// we then let it go back to the calling code
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// tAX CALCULATION
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié : yes, no
// $enfants : number of children
// $salaire: annual salary
// $this->taxAdminData: tax administration data
//
// we check that we have the correct data from the tax authorities
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// tax calculation with children
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// tax calculation without children
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// application of the family allowance ceiling
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros for the first 2 children
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros for the first 2 children, double for subsequent children
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// we take the highest tax
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// calculation of any discount
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// calculation of any tax reduction
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// result
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
// $marié : yes, no
// $enfants : number of children
// $salaire: annual salary
// $this->taxAdminData: tax administration data
//
// number of shares
$marié = strtolower($marié);
if ($marié === "oui") {
$nbParts = $enfants / 2 + 2;
} else {
$nbParts = $enfants / 2 + 1;
}
// 1 part per child from the 3rd
if ($enfants >= 3) {
// an additional half share for each child from the 3rd onwards
$nbParts += 0.5 * ($enfants - 2);
}
// taxable income
$revenuImposable = $this->getRevenuImposable($salaire);
// surcharge
$surcôte = floor($revenuImposable - 0.9 * $salaire);
// for rounding problems
if ($surcôte < 0) {
$surcôte = 0;
}
// family quotient
$quotient = $revenuImposable / $nbParts;
// tAX CALCULATION
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
// is set at the end of the limit table to stop the following loop
$limites[count($limites) - 1] = $quotient;
// tax rate search
$i = 0;
while ($quotient > $limites[$i]) {
$i++;
}
// because $quotient has been placed at the end of the $limites array, the previous loop
// cannot exceed the table $limites
// now we can calculate the tax
$impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
// result
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=annualwage-discount
// the allowance has a minimum and a maximum
private function getRevenuImposable(float $salaire): float {
// 10% salary deduction
$abattement = 0.1 * $salaire;
// this allowance cannot exceed $this->taxAdminData->getAbattementDixPourCentMax()
if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMax();
}
// the allowance cannot be less than $this->taxAdminData->getAbattementDixPourcentMin()
if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMin();
}
// taxable income
$revenuImposable = $salaire - $abattement;
// result
return floor($revenuImposable);
}
// calculates any discount
private function getDecôte(string $marié, float $salaire, float $impots): float {
// at the outset, a zero discount
$décôte = 0;
// maximum tax amount to qualify for discount
$plafondImpôtPourDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondImpotCouplePourDecote() :
$this->taxAdminData->getPlafondImpotCelibatairePourDecote();
if ($impots < $plafondImpôtPourDécôte) {
// maximum discount
$plafondDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondDecoteCouple() :
$this->taxAdminData->getPlafondDecoteCelibataire();
// theoretical discount
$décôte = $plafondDécôte - 0.75 * $impots;
// the discount cannot exceed the amount of tax due
if ($décôte > $impots) {
$décôte = $impots;
}
// no discount <0
if ($décôte < 0) {
$décôte = 0;
}
}
// result
return ceil($décôte);
}
// calculates any reduction
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
// the income ceiling to qualify for the 20% reduction
$plafondRevenuPourRéduction = $marié === "oui" ?
$this->taxAdminData->getPlafondRevenusCouplePourReduction() :
$this->taxAdminData->getPlafondRevenusCelibatairePourReduction();
$plafondRevenuPourRéduction += $enfants * $this->taxAdminData->getValeurReducDemiPart();
if ($enfants > 2) {
$plafondRevenuPourRéduction += ($enfants - 2) * $this->taxAdminData->getValeurReducDemiPart();
}
// taxable income
$revenuImposable = $this->getRevenuImposable($salaire);
// reduction
$réduction = 0;
if ($revenuImposable < $plafondRevenuPourRéduction) {
// 20% discount
$réduction = 0.2 * $impots;
}
// result
return ceil($réduction);
}
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// exceptions from the [dao] layer are allowed to bubble up
// retrieve taxpayer data
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// results table
$results = [];
// we exploit them
foreach ($taxPayersData as $taxPayerData) {
// tax calculation
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// complete [$taxPayerData]
$taxPayerData->setMontant($result["impôt"]);
$taxPayerData->setDécôte($result["décôte"]);
$taxPayerData->setSurCôte($result["surcôte"]);
$taxPayerData->setTaux($result["taux"]);
$taxPayerData->setRéduction($result["réduction"]);
// put the result in the results table
$results [] = $taxPayerData;
}
// recording results
$this->dao->saveResults($resultsFileName, $results);
}
}
- lines 19–26: the constructor of the PHP class. Since we specified that we were constructing a [métier] layer independent of the [dao] layer, we will make two changes to this constructor in Javascript:
- it will not receive an instance of the [dao] layer (it no longer needs it);
- it will not request tax data from the [taxAdminData] administration layer to the [dao] layer: the calling code will transmit this data to the constructor;
- lines 197–122: we will not implement the [executeBatchImpots] method, whose ultimate purpose was to save simulation results to a text file. We want code that works both under [node.js] and in a browser. However, saving data to the file system of the machine running the client browser is not possible;
Given these restrictions, the code for the Javascript and [Métier] classes is as follows:
'use strict';
// job class
class Métier {
// manufacturer
constructor(taxAdmindata) {
// this.taxAdminData: tax administration data
this.taxAdminData = taxAdmindata;
}
// tAX CALCULATION
// --------------------------------------------------------------------------
calculerImpot(marié, enfants, salaire) {
// married: yes, no
// children: number of children
// salary: annual salary
// this.taxAdminData: tax administration data
//
// tax calculation with children
const result1 = this.calculerImpot2(marié, enfants, salaire);
const impot1 = result1["impôt"];
// tax calculation without children
let result2, impot2, plafondDemiPart;
if (enfants !== 0) {
result2 = this.calculerImpot2(marié, 0, salaire);
impot2 = result2["impôt"];
// application of the family allowance ceiling
plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
if (enfants < 3) {
// PLAFOND_QF_DEMI_PART euros for the first 2 children
impot2 = impot2 - enfants * plafondDemiPart;
} else {
// PLAFOND_QF_DEMI_PART euros for the first 2 children, double for subsequent children
impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
}
} else {
// no tax recalculation
impot2 = impot1;
result2 = result1;
}
// we take the highest tax in [impot1, impot2]
let impot, taux, surcôte;
if (impot1 > impot2) {
impot = impot1;
taux = result1["taux"];
surcôte = result1["surcôte"];
} else {
surcôte = impot2 - impot1 + result2["surcôte"];
impot = impot2;
taux = result2["taux"];
}
// calculation of any discount
const décôte = this.getDecôte(marié, impot);
impot -= décôte;
// calculation of any tax reduction
const réduction = this.getRéduction(marié, salaire, enfants, impot);
impot -= réduction;
// result
return {
"impôt": Math.floor(impot), "surcôte": surcôte, "décôte": décôte, "réduction": réduction,
"taux": taux
};
}
// --------------------------------------------------------------------------
calculerImpot2(marié, enfants, salaire) {
// married: yes, no
// children: number of children
// salary: annual salary
// this->taxAdminData: tax administration data
//
// number of shares
marié = marié.toLowerCase();
let nbParts;
if (marié === "oui") {
nbParts = enfants / 2 + 2;
} else {
nbParts = enfants / 2 + 1;
}
// 1 part per child from the 3rd
if (enfants >= 3) {
// an additional half share for each child from the 3rd onwards
nbParts += 0.5 * (enfants - 2);
}
// taxable income
const revenuImposable = this.getRevenuImposable(salaire);
// surcharge
let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
// for rounding problems
if (surcôte < 0) {
surcôte = 0;
}
// family quotient
const quotient = revenuImposable / nbParts;
// tAX CALCULATION
const limites = this.taxAdminData.limites;
const coeffR = this.taxAdminData.coeffR;
const coeffN = this.taxAdminData.coeffN;
// is set at the end of the limit table to stop the following loop
limites[limites.length - 1] = quotient;
// tax rate search
let i = 0;
while (quotient > limites[i]) {
i++;
}
// because we've placed quotient at the end of the limit array, the previous loop
// cannot go beyond the limit table
// now we can calculate the tax
const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
// result
return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
}
// revenuImposable=annualwage-discount
// the allowance has a minimum and a maximum
getRevenuImposable(salaire) {
// 10% salary deduction
let abattement = 0.1 * salaire;
// this allowance cannot exceed taxAdminData.getAbattementDixPourCentMax()
if (abattement > this.taxAdminData.abattementDixPourCentMax) {
abattement = this.taxAdminData.abattementDixPourcentMax;
}
// the allowance cannot be less than taxAdminData.getAbattementDixPourcentMin()
if (abattement < this.taxAdminData.abattementDixPourcentMin) {
abattement = this.taxAdminData.abattementDixPourcentMin;
}
// taxable income
const revenuImposable = salaire - abattement;
// result
return Math.floor(revenuImposable);
}
// calculates any discount
getDecôte(marié, impots) {
// at the outset, a zero discount
let décôte = 0;
// maximum tax amount to qualify for discount
let plafondImpôtPourDécôte = marié === "oui" ?
this.taxAdminData.plafondImpotCouplePourDecote :
this.taxAdminData.plafondImpotCelibatairePourDecote;
let plafondDécôte;
if (impots < plafondImpôtPourDécôte) {
// maximum discount
plafondDécôte = marié === "oui" ?
this.taxAdminData.plafondDecoteCouple :
this.taxAdminData.plafondDecoteCelibataire;
// theoretical discount
décôte = plafondDécôte - 0.75 * impots;
// the discount cannot exceed the amount of tax due
if (décôte > impots) {
décôte = impots;
}
// no discount <0
if (décôte < 0) {
décôte = 0;
}
}
// result
return Math.ceil(décôte);
}
// calculates any reduction
getRéduction(marié, salaire, enfants, impots) {
// the income ceiling to qualify for the 20% reduction
let plafondRevenuPourRéduction = marié === "oui" ?
this.taxAdminData.plafondRevenusCouplePourReduction :
this.taxAdminData.plafondRevenusCelibatairePourReduction;
plafondRevenuPourRéduction += enfants * this.taxAdminData.valeurReducDemiPart;
if (enfants > 2) {
plafondRevenuPourRéduction += (enfants - 2) * this.taxAdminData.valeurReducDemiPart;
}
// taxable income
const revenuImposable = this.getRevenuImposable(salaire);
// reduction
let réduction = 0;
if (revenuImposable < plafondRevenuPourRéduction) {
// 20% discount
réduction = 0.2 * impots;
}
// result
return Math.ceil(réduction);
}
}
// class export
export default Métier;
- The code Javascript closely follows the code PHP;
- The class [Métier] is exported on line 187;
14.3.2. The class Javascript [Dao2]

The class [Dao2] implements the [dao] layer of the above client Javascript as follows:
'use strict';
// imports
import qs from 'qs'
class Dao2 {
// manufacturer
constructor(axios) {
this.axios = axios;
// session cookie
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// init session
async initSession() {
// query options HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// URL parameters
params: {
action: 'init-session',
type: 'json'
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// query options HHTP [post /main.php?action=authenticate-user]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// body of POST
data: qs.stringify({
user: user,
password: password
}),
// URL parameters
params: {
action: 'authentifier-utilisateur'
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// query options HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// URL parameters
params: {
action: 'get-admindata'
}
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
async getRemoteData(options) {
// for the session cookie
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// execute query HTTP
let response;
try {
// asynchronous request
response = await this.axios.request('main.php', options);
} catch (error) {
// parameter [error] is an exception instance - it can take various forms
if (error.response) {
// the server response is in [error.response]
response = error.response;
} else {
// error restart
throw error;
}
}
// response is the entire HTTP response from the server (HTTP headers + response itself)
// retrieve the session cookie if it exists
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie is an array
// look for the session cookie in this table
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// look for the session cookie
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// the session cookie is stored
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// we found
trouvé = true;
} else {
// next item
i++;
}
}
}
// the server response is in [response.data]
return response.data;
}
}
// class export
export default Dao2;
Comments
- The [Dao2] class implements only three of the possible requests to the tax calculation server:
- [init-session] (lines 17–29): to initialize the jSON session;
- [authentifier-utilisateur] (lines 31–50): to authenticate;
- [get-admindata] (lines 52–65): to retrieve data from the tax authority that will enable tax calculations on the client side;
- lines 52-65: we are introducing a new action, [get-admindata], to the server. This action had not been implemented until now. We are doing so now.
14.3.3. Modification of the tax calculation server
The tax calculation server must implement a new action. We will do this on the server’s version 14. The action to be implemented has the following characteristics:
- it is requested by a [get /main.php?action=get-admindata] operation;
- it returns the jSON string of an object encapsulating tax administration data;
We will review how to add an action to our server.
The modification will be made under Netbeans:

In [2], we modify the file [config.json] to add the new action:
{
"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"
}
The modification consists of:
- line 67: add the action [get-admindata] and associate it with a controller;
- line 36: declare this controller in the list of classes to be loaded by the PHP application;
The next step is to implement the [AdminDataController] [3] controller:
<?php
namespace Application;
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// layer alias [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class AdminDataController implements InterfaceController {
// $config is the application configuration
// request processing
// session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// you must have a single parameter GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
// we note the error
$message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
$état = 1001;
// return result to main controller
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// we can work
// Redis
\Predis\Autoloader::register();
try {
// customer [predis]
$redis = new \Predis\Client();
// connect to the server to see if it's there
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// it didn't go well
// return result with error to main controller
$état = 1050;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// data recovery from tax authorities
// first search the cache [redis]
if (!$redis->get("taxAdminData")) {
try {
// retrieve tax data from the database
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// taxAdminData
$taxAdminData = $dao->getTaxAdminData();
// put the recovered data into redis
$redis->set("taxAdminData", $taxAdminData);
} catch (\RuntimeException $ex) {
// it didn't go well
// return result with error to main controller
$état = 1041;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// tax data are taken from the [redis] memory of the [application] scope
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
// we instantiate an object [TaxAdminData] from the previous attribute array
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
}
// return result to main controller
$état = 1000;
return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
}
}
Comments
- line 12: like the server’s other controllers, [AdminDataController] implements the [InterfaceController] interface consisting of the [execute] method on lines 19–79;
- line 78: as with the server’s other controllers, the [AdminDataController.execute] method returns an array [$status, $état, [‘réponse’=>$response]] containing:
- [$status]: the response status code HTTP;
- [$état]: an internal application code representing the server’s state after executing the client’s request;
- [$response]: an array encapsulating the response to be sent to the client. Here, this array will later be converted into the string jSON;
- lines 25–34: we verify that the client’s action [get-admindata] is syntactically correct;
- lines 37–74: an object named [TaxAdminData] is retrieved; if it is found:
- lines 56–59: from the database if it was not found in the cache [redis];
- lines 70–73: from the cache [redis];
This code is the same as that of the [CalculerImpotController] controller explained in the linked article. In fact, this controller also had to retrieve the [TaxAdminData] object encapsulating the tax administration data.
During testing of the Javascript client, the jSON form of [TaxAdminData] caused an issue when this object was found in the [redis] cache. To understand this, let’s examine the format in which this object is stored in [redis]:


- In [5-7], we see that numeric values were stored as character strings. PHP handled this because the + operator in calculations involving numbers and strings implicitly causes a type conversion from string to number. But Javascript does the opposite: the + operator in calculations involving numbers and strings implicitly causes a type cast from the number to a string. The calculations in the Javascript and [Métier] classes are therefore incorrect;
To fix this issue, we modify the [TaxAdminData.setFromArrayOfAttributes] method used on line 71 of the controller to instantiate a [TaxAdminData] object (see article) from the string jSON found in the cache [redis]:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// tax brackets
protected $limites;
protected $coeffR;
protected $coeffN;
// tax calculation constants
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// initialization
public function setFromJsonFile(string $taxAdminDataFilename) {
// parent
parent::setFromJsonFile($taxAdminDataFilename);
// check attribute values
$this->checkAttributes();
// we return the object
return $this;
}
protected function check($value): \stdClass {
// $value is an array of elements of type string or a single element
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// transform the array of strings into an array of reals
$newTableau = [];
$result = new \stdClass();
// table elements must be positive or zero decimal numbers
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// put the float in newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// we note the error
$result->erreur = TRUE;
// we leave
return $result;
}
}
// we return the result
$result->erreur = FALSE;
if (!\is_array($value)) {
// a single value
$result->value = $newTableau[0];
} else {
// a list of values
$result->value = $newTableau;
}
return $result;
}
// initialization by an array of attributes
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// parent
parent::setFromArrayOfAttributes($arrayOfAttributes);
// check attribute values
$this->checkAttributes();
// we return the object
return $this;
}
// checking attribute values
protected function checkAttributes() {
// check that attribute values are real >=0
foreach ($this as $key => $value) {
if (is_string($value)) {
// $value must be a real number >=0 or an array of reals >=0
$result = $this->check($value);
// mistake?
if ($result->erreur) {
// throw an exception
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// we note the value
$this->$key = $result->value;
}
}
}
// we return the object
return $this;
}
// getters and setters
...
}
Comments
- line 5: the [TaxAdminData] class extends the [BaseEntity] class, which already has the [setFromArrayOfAttributes] method. Since this method is not suitable, we redefine it on lines 67–75;
- line 70: the [setFromArrayOfAttributes] method of the parent class is first used to initialize the class attributes;
- line 72: the [checkAttributes] method checks that the associated values are indeed numbers. If they are strings, they are converted to numbers;
- line 74: the resulting [$this] object is then an object with attributes having numeric values;
- lines 78–93: the [checkAttributes] method verifies that the values associated with the object’s attributes are indeed numeric;
- line 80: the list of attributes is traversed;
- line 81: if the value of an attribute is of type [string];
- line 83: then we verify that this string represents a number;
- line 90: if so, the string is converted to a number and assigned to the tested attribute;
- lines 85–86: if not, an exception is thrown;
- Lines 32–65: The function [check] does a little more than necessary. It handles both arrays and single values. However, here it is called only to check a value of type [string]. It returns an object with the properties [erreur, value], where:
- [erreur] is a Boolean indicating whether an error occurred or not;
- [value] is the parameter [value] from line 32, converted to a number or an array of numbers as appropriate;
The class [BaseEntity], which could have had an attribute named [arrayOfAttributes], is modified to no longer have this attribute: it indeed pollutes the string jSON from [TaxAdminData]. The class is rewritten as follows:
<?php
namespace Application;
class BaseEntity {
// initialization from a JSON file
public function setFromJsonFile(string $jsonFilename) {
// retrieve the contents of the tax data file
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// mistake?
if (!$fileContents) {
// we note the error
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// retrieve the JSON code from the configuration file in an associative array
$arrayOfAttributes = \json_decode($fileContents, true);
// mistake?
if ($arrayOfAttributes === FALSE) {
// we note the error
$erreur = TRUE;
$message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
}
}
// mistake?
if ($erreur) {
// throw an exception
throw new ExceptionImpots($message);
}
// initialization of class attributes
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// we check the presence of all attributes
$this->checkForAllAttributes($arrayOfAttributes);
// we return the object
return $this;
}
public function checkForAllAttributes($arrayOfAttributes) {
// check that all keys have been initialized
foreach (\array_keys($arrayOfAttributes) as $key) {
if (!isset($this->$key)) {
throw new ExceptionImpots("L'attribut [$key] de la classe "
. get_class($this) . " n'a pas été initialisé");
}
}
}
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// on initialise certains attributs de la classe (pas forcément tous)
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// object is returned
return $this;
}
// toString
public function __toString() {
// object attributes
$arrayOfAttributes = \get_object_vars($this);
// string jSON of object
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
}
Comments
- line 20: the [$this→arrayOfAttributes] attribute has been converted into a variable that must now be passed to the [checkForAllAttributes] method on line 38, which previously operated on the [$this→arrayOfAttributes] attribute;
Due to this change to [BaseEntity], the [Database] class must also be slightly modified:
<?php
namespace Application;
class Database extends BaseEntity {
// attributes
protected $dsn;
protected $id;
protected $pwd;
protected $tableTranches;
protected $colLimites;
protected $colCoeffR;
protected $colCoeffN;
protected $tableConstantes;
protected $colPlafondQfDemiPart;
protected $colPlafondRevenusCelibatairePourReduction;
protected $colPlafondRevenusCouplePourReduction;
protected $colValeurReducDemiPart;
protected $colPlafondDecoteCelibataire;
protected $colPlafondDecoteCouple;
protected $colPlafondImpotCelibatairePourDecote;
protected $colPlafondImpotCouplePourDecote;
protected $colAbattementDixPourcentMax;
protected $colAbattementDixPourcentMin;
// setter
// initialization
public function setFromJsonFile(string $jsonFilename) {
// parent
parent::setFromJsonFile($jsonFilename);
// object is returned
return $this;
}
// getters and setters
...
}
Comments
- In the original code, after line 30, the method [parent::checkForAllAttributes] was called. This is no longer necessary since it is now automatically handled by the method [parent::setFromJsonFile($jsonFilename)];
14.3.4. [Postman] server tests
[Postman] was presented in the linked article.
We use the following Postman tests:



The result of this last request is as follows:

- In [5-8], we can see that the attributes of the string jSON do indeed have numeric values (and not character strings). This result will allow the Javascript and [Métier] classes to execute normally;
14.3.5. The main script [main]

The main script [main] for the client Javascript is as follows:
// imports
import axios from 'axios';
// imports
import Dao from './Dao2';
import Métier from 'trade';
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// instantiation layer [dao]
const dao = new Dao(axios);
// requests HTTP
let taxAdminData;
try {
// init session
log("-----------init-session");
let response = await dao.initSession();
log(response);
// authentication
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// tax information
log("-----------get-admindata");
response = await dao.getAdminData();
log(response);
taxAdminData = response.réponse;
} catch (error) {
// we log the error
console.log("erreur=", error.message);
// end
return;
}
// instantiation layer [business]
const métier = new Métier(taxAdminData);
// tax calculations
log("-----------calculer-impot x 3");
const simulations = [];
simulations.push(métier.calculerImpot("oui", 2, 45000));
simulations.push(métier.calculerImpot("non", 2, 45000));
simulations.push(métier.calculerImpot("non", 1, 30000));
// list of simulations
log("-----------liste-des-simulations");
log(simulations);
// deleting a simulation
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// log jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execution
main();
Comments
- lines 5-6: imports of the [Dao] and [Métier] classes;
- line 9: the asynchronous function [main], which will handle communication with the server using the [Dao] class and request the [Métier] class to perform the tax calculations;
- lines 10–36: the script calls the [initSession, authentifierUtilisateur, getAdminData] methods of the [dao] layer sequentially and in a blocking manner;
- line 38: the [dao] layer is no longer needed. We have all the elements to run the [métier] layer of the Javascript client;
- lines 41–46: we perform three tax calculations and aggregate the results in a table [simulations];
- line 49: we display the simulation table;
- line 52: one of them is deleted;
The results of running the main script are as follows:
[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. Client HTTP 3

In this section, we port the [Client HTTP 2] application to a browser using the following architecture:

Porting is not immediate. While [node.js] can execute Javascript and ES6, this is generally not the case for browsers. You must therefore use tools that translate the ES6 code into ES5 code understood by modern browsers. Fortunately, these tools are both powerful and fairly easy to use.
Here, we followed the article [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].
In the [client HTTP 3/src] folder, we placed the [main.js, Métier.js, Dao2.js] elements of the [Client Http 2] application that we just developed.
14.4.1. Initializing the project
We will be working in the [client http 3] folder. We open a terminal in [VSCode] and navigate to this folder:

We initialize this project with the command [npm init] and accept the default answers to the questions asked:

- In [4-5], the project configuration file [package.json] is generated based on the various answers provided;
14.4.2. Installing project dependencies
We will install the following dependencies:
- [@babel/core]: the core of the [Babel] [https://babeljs.io] tool that transforms 2015+ ES code into code executable on both modern and older browsers;
- [@babel/preset-env]: part of the Babel toolkit. Runs before the ES6 → ES5 transpilation;
- [babel-loader]: this dependency allows the [webpack] tool to call the [Babel] tool;
- [webpack]: orchestrator. It is [webpack] that calls upon Babel to transpilate the codes ES6 → ES5, and then it assembles all the resulting files into a single file;
- [webpack-cli]: required by [webpack];
- [@webpack-cli/init]: used to configure [webpack];
- [webpack-dev-server]: provides a development web server that runs by default on port 8080. When source files are modified, it automatically reloads the web application;
The project dependencies are installed as follows in a [VSCode] terminal:
npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

After installing the dependencies, the [package.json] file has changed as follows:
{
"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"
}
}
- lines 12–19: the project dependencies are [devDependencies]: these are needed during the development phase but not in the production phase. In fact, in production, the file [dist/main.js] is used. It is written in ES5 and no longer requires the tools for transpilating code from ES6 to ES5;
We need to add two dependencies to the project:
- [core-js]: contains "polyfills" for ECMAScript 2019. A polyfill allows recent code, such as ECMAScript 2019 (Sept. 2019), to run on older browsers;
- [regenerator-runtime]: according to the library’s website --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];
Starting with Babel 7, these two dependencies replace the [@babel/polyfill] dependency, which previously served this purpose and is now (Sept 2019) deprecated. They are installed as follows:

The [package.json] file then changes as follows:
{
"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"
}
}
Using the [core-js, regenerator-runtime] dependencies requires adding the following [imports] lines (lines 3-4) to the main [src/main.js] script:
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// imports
import Dao from './Dao2';
import Métier from 'trade';
14.4.3. Configuration of [webpack]
[webpack] is the tool that will control:
- the transpilation of all Javascript files in the project from ES6 to ES5;
- the merging of the generated files into a single file;
This tool is controlled by a configuration file named [webpack.config.js], which can be generated using a dependency named [@webpack-cli/init] (Sept. 2019). This dependency was installed along with the others in the "Link" section.
We run the command [npx webpack-cli init] in a terminal [VSCode]:

After answering the various questions (for which we can accept most of the default answers), a file named [webpack.config.js] is generated at the root of the [4] project:
The [webpack.config.js] file looks like this:
/* 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
}
};
I don't understand all the details of this file, but a few points stand out:
- line 1: the file does not contain code ES6. [Eslint] then reports errors that trace back to the root of the project [javascript]. This is annoying. To prevent ESLint from analyzing a file, simply comment out line 1;
- line 31: we are working in [développement] mode;
- line 32: the input script here is [src/index.js]. We will need to change this;
- line 36: the folder where the outputs of [webpack] will be stored is the [dist] folder;
- line 46: we see that [webpack] uses [babel-loader], one of the dependencies we installed;
- line 54: we see that [webpack] uses [@babel-preset/env], one of the dependencies we installed;
The initialization of [webpack] has modified the file [package.json] (it is requesting permission):
{
"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"
}
}
- line 4: it has been modified;
- lines 8–9, 18–19: these have been added;
- line 8: the [npm] task that compiles the project;
- line 9: the task [npm], which runs the project;
- line 18: ?
- line 19: generates a [dist/index.html] file that automatically embeds the [dist/main.js] script generated by [webpack], and it is this script that is used when the project is executed;
Finally, the configuration of [webpack] generated a file named [src/index.js]:

The contents of [index.js] are as follows (Sept. 2019):
console.log("Hello World from your main file!");
14.4.4. Compiling and running the project
The [package.json] file has three tasks named [npm]:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack-dev-server"
},
These tasks are included in [VSCode], which executes them:

- in [1-3], the project is compiled;
- in [4]: the project is compiled in [dist/main.hash.js] and a page [dist/index.html] is created;
The generated [index.html] page is as follows:
<!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>
This page simply encapsulates the [main.hash.js] file generated by [webpack].
The project is executed by the [start] task:

The [dist/index.html] page is then loaded onto a server, part of the [webpack] suite, running on port 8080 of the local machine and displayed by the machine’s default browser:

- in [2], the service port of the [webpack] web server;
- in [3], the body of the [dist/index.html] page is empty;
- in [4], the [console] tab of the browser’s developer tools, here Firefox (F12);
- in [5], the result of executing the file [src/index.js]. Recall that the content of this file was as follows:
Now, let’s change this content to the following line:
Automatically (without recompiling), new [main.js, index.html] files are generated and the new [index.html] file is loaded into the browser:

It is not necessary to run the [build] task before the [start] task: the latter first compiles the project. It does not store the output of this compilation in the [dist] folder. To verify this, simply delete that folder. You will then see that the [start] task compiles and runs the project without creating the [dist] folder. It appears to store its [index.html, main.hash.js] outputs in a folder specific to [webpackdev-server]. This behavior is sufficient for our tests.
When the development server is running, any saved changes to one of the project files trigger a recompilation. For this reason, we disable the [Auto Save] mode of [VSCode]. Indeed, we do not want a recompilation to occur every time characters are typed into one of the project files. We only want recompilation to occur when changes are saved:

- in [2], the option and [Auto Save] options must not be checked;
14.4.5. Testing the Javascript client for the tax calculation server
To test the Javascript client of the tax calculation server, you must designate [main.js] [1] as the project entry point in the file [webpack.config.js] [2-3]:

Let’s not forget that the [main.js] script must include two additional imports compared to its version counterpart in [Client http 2]:

In addition, we have slightly modified the code to handle errors that the server may return:
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// imports
import Dao from './Dao2';
import Métier from 'trade';
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// instantiation layer [dao]
const dao = new Dao(axios);
// requests HTTP
let taxAdminData;
try {
// init session
log("-----------init-session");
let response = await dao.initSession();
log(response);
if (response.état != 700) {
throw new Error(JSON.stringify(response.réponse));
}
// authentication
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
if (response.état != 200) {
throw new Error(JSON.stringify(response.réponse));
}
// tax information
log("-----------get-admindata");
response = await dao.getAdminData();
log(response);
if (response.état != 1000) {
throw new Error(JSON.stringify(response.réponse));
}
taxAdminData = response.réponse;
} catch (error) {
// we log the error
console.log("erreur=", error.message);
// end
return;
}
// instantiation layer [business]
const métier = new Métier(taxAdminData);
// tax calculations
log("-----------calculer-impot x 3");
const simulations = [];
simulations.push(métier.calculerImpot("oui", 2, 45000));
simulations.push(métier.calculerImpot("non", 2, 45000));
simulations.push(métier.calculerImpot("non", 1, 30000));
// list of simulations
log("-----------liste-des-simulations");
log(simulations);
// deleting a simulation
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// log jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execution
main();
Comments
- On lines [24-26], [31-33], and [38-40], we validate the code [response.état] sent in the server’s response jSON. If this code indicates an error, an exception is thrown with the error message string jSON from the server response [response.réponse];
Once this is done, we execute the project [5-6].
The page [index.html] is then generated and loaded into the browser:

- In [7], we see that the [init-session] action could not be completed due to a [CORS] issue (Cross-Origin Resource Sharing);
The CORS issue stems from the client/server relationship:
- our client Javascript was downloaded to the machine [http://localhost:8080];
- the tax calculation server is running on the [http://localhost:80] machine;
- the client and server are therefore not in the same domain (same machine but different ports);
- The browser running the Javascript client, which was loaded from the [http://localhost:8080] machine, blocks any request that is not directed to [http://localhost:80]. This is a security measure. It therefore blocks the client’s request to the server running on the [http://localhost:80] machine;
In fact, the browser does not completely block the request. It actually waits for the server to “tell” it that it accepts cross-domain requests. If it receives this authorization, the browser will then send the cross-domain request.
The server grants its authorization by sending specific headers:
- Line 1: The client Javascript operates on the domain [http://localhost:8080]. The server must explicitly respond that it accepts this domain;
- Line 2: The client Javascript will use the headers HTTP and [Accept, Content-Type] in its requests:
- [Accept]: this header is sent in every request;
- [Content-Type]: this header is used in POST operations to indicate the type of parameters in POST;
The server must explicitly accept these two headers HTTP;
- Line 3: The client Javascript will use requests GET and POST. The server must explicitly accept these two types of requests;
- line 4: the client Javascript will send session cookies. The server accepts them with the header from line 4;
We therefore need to modify the server. We do this in [Netbeans]. The issue with CORS is a problem encountered only in development mode. In production, the client and server will operate within the same domain [http://localhost:80], and there will be no issue with CORS. We therefore need a way to enable or disable CORS requests via server configuration.

Server modifications are made in three places:
- [1, 4]: in the configuration file [config.json] to set a boolean that controls whether or not cross-domain requests are accepted;
- [2]: in the [ParentResponse] class that sends the response to the Javascript client. This class will send the CORS headers expected by the client browser;
- [3]: within the [HtmlResponse, JsonResponse, XmlResponse] classes that generate responses for the [html, json, xml] sessions, respectively. These classes must pass the boolean [corsAllowed] found in [4] to their parent class [2]. This is done in [5] by passing the image array from the file jSON to [2];
The class [ParentResponse] [2] evolves as follows:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class ParentResponse {
// int $statusCode: HTTP response status code
// string $content: the body of the reply to be sent
// depending on the case, this is a JSON, XML, HTML string
// array $headers: HTTP headers to be added to the response
public function sendResponse(
Request $request,
int $statusCode,
string $content,
array $headers,
array $config): void {
// preparing the server's text response
$response = new Response();
$response->setCharset("utf-8");
// status code
$response->setStatusCode($statusCode);
// headers for cross-domain requests
if ($config['corsAllowed']) {
$origin = $request->headers->get("origin");
if (strpos($origin, "http://localhost") === 0) {
$headers = array_merge($headers,
["Access-Control-Allow-Origin" => $origin,
"Access-Control-Allow-Headers" => "Accept, Content-Type",
"Access-Control-Allow-Methods" => "GET, POST",
"Access-Control-Allow-Credentials" => "true"
]);
}
}
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// special case of the [OPTIONS] method
// only the headers are important in this case
$method = strtolower($request->getMethod());
if ($method === "options") {
$content = "";
$response->setStatusCode(Response::HTTP_OK);
}
// we send the answer
$response->setContent($content);
$response->send();
}
}
- line 29: we check if we need to handle cross-domain requests. If so, we generate the headers HTTP and CORS (lines 33–37) even if the current request is not a cross-domain request. In the latter case, the CORS headers will be unnecessary and will remain unused by the client;
- Line 30: In a cross-domain request, the client browser querying the server sends a HTTP [Origin: http://localhost:8080] header (in the specific case of our client, Javascript). Line 30: We retrieve this HTTP header from the [$request] request;
- Line 31: We will only accept cross-domain requests originating from the machine [http://localhost]. Note that these requests only occur in the project’s development mode;
- lines 32–36: the headers CORS are added to the headers already present in the table [$headers];
- Lines 45–49: The way the client browser requests the CORS permissions may vary depending on the client being used. In some cases, the client browser may request these permissions using the HTTP or [OPTIONS] commands. This is a new feature for our server, which was built to handle only [GET, POST] commands. In the case of a [OPTIONS] command, the server currently generates an error response. Lines 46–49: we correct this at the last moment. If, on line 46, we determine that the current command is a [OPTIONS] command, then we generate the following for the client:
- lines 47, 51: an empty [$content] response;
- line 48: a status code of 200 indicating that the request was successful. The only important thing for this request is sending the CORS headers from lines 33–36. This is what the client browser expects;
Once the server has been corrected in this way, the client Javascript runs better but displays a new error:

- In [1], the jSON session is initialized correctly;
- In [2], the [authentifier-utilisateur] action fails: the server indicates that there is no active session. This means that the client Javascript did not correctly return the session cookie it sent during the action [init-session];
Let’s examine the network traffic that took place:

- In [4], the request [init-session]. It completed successfully with a 200 status code for the response;
- In [5], the request [authentifier-utilisateur]. This one fails with a 400 (Bad Request) status code [6] for the response;
If we examine the headers HTTP and [7] of the request [5], we can see that the client Javascript did not send the headers HTTP and [Cookie], which would have allowed it to return the session cookie initially sent by the server. This is why the server reports that there is no session.
To have the client send the session cookie, you must add a configuration to the [axios] object:
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// imports
import Dao from './Dao2';
import Métier from 'trade';
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
axios.defaults.withCredentials = true;
// instantiation layer [dao]
const dao = new Dao(axios);
// requests HTTP
let taxAdminData;
...
Line 15 specifies that cookies should be included in the HTTP headers of the [axios] request. Note that this was not necessary in the [node.js] environment. There are therefore code differences between the two environments.
Once this error is corrected, the Javascript client runs normally:


14.5. Improvement to client HTTP 3
When the previous [Dao2] class runs within a browser, session cookie management is unnecessary. This is because the browser hosting the [dao] layer manages the session cookie: it automatically returns any cookie the server sends to it. Consequently, the [Dao2] class can be rewritten as the following [Dao3] class:
"use strict";
// imports
import qs from "qs";
class Dao3 {
// manufacturer
constructor(axios) {
this.axios = axios;
}
// init session
async initSession() {
// query options HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// URL parameters
params: {
action: "init-session",
type: "json"
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// query options HHTP [post /main.php?action=authenticate-user]
const options = {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded"
},
// body of POST
data: qs.stringify({
user: user,
password: password
}),
// URL parameters
params: {
action: "authentifier-utilisateur"
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// query options HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// URL parameters
params: {
action: "get-admindata"
}
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
async getRemoteData(options) {
// execute query HTTP
let response;
try {
// asynchronous request
response = await this.axios.request("main.php", options);
} catch (error) {
// parameter [error] is an exception instance - it can take various forms
if (error.response) {
// the server response is in [error.response]
response = error.response;
} else {
// error restart
throw error;
}
}
// response is the entire HTTP response from the server (HTTP headers + response itself)
// the server response is in [response.data]
return response.data;
}
}
// class export
export default Dao3;
Everything related to managing the management cookie has been removed.
We modify the previous project as follows:

In the [src] folder, we have added two files:
- the [Dao3] class we just introduced;
- the file [main3], which is responsible for launching the new version;
The [main3] file remains identical to the [main] file from the previous version, but it now uses the [Dao3] class:
// imports
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";
// imports
import Dao from "./Dao3";
import Métier from "./Métier";
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL =
"http://localhost/php7/scripts-web/impots/version-14";
axios.defaults.withCredentials = true;
// instantiation layer [dao]
const dao = new Dao(axios);
// requests HTTP
...
}
// log jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execution
main();
The file [webpack.config] has been modified to now execute the 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()],
...
};
Once this is done, we run the project after starting the tax calculation server:

The results displayed in the browser console are identical to those of the previous version.
14.6. Conclusion
We now have all the tools to develop the Javascript code for a web application. We can:
- use the latest ECMAScript code;
- test isolated elements of this code in a simpler [node.js] environment for debugging and testing;
- then port this code to a browser using the [babel] and [webpack] tools;