Skip to content

14. Klienci HTTP JavaScript usługi obliczania podatku

14.1. Introduction

W tym miejscu zamierzamy napisać klienta [node.js] dla wersji 14 serwisu obliczającego podatek. Architektura klient/serwer będzie wyglądać następująco:

Image

Przeanalizujemy dwie wersje klienta:

  • wersja 1 klienta będzie miała następującą strukturę warstwową [main, dao]:

Image

  • wersja 2 klienta będzie miała strukturę [main, métier, dao]. Warstwa serwera o nazwie [métier] zostanie przeniesiona na klienta:

Image

14.2. Klient HTTP 1

Image

Jak już wspomniano, klient HTTP 1 realizuje następującą architekturę klient-serwer:

Image

Zaimplementujemy:

  • warstwę [dao] w postaci klasy;
  • warstwę [main] w postaci skryptu wykorzystującego tę klasę;

14.2.1. Warstwa [dao]

Warstwa [dao] zostanie zaimplementowana przez następującą klasę [Dao1.js]:


'use strict';

// importy
import qs from 'qs'

class Dao1 {

  // konstruktor
  constructor(axios) {
    // biblioteka axios do wysyłania żądań HTTP
    this.axios = axios;
    // plik cookie sesji
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // rozpoczęcie sesji
  async  initSession() {
    // opcje zapytania HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parametry URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // wykonanie zapytania HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // opcje zapytania HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // treść POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parametry URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // wykonanie zapytania HTTP
    return await this.getRemoteData(options);
  }

  // obliczenie podatku
  async  calculerImpot(marié, enfants, salaire) {
    // opcje zapytania HHTP [post /main.php?action=calculer-impot]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // treść POST [marié, enfants, salaire]
      data: qs.stringify({
        marié: marié,
        enfants: enfants,
        salaire: salaire
      }),
      // parametry URL
      params: {
        action: 'calculer-impot'
      }
    };
    // wykonanie zapytania HTTP
    const data = await this.getRemoteData(options);
    // wynik
    return data;
  }

  // lista symulacji
  async  listeSimulations() {
    // opcje zapytania HHTP [get /main.php?action=lister-simulations]
    const options = {
      method: "GET",
      // parametry zapytania URL
      params: {
        action: 'lister-simulations'
      },
    };
    // wykonanie zapytania HTTP
    const data = await this.getRemoteData(options);
    // wynik
    return data;
  }

  // lista symulacji
  async  supprimerSimulation(index) {
    // opcje zapytania HHTP  [get /main.php?action=supprimer-simulation&numéro=index]
    const options = {
      method: "GET",
      // parametry zapytania URL
      params: {
        action: 'supprimer-simulation',
        numéro: index
      },
    };
    // wykonanie zapytania HTTP
    const data = await this.getRemoteData(options);
    // wynik
    return data;
  }

  async  getRemoteData(options) {
    // dla pliku cookie sesji
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // wykonanie zapytania HTTP
    let response;
    try {
      // żądanie asynchroniczne
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // parametr [error] jest instancją wyjątku – może przybierać różne formy
      if (error.response) {
        // odpowiedź serwera znajduje się w [error.response]
        response = error.response;
      } else {
        // błąd jest ponownie generowany
        throw error;
      }
    }
    // odpowiedź stanowi całość odpowiedzi serwera w formacie HTTP (nagłówki HTTP + sama odpowiedź)
    // pobieramy plik cookie sesji, jeśli istnieje
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie to tablica
      // w tej tablicy szuka się pliku cookie sesji
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // szukamy pliku cookie sesji
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // zapisujemy plik cookie sesji
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // znaleziono
          trouvé = true;
        } else {
          // kolejny element
          i++;
        }
      }
    }
    // odpowiedź serwera znajduje się w [response.data]
    return response.data;
  }
}

// eksport klasy
export default Dao1;
  • wykorzystujemy tutaj wiedzę zdobytą w akapicie „link”, gdzie przedstawiliśmy bibliotekę [axios] umożliwiającą wysyłanie zapytań HTTP zarówno w środowisku [node.js], jak i w przeglądarce. Zwrócimy szczególną uwagę na skrypt z akapitu „link”;
  • wiersze 9–15: konstruktor klasy. Będzie ona miała trzy właściwości:
    • [axios]: obiekt [axios] umożliwiający wysyłanie zapytań HTTP. Jest on przekazywany przez kod wywołujący;
    • [sessionCookieName]: w zależności od serwera plik cookie sesji może nosić różne nazwy. W tym przypadku jest to [PHPSESSID];
    • [sessionCookie]: plik cookie sesji wysłany przez serwer i zapisany przez klienta;
  • wiersze 53–76: funkcja asynchroniczna [calculerImpot] wysyła żądanie [post /main.php?action=calculer-impot], przekazując parametry [marié, enfants, salaire]. Zwraca ona ciąg znaków jSON przesłany przez serwer w postaci obiektu JavaScript;
  • wiersze 79–92: funkcja asynchroniczna [listeSimulations] wysyła żądanie [get /main.php?action=lister-simulations. Zwraca ciąg znaków jSON przesłany przez serwer w postaci obiektu JavaScript;
  • wiersze 95–109: funkcja asynchroniczna [supprimerSimulation] wysyła żądanie [get /main.php?action=supprimer-simulation&numéro=index]. Zwraca ona ciąg znaków jSON przesłany przez serwer w postaci obiektu JavaScript;
  • wiersz 121: stosuje się notację [this.axios], ponieważ w tym przypadku obiekt [axios] przekazany do konstruktora został zapisany we właściwości [this.axios];
  • wiersz 161: klasa [Dao1] jest eksportowana, aby można było z niej korzystać;

14.2.2. Skrypt [main1.js]

Skrypt [main1.js] wykonuje serię wywołań do serwera przy użyciu klasy [Dao1]:

  • inicjalizacja sesji jSON;
  • uwierzytelnianie za pomocą [admin, admin];
  • wniosek o trzy obliczenia podatków;
  • żąda listy symulacji;
  • usuwa jedną z nich;

Kod wygląda następująco:


// import axios
import axios from 'axios';
// import klasy Dao1
import Dao from './Dao1';

// funkcja asynchroniczna [main]
async function main() {
  // konfiguracja axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instancjonowanie warstwy [dao]
  const dao = new Dao(axios);
  // korzystanie z warstwy [dao]
  try {
    // inicjowanie sesji
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // uwierzytelnianie
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // obliczenia podatkowe
    log("-----------calculer-impot x 3");
    response = await Promise.all([
      dao.calculerImpot("oui", 2, 45000),
      dao.calculerImpot("non", 2, 45000),
      dao.calculerImpot("non", 1, 30000)
    ]);
    log(response);
    // lista symulacji
    log("-----------liste-des-simulations");
    response = await dao.listeSimulations();
    log(response);
    // usunięcie symulacji
    log("-----------suppression simulation n° 1");
    response = await dao.supprimerSimulation(1);
    log(response);
  } catch (error) {
    // rejestruje się błąd
    console.log("erreur=", error.message);
  }
}

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

// wykonanie
main();

Komentarze

  • wiersz 2: importujemy bibliotekę [axios];
  • wiersz 4: importujemy klasę [Dao];
  • wiersz 7: funkcja [main], która komunikuje się z serwerem, jest asynchroniczna;
  • wiersze 9–10: domyślna konfiguracja zapytań HTTP, które będą kierowane do serwera:
    • wiersz 9: [timeout] z czasem oczekiwania 2 sekundy;
    • wiersz 10: wszystkie żądania URL mają jako prefiks URL, który jest podstawą wersji 14 serwera obliczającego podatek;
  • wiersz 12: warstwa [Dao] została utworzona. Można z niej teraz korzystać;
  • wiersze 46–48: funkcja [log] ma za zadanie wyświetlić ciąg jSON z obiektu JavaScript w upiększonej formie: w układzie pionowym z wcięciem wynoszącym dwie spacje (trzeci parametr);
  • wiersze 15–18: inicjalizacja sesji jSON;
  • wiersze 19–22: uwierzytelnianie;
  • wiersze 23–30: równoległe zlecenie trzech obliczeń podatku. Dzięki [await Promise.all] wykonanie jest zablokowane do momentu uzyskania wszystkich trzech wyników;
  • wiersze 31–34: lista symulacji;
  • wiersze 35–38: usunięcie symulacji;
  • wiersze 39–42: obsługa ewentualnego wyjątku;

Wyniki wykonania są następujące:


[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. Klient HTTP 2

Image

Architektura klienta HTTP2 przedstawia się następująco:

Image

Warstwa [métier] została przeniesiona z serwera do klienta JavaScript. W przeciwieństwie do tego, co zrobiliśmy w kursie PHP7, warstwa [main] nie będzie tutaj musiała przechodzić przez warstwę [métier], aby dotrzeć do warstwy [dao]. Wykorzystamy te dwie warstwy jako centra kompetencji:

  • warstwa [main] przechodzi przez warstwę [dao], gdy tylko potrzebuje danych znajdujących się na serwerze;
  • warstwa [main] zwraca się do warstwy [métier] z prośbą o wykonanie obliczeń podatkowych;
  • warstwa [métier] jest niezależna od warstwy [dao] i nigdy do niej nie odwołuje się;

14.3.1. Klasa JavaScript [Métier]

Istota klasy [Métier] w PHP została opisana w artykule, do którego link znajduje się poniżej. Jest to dość złożony kod, który przytaczamy tutaj nie po to, aby go wyjaśnić, ale aby móc przetłumaczyć go na JavaScript:


<?php

// przestrzeń nazw
namespace Application;

class Metier implements InterfaceMetier {
  // warstwa DAO
  private $dao;
  // dane administracji podatkowej
  private $taxAdminData;

  //---------------------------------------------
  // ustawianie warstwy [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceDao $dao) {
    // zapisuje się odniesienie do warstwy [dao]
    $this->dao = $dao;
    // pobierane są dane umożliwiające obliczenie podatku
    // metoda [getTaxAdminData] może wygenerować wyjątek ExceptionImpots
    // następnie pozwala się na przekazanie wyjątku do kodu wywołującego
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// obliczenie podatku
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié: tak, nie
    // $enfants: liczba dzieci
    // $salaire: roczne wynagrodzenie
    // $this->taxAdminData: dane z urzędu skarbowego
    //
    // sprawdzamy, czy posiadamy dane z urzędu skarbowego
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // obliczenie podatku z uwzględnieniem dzieci
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // obliczenie podatku bez uwzględnienia dzieci
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // zastosowanie limitu ilorazu rodzinnego
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci, dwukrotność tej kwoty na kolejne
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // bierzemy najwyższą stawkę podatkową
    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"];
    }
    // obliczenie ewentualnej ulgi podatkowej
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // obliczenie ewentualnej ulgi podatkowej
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // wynik
    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é: tak, nie
    // $enfants: liczba dzieci
    // $salaire: roczne wynagrodzenie
    // $this->taxAdminData: dane z urzędu skarbowego
    //
    // liczba udziałów
    $marié = strtolower($marié);
    if ($marié === "oui") {
      $nbParts = $enfants / 2 + 2;
    } else {
      $nbParts = $enfants / 2 + 1;
    }
    // 1 część na dziecko, począwszy od trzeciego
    if ($enfants >= 3) {
      // pół udziału dodatkowo na każde dziecko, począwszy od trzeciego
      $nbParts += 0.5 * ($enfants - 2);
    }
    // dochód podlegający opodatkowaniu
    $revenuImposable = $this->getRevenuImposable($salaire);
    // dopłata
    $surcôte = floor($revenuImposable - 0.9 * $salaire);
    // z powodu zaokrągleń
    if ($surcôte < 0) {
      $surcôte = 0;
    }
    // współczynnik rodzinny
    $quotient = $revenuImposable / $nbParts;
    // obliczenie podatku
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // umieszczony na końcu tabeli limitów w celu zatrzymania następującej po niej pętli
    $limites[count($limites) - 1] = $quotient;
    // wyszukiwanie stawki podatkowej
    $i = 0;
    while ($quotient > $limites[$i]) {
      $i++;
    }
    // ponieważ umieszczono $quotient na końcu tablicy $limites, poprzednia pętla
    // nie może wykraczać poza tablicę $limites
    // teraz można obliczyć podatek
    $impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
    // wynik
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable = roczne wynagrodzenie – ulga
  // ulga ma wartość minimalną i maksymalną
  private function getRevenuImposable(float $salaire): float {
    // odliczenie w wysokości 10% wynagrodzenia
    $abattement = 0.1 * $salaire;
    // odliczenie to nie może przekroczyć wartości $this->taxAdminData->getAbattementDixPourCentMax()
    if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMax();
    }
    // ulga nie może być niższa niż $this->taxAdminData->getAbattementDixPourcentMin()
    if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMin();
    }
    // dochód podlegający opodatkowaniu
    $revenuImposable = $salaire - $abattement;
    // wynik
    return floor($revenuImposable);
  }

// oblicza ewentualną obniżkę
  private function getDecôte(string $marié, float $salaire, float $impots): float {
    // początkowo odliczenie wynosi zero
    $décôte = 0;
    // maksymalna kwota podatku uprawniająca do ulgi
    $plafondImpôtPourDécôte = $marié === "oui" ?
      $this->taxAdminData->getPlafondImpotCouplePourDecote() :
      $this->taxAdminData->getPlafondImpotCelibatairePourDecote();
    if ($impots < $plafondImpôtPourDécôte) {
      // maksymalna kwota ulgi
      $plafondDécôte = $marié === "oui" ?
        $this->taxAdminData->getPlafondDecoteCouple() :
        $this->taxAdminData->getPlafondDecoteCelibataire();
      // obniżka teoretyczna
      $décôte = $plafondDécôte - 0.75 * $impots;
      // ulga nie może przekroczyć kwoty podatku
      if ($décôte > $impots) {
        $décôte = $impots;
      }
      // brak ulgi <0
      if ($décôte < 0) {
        $décôte = 0;
      }
    }
    // wynik
    return ceil($décôte);
  }

// oblicza ewentualną ulgę
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    // limit dochodów uprawniający do ulgi w wysokości 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();
    }
    // dochód podlegający opodatkowaniu
    $revenuImposable = $this->getRevenuImposable($salaire);
    // ulga
    $réduction = 0;
    if ($revenuImposable < $plafondRevenuPourRéduction) {
      // ulga w wysokości 20%
      $réduction = 0.2 * $impots;
    }
    // wynik
    return ceil($réduction);
  }

  // obliczanie podatków w trybie wsadowym
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // pozwala na zgłaszanie wyjątków pochodzących z warstwy [dao]
    // pobieranie danych podatników
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabela wyników
    $results = [];
    // przetwarzamy je
    foreach ($taxPayersData as $taxPayerData) {
      // obliczamy podatek
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // uzupełnianie [$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"]);
      // wpisuje się wynik do tabeli wyników
      $results [] = $taxPayerData;
    }
    // zapisywanie wyników
    $this->dao->saveResults($resultsFileName, $results);
  }

}
  • wiersze 19–26: konstruktor klasy PHP. Ponieważ stwierdziliśmy, że tworzymy warstwę [métier] niezależną od warstwy [dao], wprowadzimy w JavaScript dwie modyfikacje do tego konstruktora:
    • nie będzie on odbierał instancji warstwy [dao] (nie jest już potrzebna);
    • nie będzie żądał danych podatkowych od administracji [taxAdminData] od warstwy [dao]: to kod wywołujący przekaże te dane do konstruktora;
  • wiersze 197–122: nie będziemy implementować metody [executeBatchImpots], której ostatecznym celem było zapisywanie wyników symulacji w pliku tekstowym. Chcemy uzyskać kod, który będzie działał zarówno w ramach [node.js], jak i w przeglądarce. Jednak zapisywanie danych w systemie plików komputera, na którym uruchomiona jest przeglądarka kliencka, nie jest możliwe;

Biorąc pod uwagę te ograniczenia, kod klasy JavaScript [Métier] wygląda następująco:


'use strict';

// klasa Métier
class Métier {

  // konstruktor
  constructor(taxAdmindata) {
    // this.taxAdminData: dane z urzędu skarbowego
    this.taxAdminData = taxAdmindata;
  }

  // obliczenie podatku
  // --------------------------------------------------------------------------
  calculerImpot(marié, enfants, salaire) {
    // stan cywilny: tak, nie
    // dzieci: liczba dzieci
    // wynagrodzenie: roczne wynagrodzenie
    // this.taxAdminData: dane z urzędu skarbowego
    //
    // obliczenie podatku z uwzględnieniem dzieci
    const result1 = this.calculerImpot2(marié, enfants, salaire);
    const impot1 = result1["impôt"];
    // obliczenie podatku bez uwzględnienia dzieci
    let result2, impot2, plafondDemiPart;
    if (enfants !== 0) {
      result2 = this.calculerImpot2(marié, 0, salaire);
      impot2 = result2["impôt"];
      // zastosowanie limitu ilorazu rodzinnego
      plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
      if (enfants < 3) {
        // PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci
        impot2 = impot2 - enfants * plafondDemiPart;
      } else {
        // PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci, dwukrotność tej kwoty na kolejne
        impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
      }
    } else {
      // brak ponownego obliczenia podatku
      impot2 = impot1;
      result2 = result1;
    }
    // bierzemy najwyższy podatek z [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"];
    }
    // obliczenie ewentualnej ulgi
    const décôte = this.getDecôte(marié, impot);
    impot -= décôte;
    // obliczenie ewentualnej ulgi podatkowej
    const réduction = this.getRéduction(marié, salaire, enfants, impot);
    impot -= réduction;
    // wynik
    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) {
    // stan cywilny: tak, nie
    // dzieci: liczba dzieci
    // wynagrodzenie: roczne wynagrodzenie
    // this->taxAdminData: dane z urzędu skarbowego
    //
    // liczba udziałów
    marié = marié.toLowerCase();
    let nbParts;
    if (marié === "oui") {
      nbParts = enfants / 2 + 2;
    } else {
      nbParts = enfants / 2 + 1;
    }
    // 1 część na dziecko, począwszy od trzeciego
    if (enfants >= 3) {
      // pół udziału dodatkowo na każde dziecko od trzeciego
      nbParts += 0.5 * (enfants - 2);
    }
    // dochód podlegający opodatkowaniu
    const revenuImposable = this.getRevenuImposable(salaire);
    // dopłata
    let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
    // w przypadku problemów z zaokrągleniem
    if (surcôte < 0) {
      surcôte = 0;
    }
    // współczynnik rodzinny
    const quotient = revenuImposable / nbParts;
    // obliczenie podatku
    const limites = this.taxAdminData.limites;
    const coeffR = this.taxAdminData.coeffR;
    const coeffN = this.taxAdminData.coeffN;
    // umieszczony na końcu tabeli limitów w celu zatrzymania następującej po niej pętli
    limites[limites.length - 1] = quotient;
    // wyszukiwanie stawki podatkowej
    let i = 0;
    while (quotient > limites[i]) {
      i++;
    }
    // ponieważ iloraz rodzinny umieszczono na końcu tablicy limitów, poprzednia pętla
    // nie może wykraczać poza tablicę limitów
    // teraz można obliczyć podatek
    const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
    // wynik
    return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
  }

  // revenuImposable = roczne wynagrodzenie – ulga
  // ulga ma wartość minimalną i maksymalną
  getRevenuImposable(salaire) {
    // odliczenie w wysokości 10% wynagrodzenia
    let abattement = 0.1 * salaire;
    // odliczenie to nie może przekroczyć wartości taxAdminData.getAbattementDixPourCentMax()
    if (abattement > this.taxAdminData.abattementDixPourCentMax) {
      abattement = this.taxAdminData.abattementDixPourcentMax;
    }
    // odliczenie nie może być niższe niż taxAdminData.getAbattementDixPourcentMin()
    if (abattement < this.taxAdminData.abattementDixPourcentMin) {
      abattement = this.taxAdminData.abattementDixPourcentMin;
    }
    // dochód podlegający opodatkowaniu
    const revenuImposable = salaire - abattement;
    // wynik
    return Math.floor(revenuImposable);
  }

  // oblicza ewentualną obniżkę
  getDecôte(marié, impots) {
    // początkowo odliczenie wynosi zero
    let décôte = 0;
    // maksymalna kwota podatku uprawniająca do ulgi
    let plafondImpôtPourDécôte = marié === "oui" ?
      this.taxAdminData.plafondImpotCouplePourDecote :
      this.taxAdminData.plafondImpotCelibatairePourDecote;
    let plafondDécôte;
    if (impots < plafondImpôtPourDécôte) {
      // maksymalna kwota obniżki
      plafondDécôte = marié === "oui" ?
        this.taxAdminData.plafondDecoteCouple :
        this.taxAdminData.plafondDecoteCelibataire;
      // obniżka teoretyczna
      décôte = plafondDécôte - 0.75 * impots;
      // ulga nie może przekroczyć kwoty podatku
      if (décôte > impots) {
        décôte = impots;
      }
      // brak obniżki <0
      if (décôte < 0) {
        décôte = 0;
      }
    }
    // wynik
    return Math.ceil(décôte);
  }

  // oblicza ewentualną ulgę
  getRéduction(marié, salaire, enfants, impots) {
    // limit dochodów uprawniający do ulgi w wysokości 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;
    }
    // dochód podlegający opodatkowaniu
    const revenuImposable = this.getRevenuImposable(salaire);
    // ulga
    let réduction = 0;
    if (revenuImposable < plafondRevenuPourRéduction) {
      // ulga w wysokości 20%
      réduction = 0.2 * impots;
    }
    // wynik
    return Math.ceil(réduction);
  }
}

// eksport klasy
export default Métier;
  • kod JavaScript ściśle odpowiada kodowi PHP;
  • klasa [Métier] została wyeksportowana, wiersz 187;

14.3.2. Klasa JavaScript [Dao2]

Image

Klasa [Dao2] implementuje warstwę [dao] powyższego klienta JavaScript w następujący sposób:


'use strict';

// importy
import qs from 'qs'

class Dao2 {

  // konstruktor
  constructor(axios) {
    this.axios = axios;
    // plik cookie sesji
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // inicjowanie sesji
  async  initSession() {
    // opcje żądania HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parametry URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // wykonanie zapytania HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // opcje zapytania HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // treść POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parametry URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // wykonanie zapytania HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // opcje zapytania HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // parametry zapytania URL
      params: {
        action: 'get-admindata'
      }
    };
    // wykonanie zapytania HTTP
    const data = await this.getRemoteData(options);
    // wynik
    return data;
  }

  async  getRemoteData(options) {
    // dla pliku cookie sesji
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // wykonanie zapytania HTTP
    let response;
    try {
      // żądanie asynchroniczne
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // parametr [error] jest instancją wyjątku – może przybierać różne formy
      if (error.response) {
        // odpowiedź serwera znajduje się w [error.response]
        response = error.response;
      } else {
        // błąd jest ponownie generowany
        throw error;
      }
    }
    // odpowiedź stanowi całość odpowiedzi serwera HTTP (nagłówki HTTP + sama odpowiedź)
    // pobieramy plik cookie sesji, jeśli istnieje
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie to tablica
      // w tej tablicy szuka się pliku cookie sesji
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // szukamy pliku cookie sesji
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // zapisujemy plik cookie sesji
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // znaleziono
          trouvé = true;
        } else {
          // kolejny element
          i++;
        }
      }
    }
    // odpowiedź serwera znajduje się w [response.data]
    return response.data;
  }
}

// eksport klasy
export default Dao2;

Uwagi

  • klasa [Dao2] implementuje tylko trzy z możliwych żądań kierowanych do serwera obliczającego podatek:
    • [init-session] (wiersze 17–29): w celu zainicjowania sesji jSON;
    • [authentifier-utilisateur] (wiersze 31–50): w celu uwierzytelnienia;
    • [get-admindata] (wiersze 52–65): w celu pobrania danych z urzędu skarbowego, które umożliwią obliczenie podatku po stronie klienta;
  • wiersze 52–65: wprowadzamy nową akcję [get-admindata] skierowaną do serwera. Akcja ta nie była dotychczas zaimplementowana. Robimy to teraz.

14.3.3. Modyfikacja serwera obliczającego podatek

Serwer obliczający podatek musi zaimplementować nową akcję. Zrobimy to w wersji 14 serwera. Akcja, którą należy zaimplementować, ma następujące cechy:

  • jest wywoływana przez operację [get /main.php?action=get-admindata];
  • zwraca ciąg jSON z obiektu zawierającego dane administracji podatkowej;

Przyjrzymy się teraz, jak dodać akcję do naszego serwera.

Modyfikację przeprowadzimy w środowisku NetBeans:

Image

W [2] modyfikujemy plik [config.json], aby dodać nową akcję:


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

Modyfikacja polega na:

  • wiersz 67: dodanie akcji [get-admindata] i powiązanie jej z kontrolerem;
  • wiersz 36: zadeklarowanie tego kontrolera na liście klas, które mają być ładowane przez aplikację PHP;

Kolejnym etapem jest zaimplementowanie kontrolera [AdminDataController] [3]:


<?php

namespace Application;

// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias warstwy [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

class AdminDataController implements InterfaceController {

  // $config to konfiguracja aplikacji
  // przetwarzanie żądania Request
  // korzysta z sesji Session i może ją modyfikować
  // $infos to dodatkowe informacje specyficzne dla każdego kontrolera
  // zwraca tablicę [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // musi występować tylko jeden parametr GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      // odnotowano błąd
      $message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
      $état = 1001;
      // przekazanie wyniku do głównego kontrolera
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }

    // można kontynuować pracę
    // Redis
    \Predis\Autoloader::register();
    try {
      // klient [predis]
      $redis = new \Predis\Client();
      // nawiązujemy połączenie z serwerem, aby sprawdzić, czy jest dostępny
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // nie poszło dobrze
      // zwrot wyniku z błędem do głównego kontrolera
      $état = 1050;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }

    // pobieranie danych z urzędu skarbowego
    // najpierw przeszukujemy pamięć podręczną [redis]
    if (!$redis->get("taxAdminData")) {
      try {
        // pobieranie danych podatkowych z bazy danych
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // taxAdminData
        $taxAdminData = $dao->getTaxAdminData();
        // pobrane dane zapisujemy w Redis
        $redis->set("taxAdminData", $taxAdminData);
      } catch (\RuntimeException $ex) {
        // coś poszło nie tak
        // zwracamy wynik z błędem do głównego kontrolera
        $état = 1041;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // dane podatkowe są pobierane z pamięci o zakresie [redis]
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      // tworzy się instancję obiektu [TaxAdminData] na podstawie poprzedniej tablicy atrybutów
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
    }

    // zwracamy wynik do głównego kontrolera
    $état = 1000;
    return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
  }

}

Komentarze

  • wiersz 12: podobnie jak inne kontrolery serwera, [AdminDataController] implementuje interfejs [InterfaceController], na który składa się metoda [execute] z wierszy 19–79;
  • wiersz 78: podobnie jak w przypadku innych kontrolerów serwera, metoda [AdminDataController.execute] zwraca tablicę [$status, $état, [‘réponse’=>$response]] zawierającą:
    • [$status]: kod statusu odpowiedzi HTTP;
    • [$état]: wewnętrzny kod aplikacji reprezentujący stan, w jakim znajduje się serwer po wykonaniu żądania klienta;
    • [$response]: tablica zawierająca odpowiedź, która ma zostać wysłana do klienta. W tym przypadku tablica ta zostanie później przekształcona w ciąg znaków jSON;
  • wiersze 25–34: sprawdzane jest, czy akcja klienta [get-admindata] jest poprawna pod względem składniowym;
  • wiersze 37–74: pobierany jest obiekt [TaxAdminData] znaleziony albo:
    • wiersze 56–59: w bazie danych, jeśli nie znaleziono go w pamięci podręcznej [redis];
    • wiersze 70–73: z pamięci podręcznej [redis];

Kod ten jest identyczny z kodem kontrolera [CalculerImpotController] omówionym w artykule (link). W rzeczywistości ten kontroler również miał pobrać obiekt [TaxAdminData] zawierający dane administracji podatkowej.

Podczas testów klienta JavaScript forma jSON obiektu [TaxAdminData] stanowiła problem, gdy obiekt ten znajdował się w pamięci podręcznej [redis]. Aby to zrozumieć, przyjrzyjmy się, w jakiej postaci obiekt ten jest przechowywany w [redis]:

Image

Image

  • W przypadku [5-7] widać, że wartości liczbowe zostały zapisane w postaci ciągów znaków. PHP poradził sobie z tym, ponieważ operator + w obliczeniach między liczbami a ciągami znaków powoduje domyślnie zmianę typu z ciągu znaków na liczbę. Jednak JavaScript działa odwrotnie: operator + w obliczeniach między liczbami a ciągami znaków powoduje domyślną zmianę typu z liczby na ciąg znaków. Obliczenia w klasie JavaScript [Métier] są zatem błędne;

Aby rozwiązać ten problem, modyfikujemy metodę [TaxAdminData.setFromArrayOfAttributes] używaną w linii 71 kontrolera w celu utworzenia instancji obiektu [TaxAdminData] (patrz artykuł) na podstawie ciągu znaków jSON znalezionego w pamięci podręcznej [redis]:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // przedziały podatkowe
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // stałe służące do obliczania podatku
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // inicjalizacja
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // element nadrzędny
    parent::setFromJsonFile($taxAdminDataFilename);
    // sprawdzanie wartości atrybutów
    $this->checkAttributes();
    // zwracamy obiekt
    return $this;
  }

  protected function check($value): \stdClass {
    // $value jest tablicą elementów typu string lub pojedynczym elementem
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // przekształca się tablicę ciągów znaków w tablicę liczb rzeczywistych
    $newTableau = [];
    $result = new \stdClass();
    // elementy tablicy muszą być dodatnimi liczbami dziesiętnymi lub zerami
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // umieszczamy liczbę zmiennoprzecinkową w newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // odnotowuje się błąd
        $result->erreur = TRUE;
        // kończymy
        return $result;
      }
    }
    // zwracamy wynik
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // pojedyncza wartość
      $result->value = $newTableau[0];
    } else {
      // lista wartości
      $result->value = $newTableau;
    }
    return $result;
  }

  // inicjalizacja za pomocą tablicy atrybutów
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // element nadrzędny
    parent::setFromArrayOfAttributes($arrayOfAttributes);
    // sprawdzamy wartości atrybutów
    $this->checkAttributes();
    // zwracamy obiekt
    return $this;
  }

  // sprawdzanie wartości atrybutów
  protected function checkAttributes() {
    // sprawdzamy, czy wartości atrybutów są liczbami rzeczywistymi >=0
    foreach ($this as $key => $value) {
      if (is_string($value)) {
        // $value musi być liczbą rzeczywistą >=0 lub tablicą liczb rzeczywistych >=0
        $result = $this->check($value);
        // błąd?
        if ($result->erreur) {
          // wyzwalany jest wyjątek
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // zapisano wartość
          $this->$key = $result->value;
        }
      }
    }

    // zwracamy obiekt
    return $this;
  }

  // metody pobierające i ustawiające
  ...

}

Komentarze

  • wiersz 5: klasa [TaxAdminData] dziedziczy po klasie [BaseEntity], która posiada już metodę [setFromArrayOfAttributes]. Ponieważ ta metoda nie jest odpowiednia, redefiniujemy ją w wierszach 67–75;
  • wiersz 70: metoda [setFromArrayOfAttributes] klasy nadrzędnej jest najpierw wykorzystywana do zainicjowania atrybutów klasy;
  • wiersz 72: metoda [checkAttributes] sprawdza, czy powiązane wartości są liczbami. Jeśli są to ciągi znaków, są one konwertowane na liczby;
  • wiersz 74: renderowany obiekt [$this] jest zatem obiektem z atrybutami o wartościach liczbowych;
  • wiersze 78–93: metoda [checkAttributes] sprawdza, czy wartości powiązane z atrybutami obiektu są rzeczywiście liczbami;
  • wiersz 80: przeglądana jest lista atrybutów;
  • wiersz 81: jeśli wartość atrybutu jest typu [string];
  • wiersz 83: wówczas sprawdzane jest, czy ten ciąg znaków reprezentuje liczbę;
  • wiersz 90: jeśli tak jest, ciąg znaków jest przekształcany na liczbę i przypisywany do sprawdzanego atrybutu;
  • wiersze 85–86: jeśli tak nie jest, zgłaszany jest wyjątek;
  • wiersze 32–65: funkcja [check] wykonuje nieco więcej niż to konieczne. Obsługuje zarówno tablice, jak i pojedyncze wartości. Tymczasem w tym przypadku jest ona wywoływana wyłącznie w celu sprawdzenia wartości typu [string]. Zwraca obiekt o właściwościach [erreur, value], gdzie:
    • [erreur] to wartość logiczna wskazująca, czy wystąpił błąd, czy nie;
    • [value] to parametr [value] z wiersza 32, przekształcony w liczbę lub tablicę liczb w zależności od sytuacji;

Klasa [BaseEntity], która mogła posiadać atrybut o nazwie [arrayOfAttributes], została zmodyfikowana tak, aby go nie zawierała: powoduje on bowiem zanieczyszczenie ciągu jSON wartością [TaxAdminData]. Klasa została przepisana w następujący sposób:


<?php

namespace Application;

class BaseEntity {
  
  // inicjalizacja z pliku JSON
  public function setFromJsonFile(string $jsonFilename) {
    // pobieramy zawartość pliku z danymi podatkowymi
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // błąd?
    if (!$fileContents) {
      // odnotowujemy błąd
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // pobieranie kodu JSON z pliku konfiguracyjnego do tablicy asocjacyjnej
      $arrayOfAttributes = \json_decode($fileContents, true);
      // błąd?
      if ($arrayOfAttributes === FALSE) {
        // odnotowuje się błąd
        $erreur = TRUE;
        $message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // błąd?
    if ($erreur) {
      // rzucamy wyjątek
      throw new ExceptionImpots($message);
    }
    // inicjalizacja atrybutów klasy
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // sprawdzamy obecność wszystkich atrybutów
    $this->checkForAllAttributes($arrayOfAttributes);
    // zwracamy obiekt
    return $this;
  }

  public function checkForAllAttributes($arrayOfAttributes) {
    // sprawdzamy, czy wszystkie klucze zostały zainicjowane
    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) {
    // inicjowanie niektórych atrybutów klasy (niekoniecznie wszystkich)
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // zwraca się obiekt
    return $this;
  }

  // toString
  public function __toString() {
    // atrybuty obiektu
    $arrayOfAttributes = \get_object_vars($this);
    // ciąg znaków jSON obiektu
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }

}

Komentarze

  • wiersz 20: atrybut [$this→arrayOfAttributes] został przekształcony w zmienną, która musi być teraz przekazywana do metody [checkForAllAttributes], wiersz 38, która wcześniej operowała na atrybucie [$this→arrayOfAttributes];

W związku z tą zmianą dotyczącą [BaseEntity] klasa [Database] również wymaga niewielkiej modyfikacji:


<?php

namespace Application;

class Database extends BaseEntity {
  // atrybuty
  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;

  // metoda ustawiająca
  // inicjalizacja
  public function setFromJsonFile(string $jsonFilename) {
    // obiekt nadrzędny
    parent::setFromJsonFile($jsonFilename);
    // zwraca obiekt
    return $this;
  }

  // metody pobierające i ustawiające
  ...
}

Uwagi

  • w oryginalnym kodzie, po wierszu 30 wywoływano metodę [parent::checkForAllAttributes]. Nie ma już takiej potrzeby, ponieważ jest to teraz obsługiwane automatycznie przez metodę [parent::setFromJsonFile($jsonFilename)];

14.3.4. Testy serwera [Postman]

Metoda [Postman] została przedstawiona w artykule (link).

Korzystamy z następujących testów w Postmanie:

Image

Image

Image

Wynik jSON tego ostatniego zapytania jest następujący:

Image

  • w [5-8] można zauważyć, że atrybuty ciągu jSON mają wartości liczbowe (a nie ciągi znaków). Wynik ten pozwoli na normalne wykonanie klasy JavaScript [Métier];

14.3.5. Główny skrypt [main]

Image

Główny skrypt [main] klienta JavaScript wygląda następująco:


// importy
import axios from 'axios';

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

// funkcja asynchroniczna [main]
async function main() {
  // konfiguracja axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instancjonowanie warstwy [dao]
  const dao = new Dao(axios);
  // żądania HTTP
  let taxAdminData;
  try {
    // inicjowanie sesji
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // uwierzytelnianie
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // dane podatkowe
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response.réponse;
  } catch (error) {
    // rejestruje się błąd
    console.log("erreur=", error.message);
    // koniec
    return;
  }

  // instancjonowanie warstwy [métier]
  const métier = new Métier(taxAdminData);

  // obliczenia podatku
  log("-----------calculer-impot x 3");
  const simulations = [];
  simulations.push(métier.calculerImpot("oui", 2, 45000));
  simulations.push(métier.calculerImpot("non", 2, 45000));
  simulations.push(métier.calculerImpot("non", 1, 30000));
  // lista symulacji
  log("-----------liste-des-simulations");
  log(simulations);
  // usunięcie symulacji
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

// wykonanie
main();

Komentarze

  • wiersze 5–6: importy klas [Dao] i [Métier];
  • wiersz 9: funkcja asynchroniczna [main], która będzie organizować komunikację z serwerem za pomocą klasy [Dao] oraz zleci klasie [Métier] wykonanie obliczeń podatkowych;
  • wiersze 10–36: skrypt wywołuje kolejno i w trybie blokującym metody [initSession, authentifierUtilisateur, getAdminData] warstwy [dao];
  • wiersz 38: warstwa [dao] nie jest już potrzebna. Mamy wszystkie elementy niezbędne do uruchomienia warstwy [métier] klienta JavaScript;
  • wiersze 41–46: wykonujemy trzy obliczenia podatku, a ich wyniki sumujemy w tablicy [simulations];
  • wiersz 49: wyświetlamy tabelę symulacji;
  • wiersz 52: usuwamy jedną z nich;

Wyniki wykonania głównego skryptu są następujące:


[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. Klient HTTP 3

Image

W tej sekcji uruchamiamy aplikację [Client HTTP 2] w przeglądarce zgodnie z następującą architekturą:

Image

Przeniesienie nie następuje natychmiast. Chociaż [node.js] potrafi wykonywać kod JavaScript ES6, przeglądarki zazwyczaj nie mają takiej możliwości. Należy zatem skorzystać z narzędzi, które przekształcają kod ES6 na kod ES5 zrozumiały dla nowszych przeglądarek. Na szczęście narzędzia te są zarówno wydajne, jak i dość proste w obsłudze.

W tym przypadku kierowaliśmy się wskazówkami zawartymi w artykule [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].

W folderze [client HTTP 3/src] umieściliśmy elementy [main.js, Métier.js, Dao2.js] aplikacji [Client Http 2], którą właśnie opracowaliśmy.

14.4.1. Inicjalizacja projektu

Będziemy pracować w folderze [client http 3]. Otwieramy terminal w aplikacji [VSCode] i przechodzimy do tego folderu:

Image

Inicjujemy ten projekt za pomocą polecenia [npm init] i na zadane pytania akceptujemy domyślne odpowiedzi:

Image

  • w [4-5] plik konfiguracyjny projektu [package.json] wygenerowany na podstawie różnych udzielonych odpowiedzi;

14.4.2. Instalacja zależności projektu

Zainstalujemy następujące zależności:

  • [@babel/core]: rdzeń narzędzia [Babel] [https://babeljs.io], który przekształca kod ES z 2015 r. i nowszy w kod wykonywalny w najnowszych i starszych przeglądarkach;
  • [@babel/preset-env]: stanowi część zestawu narzędzi Babel. Działa przed transpilacją ES6 → ES5;
  • [babel-loader]: ta zależność umożliwia narzędziu [webpack] wywołanie narzędzia [Babel];
  • [webpack]: koordynator. To właśnie narzędzie [webpack] wykorzystuje Babel do transpilacji kodu z ES6 do ES5, a następnie łączy wszystkie pliki wynikowe w jeden plik;
  • [webpack-cli]: niezbędny dla pliku [webpack];
  • [@webpack-cli/init]: służy do konfiguracji pliku [webpack];
  • [webpack-dev-server]: udostępnia serwer WWW do celów programistycznych, działający domyślnie na porcie 8080. Po wprowadzeniu zmian w plikach źródłowych automatycznie odświeża aplikację internetową;

Zależności projektu instaluje się w następujący sposób w terminalu [VSCode]:

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

Image

Po zainstalowaniu zależności plik [package.json] uległ następującym zmianom:


{
  "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"
  }
}
  • wiersze 12–19: zależności projektu to [devDependencies]: są one potrzebne na etapie rozwoju, ale nie są już wymagane na etapie produkcji. W środowisku produkcyjnym używany jest bowiem plik [dist/main.js]. Jest on zakodowany w ES5 i nie wymaga już narzędzi do transpilacji kodu z ES6 na kod ES5;

Musimy dodać do projektu dwie zależności:

  • [core-js]: zawiera „polyfille” dla ECMAScript z 2019 r. Polyfill pozwala na uruchamianie nowszego kodu, takiego jak ECMAScript 2019 (wrzesień 2019), w starszych przeglądarkach;
  • [regenerator-runtime]: zgodnie z informacją na stronie biblioteki --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];

Te dwie zależności zastępują, począwszy od wersji Babel 7, zależność [@babel/polyfill], która wcześniej pełniła tę rolę, a obecnie (wrzesień 2019 r.) jest wycofana. Instaluje się je w następujący sposób:

Image

Plik [package.json] zmienia się zatem w następujący sposób:


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

Korzystanie z zależności [core-js, regenerator-runtime] wymaga umieszczenia następujących plików [imports] (wiersze 3–4) w głównym skrypcie [src/main.js]:


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

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

14.4.3. Konfiguracja [webpack]

[webpack] to narzędzie, które będzie sterować:

  • transpilację ES6 → ES5 wszystkich plików JavaScript w projekcie;
  • łączenie wygenerowanych plików w jeden plik;

Narzędzie to jest sterowane przez plik konfiguracyjny [webpack.config.js], który można wygenerować za pomocą biblioteki o nazwie [@webpack-cli/init] (wrzesień 2019 r.). Została ona zainstalowana wraz z pozostałymi bibliotekami wymienionymi w sekcji „Link”.

W terminalu uruchamiamy polecenie [npx webpack-cli init]:

Image

Po udzieleniu odpowiedzi na różne pytania (w przypadku których można zaakceptować większość domyślnych odpowiedzi) w katalogu głównym projektu [4] generowany jest plik [webpack.config.js]:

Plik [webpack.config.js] wygląda następująco:


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

Nie rozumiem wszystkich szczegółów tego pliku, ale można zwrócić uwagę na kilka kwestii:

  • wiersz 1: plik nie zawiera kodu ES6. [Eslint] zgłasza zatem błędy, które sięgają aż do katalogu głównego projektu [javascript]. To kłopotliwe. Aby zapobiec analizowaniu pliku przez Eslint, wystarczy skomentować wiersz 1;
  • wiersz 31: pracujemy w trybie [développement];
  • wiersz 32: skrypt wejściowy to tutaj [src/index.js]. Będziemy musieli to zmienić;
  • wiersz 36: folder, w którym będą zapisywane wyniki działania [webpack], to folder [dist];
  • wiersz 46: widać, że [webpack] korzysta z [babel-loader], jednej z zależności, które zainstalowaliśmy;
  • wiersz 54: widać, że [webpack] korzysta z [@babel-preset/env], jednej z zainstalowanych przez nas zależności;

Inicjalizacja [webpack] spowodowała modyfikację pliku [package.json] (wymaga to upoważnienia):


{
  "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"
  }
}
  • wiersz 4: został zmodyfikowany;
  • wiersze 8–9, 18–19: zostały dodane;
  • wiersz 8: zadanie [npm], które umożliwia kompilację projektu;
  • wiersz 9: zadanie [npm], które umożliwia jego uruchomienie;
  • wiersz 18: ?
  • wiersz 19: umożliwia wygenerowanie pliku [dist/index.html], który automatycznie zawiera skrypt [dist/main.js] wygenerowany przez [webpack] i to właśnie ten skrypt jest wykorzystywany podczas uruchamiania projektu;

Wreszcie konfiguracja pliku [webpack] spowodowała wygenerowanie pliku [src/index.js]:

Image

Zawartość pliku [index.js] jest następująca (wrzesień 2019 r.):


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

14.4.4. Kompilacja i uruchomienie projektu

Plik [package.json] zawiera trzy zadania [npm]:


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

Zadania te są zawarte w zadaniu [VSCode], które proponuje je do wykonania:

Image

  • w zadaniu [1-3] kompilowany jest projekt;
  • w [4]: projekt jest kompilowany w [dist/main.hash.js] i tworzona jest strona [dist/index.html];

Wygenerowana strona [index.html] wygląda następująco:


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

Strona ta służy zatem wyłącznie do zawarcia pliku [main.hash.js] wygenerowanego przez [webpack].

Projekt jest realizowany przez zadanie [start]:

Image

Strona [dist/index.html] jest następnie ładowana na serwer należący do pakietu [webpack], działającego na porcie 8080 lokalnego komputera, i wyświetlana przez domyślną przeglądarkę tego komputera:

Image

  • w przypadku [2] – port usługowy serwera WWW [webpack];
  • na [3] treść strony [dist/index.html] jest pusta;
  • w [4] – zakładka [console] w narzędziach programistycznych przeglądarki, w tym przypadku Firefox (F12);
  • w [5] – wynik wykonania pliku [src/index.js]. Przypominamy, że jego zawartość była następująca:
console.log("Hello World from your main file!");

Teraz zmieńmy tę treść na następujący wiersz:

console.log("Bonjour le monde");

Automatycznie (bez ponownej kompilacji) generowane są nowe pliki [main.js, index.html], a nowy plik [index.html] jest ładowany w przeglądarce:

Image

Nie ma potrzeby uruchamiania zadania [build] przed zadaniem [start]: to ostatnie najpierw dokonuje kompilacji projektu. Nie zapisuje ono wyników tej kompilacji w folderze [dist]. Aby się o tym przekonać, wystarczy usunąć ten folder. Widać wtedy, że zadanie [start] kompiluje i uruchamia projekt bez tworzenia folderu [dist]. Wydaje się, że przechowuje swoje pliki wyjściowe [index.html, main.hash.js] w folderze przypisanym do zadania [webpackdev-server]. Takie zachowanie jest wystarczające dla naszych testów.

Po uruchomieniu serwera deweloperskiego każda zapisana zmiana w jednym z plików projektu powoduje rekompilację. Z tego powodu wyłączamy tryb [Auto Save] w [VSCode]. Nie chcemy bowiem, aby rekompilacja następowała za każdym razem, gdy wpisujemy znaki w jednym z plików projektu. Chcemy, aby rekompilacja następowała dopiero w momencie zapisywania zmian:

Image

  • w pliku [2] opcja [Auto Save] nie może być zaznaczona;

14.4.5. Testy klienta JavaScript serwera obliczającego podatek

Aby przetestować klienta JavaScript serwera obliczającego podatek, należy wyznaczyć [main.js] [1] jako punkt wejścia projektu w pliku [webpack.config.js] [2-3]:

Image

Należy pamiętać, że skrypt [main.js] musi zawierać dwa dodatkowe importy w porównaniu z wersją zawartą w pliku [Client http 2]:

Image

Ponadto nieznacznie zmodyfikowaliśmy kod, aby obsługiwał błędy, które może wysyłać serwer:


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

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

// funkcja asynchroniczna [main]
async function main() {
  // konfiguracja axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instancjonowanie warstwy [dao]
  const dao = new Dao(axios);
  // żądania HTTP
  let taxAdminData;
  try {
    // inicjowanie sesji
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    if (response.état != 700) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // uwierzytelnianie
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    if (response.état != 200) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // dane podatkowe
    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) {
    // rejestruje się błąd
    console.log("erreur=", error.message);
    // koniec
    return;
  }

  // instancjonowanie warstwy [métier]
  const métier = new Métier(taxAdminData);

  // obliczenia podatku
  log("-----------calculer-impot x 3");
  const simulations = [];
  simulations.push(métier.calculerImpot("oui", 2, 45000));
  simulations.push(métier.calculerImpot("non", 2, 45000));
  simulations.push(métier.calculerImpot("non", 1, 30000));
  // lista symulacji
  log("-----------liste-des-simulations");
  log(simulations);
  // usunięcie symulacji
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

// uruchomienie
main();

Komentarze

  • W wierszach [24-26], [31-33], [38-40] sprawdzamy kod [response.état] przesłany w odpowiedzi jSON z serwera. Jeśli kod ten wskazuje na błąd, generowany jest wyjątek z komunikatem o błędzie w postaci ciągu znaków jSON z odpowiedzi serwera [response.réponse];

Następnie uruchamiamy projekt [5-6].

Następnie generowana jest strona [index.html] i ładowana w przeglądarce:

Image

  • w [7] widać, że akcja [init-session] nie mogła zostać zakończona z powodu problemu [CORS] (Cross-Origin Resource Sharing);

Problem CORS wynika z relacji klient–serwer:

  • nasz klient JavaScript został pobrany na komputer [http://localhost:8080];
  • serwer obliczający podatek działa na komputerze o identyfikatorze [http://localhost:80];
  • klient i serwer nie znajdują się zatem w tych samych domenach (ta sama maszyna, ale inny port);
  • przeglądarka, w której uruchomiono klienta JavaScript pobranego z serwera [http://localhost:8080], blokuje każde żądanie, którego adresem docelowym nie jest [http://localhost:80]. Jest to środek bezpieczeństwa. W związku z tym blokuje on również żądanie klienta skierowane do serwera działającego na maszynie [http://localhost:80];

W rzeczywistości przeglądarka nie blokuje żądania całkowicie. Czeka ona, aż serwer „poinformuje” ją, że akceptuje żądania międzydomenowe. Jeśli otrzyma takie zezwolenie, przeglądarka prześle żądanie międzydomenowe.

Serwer udziela zgody, wysyłając określone nagłówki 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
  • wiersz 1: klient JavaScript działa w domenie [http://localhost:8080]. Serwer musi wyraźnie odpowiedzieć, że akceptuje tę domenę;
  • wiersz 2: klient JavaScript będzie używał w swoich żądaniach nagłówków HTTP i [Accept, Content-Type]:
    • [Accept]: ten nagłówek jest wysyłany w każdym żądaniu;
    • [Content-Type]: ten nagłówek jest używany w operacjach POST w celu wskazania typu parametrów POST;

Serwer musi wyraźnie zaakceptować te dwa nagłówki HTTP;

  • wiersz 3: klient JavaScript będzie korzystał z żądań GET i POST. Serwer musi wyraźnie akceptować oba te typy żądań;
  • wiersz 4: klient JavaScript będzie wysyłał pliki cookie sesji. Serwer akceptuje je wraz z nagłówkiem z wiersza 4;

Musimy zatem zmodyfikować serwer. Robimy to w pliku [Netbeans]. Problem z plikiem CORS występuje wyłącznie w trybie programistycznym. W środowisku produkcyjnym klient i serwer będą działać w tej samej domenie [http://localhost:80] i nie wystąpi problem CORS. Musimy zatem znaleźć sposób na zezwolenie lub zablokowanie żądań CORS poprzez konfigurację serwera.

Image

Zmiany na serwerze wprowadza się w trzech miejscach:

  • [1, 4]: w pliku konfiguracyjnym [config.json], aby umieścić w nim zmienną logiczną, która będzie kontrolować, czy żądania międzydomenowe są akceptowane, czy nie;
  • [2]: w klasie [ParentResponse], która wysyła odpowiedź do klienta JavaScript. To właśnie ta klasa wyśle nagłówki CORS oczekiwane przez przeglądarkę klienta;
  • [3]: w klasach [HtmlResponse, JsonResponse, XmlResponse], które generują odpowiedzi odpowiednio dla sesji [html, json, xml]. Klasy te muszą przekazać do swojej klasy nadrzędnej [2] wartość logiczną [corsAllowed] znajdującą się w klasie [4]. Odbywa się to w klasie [5] poprzez przekazanie tablicy obrazów z pliku jSON do klasy [2];

Klasa [ParentResponse] [2] zmienia się w następujący sposób:


<?php

namespace Application;

// zależności Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ParentResponse {

  // int $statusCode: kod statusu odpowiedzi HTTP
  // ciąg znaków $content: treść odpowiedzi do wysłania
  // w zależności od przypadku jest to ciąg znaków JSON, XML, HTML
  // tablica $headers: nagłówki HTTP, które należy dodać do odpowiedzi

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

    // przygotowanie tekstowej odpowiedzi serwera
    $response = new Response();
    $response->setCharset("utf-8");
    // kod statusu
    $response->setStatusCode($statusCode);
    // nagłówki dla żądań międzydomenowych
    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);
    }
    // szczególny przypadek metody [OPTIONS]
    // w tym przypadku istotne są wyłącznie nagłówki
    $method = strtolower($request->getMethod());
    if ($method === "options") {
      $content = "";
      $response->setStatusCode(Response::HTTP_OK);
    }
    // wysyłamy odpowiedź
    $response->setContent($content);
    $response->send();
  }

}
  • wiersz 29: sprawdzamy, czy należy obsłużyć żądania międzydomenowe. Jeśli tak, generujemy nagłówki HTTP i CORS (wiersze 33–37), nawet jeśli bieżące żądanie nie jest żądaniem międzydomenowym. W tym ostatnim przypadku nagłówki CORS będą zbędne i nie zostaną wykorzystane przez klienta;
  • wiersz 30: w żądaniu międzydomenowym przeglądarka klienta, która wysyła zapytanie do serwera, wysyła nagłówki HTTP i [Origin: http://localhost:8080] (w konkretnym przypadku naszego klienta JavaScript). W wierszu 30 pobieramy ten nagłówek HTTP z żądania [$request];
  • w wierszu 31: akceptowane będą wyłącznie żądania międzydomenowe pochodzące z komputera o identyfikatorze [http://localhost]. Przypominamy, że żądania te mają miejsce wyłącznie w trybie programowania projektu;
  • wiersze 32–36: dodajemy nagłówki CORS do nagłówków już obecnych w tabeli [$headers];
  • wiersze 45–49: sposób, w jaki przeglądarka kliencka żąda uprawnień CORS, może się różnić w zależności od uruchomionego klienta. Czasami zdarza się, że przeglądarka kliencka żąda tych uprawnień za pomocą polecenia HTTP [OPTIONS]. Jest to nowość dla naszego serwera, który został zaprojektowany wyłącznie do obsługi poleceń [GET, POST]. W przypadku polecenia [OPTIONS] serwer generuje obecnie odpowiedź o błędzie. W wierszach 46–49 korygujemy to w ostatniej chwili: jeśli w wierszu 46 stwierdzimy, że bieżące polecenie to [OPTIONS], wówczas generujemy dla klienta:
    • w wierszach 47 i 51: pustą odpowiedź [$content];
    • w wierszu 48: kod statusu 200 wskazujący, że polecenie zakończyło się powodzeniem. Jedyną istotną rzeczą w przypadku tego polecenia jest wysłanie nagłówków CORS z wierszy 33–36. Tego właśnie oczekuje przeglądarka klienta;

Po wprowadzeniu tej poprawki na serwerze klient JavaScript działa lepiej, ale pojawia się nowy błąd:

Image

  • w [1] sesja jSON jest poprawnie zainicjowana;
  • w [2] akcja [authentifier-utilisateur] kończy się niepowodzeniem: serwer sygnalizuje, że nie ma aktywnej sesji. Oznacza to, że klient JavaScript nie odesłał serwerowi poprawnie pliku cookie sesji, który wysłał podczas akcji [init-session];

Przyjrzyjmy się wymianie danych sieciowych, która miała miejsce:

Image

  • w [4], żądanie [init-session]. Przebiegło ono pomyślnie, a kod statusu odpowiedzi to 200;
  • w [5], żądanie [authentifier-utilisateur]. To żądanie zakończyło się niepowodzeniem z kodem 400 (Bad Request) [6] jako statusem odpowiedzi;

Jeśli przyjrzymy się nagłówkom HTTP i [7] żądania [5], można zauważyć, że klient JavaScript nie wysłał nagłówków HTTP i [Cookie], które umożliwiłyby mu odesłanie pliku cookie sesji wysłanego początkowo przez serwer. To właśnie dlatego serwer zgłasza, że nie ma sesji.

Aby klient wysłał plik cookie sesji, należy dodać konfigurację do obiektu [axios]:


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

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

// funkcja asynchroniczna [main]
async function main() {
  // konfiguracja axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  axios.defaults.withCredentials = true;
  // instancjonowanie warstwy [dao]
  const dao = new Dao(axios);
  // żądania HTTP
  let taxAdminData;
...

Wiersz 15 nakazuje dołączenie plików cookie do nagłówków HTTP w żądaniu [axios]. Należy zauważyć, że nie było to konieczne w środowisku [node.js]. Istnieją zatem różnice w kodzie między tymi dwoma środowiskami.

Po usunięciu tego błędu klient JavaScript działa normalnie:

Image

Image

14.5. Ulepszenie klienta HTTP 3

Gdy poprzednia klasa [Dao2] jest uruchamiana w przeglądarce, zarządzanie plikiem cookie sesji nie jest konieczne. To właśnie przeglądarka, w której działa warstwa [dao], zarządza plikiem cookie sesji: automatycznie zwraca każdy plik cookie, który otrzymuje od serwera. W związku z tym klasę [Dao2] można przepisać na następującą klasę [Dao3]:


"use strict";

// importy
import qs from "qs";

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

  // inicjowanie sesji
  async initSession() {
    // opcje zapytania HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // parametry URL
      params: {
        action: "init-session",
        type: "json"
      }
    };
    // wykonanie zapytania HTTP
    return await this.getRemoteData(options);
  }

  async authentifierUtilisateur(user, password) {
    // opcje zapytania HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        "Content-type": "application/x-www-form-urlencoded"
      },
      // treść zapytania POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // parametry URL
      params: {
        action: "authentifier-utilisateur"
      }
    };
    // wykonanie zapytania HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // opcje zapytania HHTP  [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // parametry zapytania URL
      params: {
        action: "get-admindata"
      }
    };
    // wykonanie zapytania HTTP
    const data = await this.getRemoteData(options);
    // wynik
    return data;
  }

  async getRemoteData(options) {
    // wykonanie zapytania HTTP
    let response;
    try {
      // zapytanie asynchroniczne
      response = await this.axios.request("main.php", options);
    } catch (error) {
      // parametr [error] jest instancją wyjątku – może przybierać różne formy
      if (error.response) {
        // odpowiedź serwera znajduje się w [error.response]
        response = error.response;
      } else {
        // błąd jest ponownie generowany
        throw error;
      }
    }
    // odpowiedź stanowi całość odpowiedzi serwera HTTP (nagłówki HTTP + sama odpowiedź)
    // odpowiedź serwera znajduje się w [response.data]
    return response.data;
  }
}

// eksport klasy
export default Dao3;

Wszystko, co dotyczyło obsługi pliku cookie zarządzania, zniknęło.

Modyfikujemy poprzedni projekt w następujący sposób:

Image

W folderze [src] dodaliśmy dwa pliki:

  • klasę [Dao3], którą właśnie przedstawiliśmy;
  • plik [main3], którego zadaniem jest uruchomienie nowej wersji;

Plik [main3] pozostaje identyczny z plikiem [main] z poprzedniej wersji, ale teraz korzysta z klasy [Dao3]:


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

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

// funkcja asynchroniczna [main]
async function main() {
  // konfiguracja axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL =
    "http://localhost/php7/scripts-web/impots/version-14";
  axios.defaults.withCredentials = true;
  // instancjonowanie warstwy [dao]
  const dao = new Dao(axios);
  // żądania HTTP
  ...
}

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

// wykonanie
main();

Plik [webpack.config] został zmodyfikowany tak, aby teraz uruchamiał skrypt [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",
  //wpis: „./src/mainjs”,
  entry: "./src/main3.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },

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

Po wykonaniu tych czynności uruchamiamy projekt po uruchomieniu serwera obliczającego podatek:

Image

Wyniki wyświetlane w konsoli przeglądarki są identyczne jak w poprzedniej wersji.

14.6. Conclusion

Mamy teraz wszystkie narzędzia potrzebne do tworzenia kodu JavaScript dla aplikacji internetowej. Możemy:

  • korzystać z najnowszego kodu ECMAScript;
  • testować poszczególne elementy tego kodu w prostszym środowisku [node.js], przeznaczonym do debugowania i testowania;
  • następnie przenieść ten kod do przeglądarki za pomocą narzędzi [babel] i [webpack];