Skip to content

14. HTTP-Javascript-Clients des Steuerberechnungsdienstes

14.1. Introduction

Wir beabsichtigen hier, einen Client [node.js] für Version 14 des Steuerberechnungsdienstes zu schreiben. Die Client-Server-Architektur sieht wie folgt aus:

Image

Wir werden zwei Versionen des Clients betrachten:

  • Die Version 1 des Clients weist die folgende Schichtenstruktur auf: [main, dao]

Image

  • Version 2 des Clients wird die Struktur [main, métier, dao] aufweisen. Die Serverschicht [métier] wird auf den Client verlagert:

Image

14.2. Client HTTP 1

Image

Wie bereits erwähnt, implementiert der Client HTTP 1 die folgende Client-Server-Architektur:

Image

Wir werden Folgendes implementieren:

  • die Schicht [dao] in Form einer Klasse;
  • die Schicht [main] in Form eines Skripts, das diese Klasse verwendet;

14.2.1. Die Schicht [dao]

Die Schicht [dao] wird durch die folgende Klasse [Dao1.js] implementiert:


'use strict';

// Importe
import qs from 'qs'

class Dao1 {

  // Konstruktor
  constructor(axios) {
    // Axios-Bibliothek zur Durchführung von Anfragen HTTP
    this.axios = axios;
    // Sitzungs-Cookie
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // Sitzung starten
  async  initSession() {
    // Abfrageoptionen HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // Parameter der Abfrage URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // Ausführung der Abfrage HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // Optionen der Abfrage HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-Type: 'application/x-www-form-urlencoded',
      },
      // Hauptteil des POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // Parameter der URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // Ausführung der Abfrage HTTP
    return await this.getRemoteData(options);
  }

  // Steuerberechnung
  async  calculerImpot(marié, enfants, salaire) {
    // Optionen der Anfrage HHTP [post /main.php?action=calculer-impot]
    const options = {
      method: "POST",
      headers: {
        '„Content-type“: „application/x-www-form-urlencoded“,
      },
      // Hauptteil des POST [marié, enfants, salaire]
      data: qs.stringify({
        marié: marié,
        enfants: enfants,
        salaire: salaire
      }),
      // Parameter des URL
      params: {
        action: 'calculer-impot'
      }
    };
    // Ausführung der Abfrage HTTP
    const data = await this.getRemoteData(options);
    // Ergebnis
    return data;
  }

  // Liste der Simulationen
  async  listeSimulations() {
    // Optionen der Abfrage HHTP [get /main.php?action=lister-simulations]
    const options = {
      method: "GET",
      // Parameter der Abfrage URL
      params: {
        action: 'lister-simulations'
      },
    };
    // Ausführung der Abfrage HTTP
    const data = await this.getRemoteData(options);
    // Ergebnis
    return data;
  }

  // Liste der Simulationen
  async  supprimerSimulation(index) {
    // Optionen der Abfrage HHTP  [get /main.php?action=supprimer-simulation&numéro=index]
    const options = {
      method: "GET",
      // Parameter der Abfrage URL
      params: {
        action: 'supprimer-simulation',
        numéro: index
      },
    };
    // Ausführung der Abfrage HTTP
    const data = await this.getRemoteData(options);
    // Ergebnis
    return data;
  }

  async  getRemoteData(options) {
    // für das Sitzungs-Cookie
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // Ausführung der Anfrage HTTP
    let response;
    try {
      // asynchrone Anfrage
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // Der Parameter [error] ist eine Ausnahmeinstanz – er kann verschiedene Formen annehmen
      if (error.response) {
        // Die Antwort des Servers befindet sich in [error.response]
        response = error.response;
      } else {
        // Der Fehler wird erneut ausgelöst
        throw error;
      }
    }
    // „response“ ist die gesamte Antwort HTTP des Servers (Header HTTP + die Antwort selbst)
    // Das Sitzungs-Cookie wird abgerufen, sofern es vorhanden ist
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie ist ein Array
      // Das Sitzungs-Cookie wird in diesem Array gesucht
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // Das Session-Cookie wird gesucht
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // Das Session-Cookie wird gespeichert
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // Es wurde gefunden
          trouvé = true;
        } else {
          // nächstes Element
          i++;
        }
      }
    }
    // Die Antwort des Servers lautet: [response.data]
    return response.data;
  }
}

// Export der Klasse
export default Dao1;
  • Wir wenden hier das an, was wir im Abschnitt „Link“ gelernt haben, in dem wir die Bibliothek [axios] vorgestellt haben, mit der sich HTTP-Abfragen sowohl unter [node.js] als auch in einem Browser ausführen lassen. Wir werden uns insbesondere das Skript aus dem Abschnitt „Link“ ansehen;
  • Zeilen 9–15: der Konstruktor der Klasse. Diese wird drei Eigenschaften haben:
    • [axios]: das Objekt [axios], mit dem die Abfragen HTTP durchgeführt werden können. Dieses wird vom aufrufenden Code übergeben;
    • [sessionCookieName]: Je nach Server hat das Sitzungs-Cookie unterschiedliche Namen. Hier lautet es [PHPSESSID];
    • [sessionCookie]: Das vom Server gesendete und vom Client gespeicherte Sitzungs-Cookie;
  • Zeilen 53–76: Die asynchrone Funktion [calculerImpot] führt die Anfrage [post /main.php?action=calculer-impot] durch und übermittelt dabei die Parameter [marié, enfants, salaire]. Sie gibt die vom Server übermittelte Zeichenkette jSON in Form eines JavaScript-Objekts zurück;
  • Zeilen 79–92: Die asynchrone Funktion [listeSimulations] führt die Anfrage [$status, $état, [‘réponse’=>$response]] aus. Sie gibt die vom Server übermittelte Zeichenfolge jSON als JavaScript-Objekt zurück;
  • Zeilen 95–109: Die asynchrone Funktion [supprimerSimulation] führt die Anfrage [get /main.php?action=supprimer-simulation&numéro=index] aus. Sie gibt die vom Server übermittelte Zeichenfolge jSON in Form eines JavaScript-Objekts zurück;
  • Zeile 121: Es wird die Notation [this.axios] verwendet, da hier das an den Konstruktor übergebene Objekt [axios] in der Eigenschaft [this.axios] gespeichert wurde;
  • Zeile 161: Die Klasse [Dao1] wird exportiert, damit sie verwendet werden kann;

14.2.2. Das Skript [main1.js]

Das Skript [main1.js] führt mithilfe der Klasse [Dao1] eine Reihe von Aufrufen an den Server durch:

  • Initialisierung einer Sitzung jSON;
  • Authentifizierung mit [admin, admin];
  • fordert drei Steuerberechnungen an;
  • fordert die Liste der Simulationen an;
  • löscht eine davon;

Der Code lautet wie folgt:


// Import von Axios
import axios from 'axios';
// Import der Klasse Dao1
import Dao from './Dao1';

// asynchrone Funktion [main]
async function main() {
  // Axios-Konfiguration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // Instanziierung der Schicht [dao]
  const dao = new Dao(axios);
  // Verwendung der Schicht [dao]
  try {
    // Sitzung initialisieren
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // Authentifizierung
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // Steuerberechnungen
    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);
    // Liste der Simulationen
    log("-----------liste-des-simulations");
    response = await dao.listeSimulations();
    log(response);
    // Löschen einer Simulation
    log("-----------suppression simulation n° 1");
    response = await dao.supprimerSimulation(1);
    log(response);
  } catch (error) {
    // Fehler wird protokolliert
    console.log("erreur=", error.message);
  }
}

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

// Ausführung
main();

Kommentare

  • Zeile 2: Die Bibliothek [axios] wird importiert;
  • Zeile 4: Die Klasse [Dao] wird importiert;
  • Zeile 7: Die Funktion [main], die mit dem Server kommuniziert, ist asynchron;
  • Zeilen 9–10: Standardkonfiguration der Anfragen HTTP, die an den Server gesendet werden:
    • Zeile 9: [timeout] mit einer Dauer von 2 Sekunden;
    • Zeile 10: Alle URL haben als Präfix die Basis-URL der Version 14 des Steuerberechnungsservers;
  • Zeile 12: Die Ebene [Dao] wird erstellt. Sie kann nun verwendet werden;
  • Zeilen 46–48: Die Funktion [log] dient dazu, die Zeichenfolge jSON eines JavaScript-Objekts in einer aufbereiteten Form anzuzeigen: vertikal mit einer Einrückung von zwei Leerzeichen (3. Parameter);
  • Zeilen 15–18: Initialisierung der Sitzung jSON;
  • Zeilen 19–22: Authentifizierung;
  • Zeilen 23–30: Es werden drei Steuerberechnungen parallel angefordert. Dank [await Promise.all] wird die Ausführung blockiert, bis alle drei Ergebnisse vorliegen;
  • Zeilen 31–34: Liste der Simulationen;
  • Zeilen 35–38: Löschen einer Simulation;
  • Zeilen 39–42: Behandlung einer möglichen Ausnahme;

Die Ergebnisse der Ausführung lauten wie folgt:


[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

Image

Die Architektur des Clients HTTP2 sieht wie folgt aus:

Image

Die Schicht [métier] wurde vom Server auf den JavaScript-Client verlagert. Anders als im Kurs PHP7 muss die Schicht [main] hier nicht über die Schicht [métier] laufen, um die Schicht [dao] zu erreichen. Wir werden diese beiden Schichten als Kompetenzzentren nutzen:

  • Die Ebene [main] greift auf die Ebene [dao] zurück, sobald sie Daten benötigt, die sich auf dem Server befinden;
  • die Schicht [main] beauftragt die Schicht [métier] mit der Durchführung der Steuerberechnungen;
  • Die Schicht [métier] ist unabhängig von der Schicht [dao] und ruft diese niemals auf;

14.3.1. Die JavaScript-Klasse [Métier]

Die Umwandlung der Klasse [Métier] in PHP wurde im Artikel unter dem Link beschrieben. Es handelt sich um einen recht komplexen Code, den wir hier nicht zur Erklärung, sondern zur Übersetzung in JavaScript wiedergeben:


<?php

// Namensraum
namespace Application;

class Metier implements InterfaceMetier {
  // Dao-Ebene
  private $dao;
  // Daten der Steuerverwaltung
  private $taxAdminData;

  //---------------------------------------------
  // Setter der Schicht [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceDao $dao) {
    // Es wird eine Referenz auf die Schicht [dao] gespeichert
    $this->dao = $dao;
    // Die Daten für die Steuerberechnung werden abgerufen
    // Die Methode [getTaxAdminData] kann eine Ausnahme ExceptionImpots auslösen
    // diese wird dann an den aufrufenden Code weitergeleitet
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// Steuerberechnung
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié: ja, nein
    // $enfants: Anzahl der Kinder
    // $salaire: Jahresgehalt
    // $this->taxAdminData: Daten der Steuerbehörde
    //
    // Es wird überprüft, ob die Daten der Steuerbehörde vorliegen
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // Steuerberechnung mit Kindern
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // Berechnung der Steuer ohne Kinder
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // Anwendung der Obergrenze für den Familienquotienten
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART Euro für die ersten beiden Kinder
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART Euro für die ersten beiden Kinder, das Doppelte für die folgenden
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // Man wählt den höchsten Steuersatz
    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"];
    }
    // Berechnung eines möglichen Steuerabzugs
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // Berechnung einer eventuellen Steuerermäßigung
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // Ergebnis
    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é: ja, nein
    // $enfants: Anzahl der Kinder
    // $salaire: Jahresgehalt
    // $this->taxAdminData: Daten der Steuerbehörde
    //
    // Anzahl der Anteile
    $marié = strtolower($marié);
    if ($marié === "oui") {
      $nbParts = $enfants / 2 + 2;
    } else {
      $nbParts = $enfants / 2 + 1;
    }
    // 1 Anteil pro Kind ab dem dritten
    if ($enfants >= 3) {
      // ein halber Anteil zusätzlich für jedes Kind ab dem dritten
      $nbParts += 0.5 * ($enfants - 2);
    }
    // steuerpflichtiges Einkommen
    $revenuImposable = $this->getRevenuImposable($salaire);
    // Zuschlag
    $surcôte = floor($revenuImposable - 0.9 * $salaire);
    // bei Rundungsproblemen
    if ($surcôte < 0) {
      $surcôte = 0;
    }
    // Familienquotient
    $quotient = $revenuImposable / $nbParts;
    // Steuerberechnung
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // wird am Ende der Grenzwertetabelle eingefügt, um die nachfolgende Schleife zu beenden
    $limites[count($limites) - 1] = $quotient;
    // Ermittlung des Steuersatzes
    $i = 0;
    while ($quotient > $limites[$i]) {
      $i++;
    }
    // da $quotient am Ende des Arrays $limites platziert wurde, wird die vorherige Schleife
    // darf nicht über das Array $limites hinausgehen
    // Nun kann die Steuer berechnet werden
    $impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
    // Ergebnis
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable = Jahresgehalt – Freibetrag
  // Der Freibetrag hat einen Mindest- und einen Höchstwert
  private function getRevenuImposable(float $salaire): float {
    // Freibetrag in Höhe von 10 % des Gehalts
    $abattement = 0.1 * $salaire;
    // Dieser Freibetrag darf $this->taxAdminData nicht überschreiten->getAbattementDixPourCentMax()
    if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMax();
    }
    // Der Freibetrag darf nicht unter $this->taxAdminData liegen->getAbattementDixPourcentMin()
    if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMin();
    }
    // steuerpflichtiges Einkommen
    $revenuImposable = $salaire - $abattement;
    // Ergebnis
    return floor($revenuImposable);
  }

// berechnet einen eventuellen Abschlag
  private function getDecôte(string $marié, float $salaire, float $impots): float {
    // Anfangswert: Null-Abschlag
    $décôte = 0;
    // Höchststeuerbetrag für den Abzug
    $plafondImpôtPourDécôte = $marié === "oui" ?
      $this->taxAdminData->getPlafondImpotCouplePourDecote() :
      $this->taxAdminData->getPlafondImpotCelibatairePourDecote();
    if ($impots < $plafondImpôtPourDécôte) {
      // Maximaler Abschlagbetrag
      $plafondDécôte = $marié === "oui" ?
        $this->taxAdminData->getPlafondDecoteCouple() :
        $this->taxAdminData->getPlafondDecoteCelibataire();
      // theoretischer Abschlag
      $décôte = $plafondDécôte - 0.75 * $impots;
      // Der Abschlag darf den Steuerbetrag nicht überschreiten
      if ($décôte > $impots) {
        $décôte = $impots;
      }
      // kein Abschlag <0
      if ($décôte < 0) {
        $décôte = 0;
      }
    }
    // Ergebnis
    return ceil($décôte);
  }

// berechnet eine eventuelle Ermäßigung
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    // Einkommensobergrenze für den Anspruch auf die Ermäßigung von 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();
    }
    // steuerpflichtiges Einkommen
    $revenuImposable = $this->getRevenuImposable($salaire);
    // Ermäßigung
    $réduction = 0;
    if ($revenuImposable < $plafondRevenuPourRéduction) {
      // Ermäßigung von 20 %
      $réduction = 0.2 * $impots;
    }
    // Ergebnis
    return ceil($réduction);
  }

  // Steuerberechnung im Batch-Modus
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // Ausnahmen, die aus der Schicht [dao] stammen, werden weitergeleitet
    // Steuerpflichtige Daten werden abgerufen
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // Ergebnistabelle
    $results = [];
    // Auswertung der Ergebnisse
    foreach ($taxPayersData as $taxPayerData) {
      // Die Steuer wird berechnet
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // Ausfüllen von [$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"]);
      // das Ergebnis wird in die Ergebnistabelle eingetragen
      $results [] = $taxPayerData;
    }
    // Speichern der Ergebnisse
    $this->dao->saveResults($resultsFileName, $results);
  }

}
  • Zeilen 19–26: der Konstruktor der Klasse PHP. Da wir gesagt haben, dass wir eine von der Schicht [dao] unabhängige Schicht [métier] erstellen, nehmen wir in JavaScript zwei Änderungen an diesem Konstruktor vor:
    • Er erhält keine Instanz der Schicht [dao] (diese benötigt er nicht mehr);
    • er wird die Steuerdaten der Behörde [taxAdminData] nicht von der Schicht [dao] anfordern: Der aufrufende Code wird diese Daten an den Konstruktor übermitteln;
  • Zeilen 197–122: Wir werden die Methode [executeBatchImpots] nicht implementieren, deren eigentlicher Zweck darin bestand, Simulationsergebnisse in einer Textdatei zu speichern. Wir wollen einen Code, der sowohl unter [node.js] als auch in einem Browser funktioniert. Das Speichern von Daten im Dateisystem des Rechners, auf dem der Client-Browser läuft, ist jedoch nicht möglich;

Unter Berücksichtigung dieser Einschränkungen lautet der Code der JavaScript-Klasse [Métier] wie folgt:


'use strict';

// Klasse „Métier“
class Métier {

  // Konstruktor
  constructor(taxAdmindata) {
    // this.taxAdminData: Daten der Steuerbehörde
    this.taxAdminData = taxAdmindata;
  }

  // Steuerberechnung
  // --------------------------------------------------------------------------
  calculerImpot(marié, enfants, salaire) {
    // verheiratet: ja, nein
    // Kinder: Anzahl der Kinder
    // Gehalt: Jahresgehalt
    // this.taxAdminData: Daten der Steuerbehörde
    //
    // Steuerberechnung mit Kindern
    const result1 = this.calculerImpot2(marié, enfants, salaire);
    const impot1 = result1["impôt"];
    // Steuerberechnung ohne Kinder
    let result2, impot2, plafondDemiPart;
    if (enfants !== 0) {
      result2 = this.calculerImpot2(marié, 0, salaire);
      impot2 = result2["impôt"];
      // Anwendung der Obergrenze für den Familienquotienten
      plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
      if (enfants < 3) {
        // PLAFOND_QF_DEMI_PART Euro für die ersten beiden Kinder
        impot2 = impot2 - enfants * plafondDemiPart;
      } else {
        // PLAFOND_QF_DEMI_PART Euro für die ersten beiden Kinder, das Doppelte für die folgenden
        impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
      }
    } else {
      // keine Neuberechnung der Steuer
      impot2 = impot1;
      result2 = result1;
    }
    // Es wird die höchste Steuer aus [impot1, impot2] herangezogen
    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"];
    }
    // Berechnung eines eventuellen Steuerabzugs
    const décôte = this.getDecôte(marié, impot);
    impot -= décôte;
    // Berechnung einer möglichen Steuerermäßigung
    const réduction = this.getRéduction(marié, salaire, enfants, impot);
    impot -= réduction;
    // Ergebnis
    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) {
    // verheiratet: ja, nein
    // Kinder: Anzahl der Kinder
    // Gehalt: Jahresgehalt
    // this->taxAdminData: Daten der Steuerbehörde
    //
    // Anzahl der Anteile
    marié = marié.toLowerCase();
    let nbParts;
    if (marié === "oui") {
      nbParts = enfants / 2 + 2;
    } else {
      nbParts = enfants / 2 + 1;
    }
    // 1 Anteil pro Kind ab dem dritten
    if (enfants >= 3) {
      // ein halber Anteil zusätzlich für jedes Kind ab dem dritten
      nbParts += 0.5 * (enfants - 2);
    }
    // steuerpflichtiges Einkommen
    const revenuImposable = this.getRevenuImposable(salaire);
    // Zuschlag
    let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
    // wegen Rundungsproblemen
    if (surcôte < 0) {
      surcôte = 0;
    }
    // Familienquotient
    const quotient = revenuImposable / nbParts;
    // Steuerberechnung
    const limites = this.taxAdminData.limites;
    const coeffR = this.taxAdminData.coeffR;
    const coeffN = this.taxAdminData.coeffN;
    // wird am Ende der Grenzwertetabelle eingefügt, um die nachfolgende Schleife zu beenden
    limites[limites.length - 1] = quotient;
    // Ermittlung des Steuersatzes
    let i = 0;
    while (quotient > limites[i]) {
      i++;
    }
    // Da der Familienquotient am Ende des Grenzwert-Arrays platziert wurde, wird die vorherige Schleife
    // kann nicht über die Grenzen des Begrenzungstabells hinausgehen
    // Nun kann die Steuer berechnet werden
    const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
    // Ergebnis
    return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
  }

  // revenuImposable = Jahresgehalt – Freibetrag
  // Der Freibetrag hat einen Mindest- und einen Höchstwert
  getRevenuImposable(salaire) {
    // Abzug von 10 % des Gehalts
    let abattement = 0.1 * salaire;
    // Dieser Freibetrag darf taxAdminData.getAbattementDixPourCentMax() nicht überschreiten
    if (abattement > this.taxAdminData.abattementDixPourCentMax) {
      abattement = this.taxAdminData.abattementDixPourcentMax;
    }
    // Der Freibetrag darf nicht unter taxAdminData.getAbattementDixPourcentMin() liegen
    if (abattement < this.taxAdminData.abattementDixPourcentMin) {
      abattement = this.taxAdminData.abattementDixPourcentMin;
    }
    // steuerpflichtiges Einkommen
    const revenuImposable = salaire - abattement;
    // Ergebnis
    return Math.floor(revenuImposable);
  }

  // berechnet einen eventuellen Abschlag
  getDecôte(marié, impots) {
    // Anfangswert: Null-Abschlag
    let décôte = 0;
    // Höchststeuerbetrag für den Abzug
    let plafondImpôtPourDécôte = marié === "oui" ?
      this.taxAdminData.plafondImpotCouplePourDecote :
      this.taxAdminData.plafondImpotCelibatairePourDecote;
    let plafondDécôte;
    if (impots < plafondImpôtPourDécôte) {
      // Maximaler Abschlagbetrag
      plafondDécôte = marié === "oui" ?
        this.taxAdminData.plafondDecoteCouple :
        this.taxAdminData.plafondDecoteCelibataire;
      // theoretischer Abschlag
      décôte = plafondDécôte - 0.75 * impots;
      // Der Abschlag darf den Steuerbetrag nicht überschreiten
      if (décôte > impots) {
        décôte = impots;
      }
      // kein Abschlag <0
      if (décôte < 0) {
        décôte = 0;
      }
    }
    // Ergebnis
    return Math.ceil(décôte);
  }

  // berechnet eine eventuelle Ermäßigung
  getRéduction(marié, salaire, enfants, impots) {
    // Einkommensobergrenze für den Anspruch auf die Ermäßigung von 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;
    }
    // steuerpflichtiges Einkommen
    const revenuImposable = this.getRevenuImposable(salaire);
    // Ermäßigung
    let réduction = 0;
    if (revenuImposable < plafondRevenuPourRéduction) {
      // Ermäßigung von 20 %
      réduction = 0.2 * impots;
    }
    // Ergebnis
    return Math.ceil(réduction);
  }
}

// Export der Klasse
export default Métier;
  • Der JavaScript-Code folgt genau dem Code PHP;
  • Die Klasse [Métier] wird exportiert, Zeile 187;

14.3.2. Die JavaScript-Klasse [Dao2]

Image

Die Klasse [Dao2] implementiert die Schicht [dao] des oben genannten JavaScript-Clients wie folgt:


'use strict';

// Importe
import qs from 'qs'

class Dao2 {

  // Konstruktor
  constructor(axios) {
    this.axios = axios;
    // Sitzungs-Cookie
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // Sitzung initialisieren
  async  initSession() {
    // Anfrageoptionen HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // Parameter der Abfrage URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // Ausführung der Abfrage HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // Optionen der Abfrage HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-Type: 'application/x-www-form-urlencoded',
      },
      // Hauptteil des POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // Parameter der URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // Ausführung der Abfrage HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // Optionen der Abfrage HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // Parameter der Abfrage URL
      params: {
        action: 'get-admindata'
      }
    };
    // Ausführung der Abfrage HTTP
    const data = await this.getRemoteData(options);
    // Ergebnis
    return data;
  }

  async  getRemoteData(options) {
    // für das Sitzungs-Cookie
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // Ausführung der Abfrage HTTP
    let response;
    try {
      // asynchrone Anfrage
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // Der Parameter [error] ist eine Ausnahmeinstanz – er kann verschiedene Formen annehmen
      if (error.response) {
        // Die Antwort des Servers befindet sich in [error.response]
        response = error.response;
      } else {
        // Der Fehler wird erneut ausgelöst
        throw error;
      }
    }
    // „response“ ist die gesamte Antwort HTTP des Servers (Header HTTP + die Antwort selbst)
    // Das Sitzungs-Cookie wird abgerufen, sofern es vorhanden ist
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie ist ein Array
      // Das Sitzungs-Cookie wird in diesem Array gesucht
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // Das Session-Cookie wird gesucht
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // Das Session-Cookie wird gespeichert
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // Es wurde gefunden
          trouvé = true;
        } else {
          // nächstes Element
          i++;
        }
      }
    }
    // Die Antwort des Servers lautet: [response.data]
    return response.data;
  }
}

// Export der Klasse
export default Dao2;

Anmerkungen

  • Die Klasse [Dao2] implementiert nur drei der möglichen Anfragen an den Steuerberechnungsserver:
    • [init-session] (Zeilen 17–29): zur Initialisierung der Sitzung jSON;
    • [authentifier-utilisateur] (Zeilen 31–50): zur Authentifizierung;
    • [get-admindata] (Zeilen 52–65): zum Abrufen der Daten der Steuerbehörde, die die Steuerberechnungen auf der Client-Seite ermöglichen;
  • Zeilen 52–65: Wir fügen eine neue Aktion [get-admindata] zum Server hinzu. Diese Aktion war bisher nicht implementiert. Das holen wir nun nach.

14.3.3. Änderung am Steuerberechnungsserver

Der Steuerberechnungsserver muss eine neue Aktion implementieren. Wir werden dies in Version 14 des Servers vornehmen. Die zu implementierende Aktion weist folgende Merkmale auf:

  • Sie wird von einer Transaktion [get /main.php?action=get-admindata] angefordert;
  • sie gibt die Zeichenfolge jSON eines Objekts zurück, das die Daten der Steuerbehörde kapselt;

Wir werden uns nun noch einmal ansehen, wie eine Aktion zu unserem Server hinzugefügt wird.

Die Änderung erfolgt in NetBeans:

Image

In [2] bearbeiten wir die Datei [config.json], um die neue Aktion hinzuzufügen:


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

Die Änderung umfasst Folgendes:

  • Zeile 67: Die Aktion [get-admindata] hinzufügen und sie einem Controller zuordnen;
  • Zeile 36: diesen Controller in der Liste der von der Anwendung PHP zu ladenden Klassen deklarieren;

Der nächste Schritt besteht darin, den Controller [AdminDataController] [3] zu implementieren:


<?php

namespace Application;

// Symfony-Abhängigkeiten
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// Alias der Schicht [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

class AdminDataController implements InterfaceController {

  // $config ist die Anwendungskonfiguration
  // Verarbeitung einer Anfrage (Request)
  // nutzt die Sitzung „Session“ und kann diese ändern
  // $infos sind zusätzliche Informationen, die für jeden Controller spezifisch sind
  // gibt ein Array zurück: [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // Es muss ein einziger Parameter vorhanden sein: GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      // Der Fehler wird vermerkt
      $message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
      $état = 1001;
      // Ergebnis wird an den Hauptcontroller zurückgemeldet
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }

    // Es kann weitergearbeitet werden
    // Redis
    \Predis\Autoloader::register();
    try {
      // Client [predis]
      $redis = new \Predis\Client();
      // Verbindung zum Server herstellen, um zu prüfen, ob er verfügbar ist
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // Es ist schiefgelaufen
      // Ergebnis mit Fehler an den Hauptcontroller zurückgemeldet
      $état = 1050;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }

    // Abruf der Daten von der Steuerbehörde
    // Zunächst wird im Cache gesucht: [redis]
    if (!$redis->get("taxAdminData")) {
      try {
        // Die Steuerdaten werden aus der Datenbank abgerufen
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // taxAdminData
        $taxAdminData = $dao->getTaxAdminData();
        // Die abgerufenen Daten werden in Redis gespeichert
        $redis->set("taxAdminData", $taxAdminData);
      } catch (\RuntimeException $ex) {
        // Es ist schiefgelaufen
        // Das Ergebnis wird mit einem Fehler an den Hauptcontroller zurückgemeldet
        $état = 1041;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // Die Steuerdaten werden aus dem Speicher [redis] mit dem Gültigkeitsbereich [application] abgerufen
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      // Ein Objekt [TaxAdminData] wird aus dem vorherigen Attributarray instanziiert
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
    }

    // Rückgabe des Ergebnisses an den Hauptcontroller
    $état = 1000;
    return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
  }

}

Anmerkungen

  • Zeile 12: Wie die anderen Controller des Servers implementiert auch [AdminDataController] die Schnittstelle [InterfaceController], die aus der Methode [execute] in den Zeilen 19–79 besteht;
  • Zeile 78: Wie bei den anderen Controllern des Servers gibt die Methode [AdminDataController.execute] ein Array [$status, $état, [‘réponse’=>$response]] zurück, wobei:
    • [$status]: den Statuscode der Antwort auf HTTP;
    • [$état]: einen anwendungsinternen Code, der den Zustand des Servers nach Ausführung der Client-Anfrage angibt;
    • [$response]: ein Array, das die an den Client zu sendende Antwort enthält. Dieses Array wird hier später in die Zeichenkette jSON umgewandelt;
  • Zeilen 25–34: Es wird überprüft, ob die Aktion [get-admindata] des Kunden syntaktisch korrekt ist;
  • Zeilen 37–74: Es wird ein Objekt [TaxAdminData] abgerufen, das entweder:
    • Zeilen 56–59: aus der Datenbank, falls es nicht im Cache [redis] gefunden wurde;
    • Zeilen 70–73: im Cache [redis];

Dieser Code entspricht dem des Controllers [CalculerImpotController], der im Artikel unter dem Link erläutert wird. Tatsächlich musste auch dieser Controller das Objekt [TaxAdminData] abrufen, das die Daten der Steuerbehörde enthält.

Bei den Tests des JavaScript-Clients verursachte die Form jSON von [TaxAdminData] Probleme, wenn dieses Objekt im Cache [redis] gefunden wurde. Um dies zu verstehen, schauen wir uns an, in welcher Form dieses Objekt in [redis] gespeichert ist:

Image

Image

  • In [5-7] ist zu erkennen, dass numerische Werte als Zeichenketten gespeichert wurden. PHP hat dies akzeptiert, da der Operator „+“ bei Berechnungen zwischen Zahlen und Zeichenketten implizit eine Typumwandlung von der Zeichenkette in eine Zahl bewirkt. JavaScript verhält sich jedoch umgekehrt: Der Operator „+“ bei Berechnungen zwischen Zahlen und Zeichenketten bewirkt implizit eine Typumwandlung von der Zahl in eine Zeichenkette. Die Berechnungen der JavaScript-Klasse [Métier] sind daher fehlerhaft;

Um dieses Problem zu beheben, ändern wir die in Zeile 71 des Controllers verwendete Methode [TaxAdminData.setFromArrayOfAttributes], um ein Objekt [TaxAdminData] zu instanziieren (siehe Artikel) aus der Zeichenkette jSON, die im Cache [redis] gefunden wurde:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // Steuerklassen
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // Konstanten für die Steuerberechnung
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // Initialisierung
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // übergeordnetes Element
    parent::setFromJsonFile($taxAdminDataFilename);
    // Die Attributwerte werden überprüft
    $this->checkAttributes();
    // das Objekt wird zurückgegeben
    return $this;
  }

  protected function check($value): \stdClass {
    // $value ist ein Array mit Elementen vom Typ String oder ein einzelnes Element
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // Das String-Array wird in ein Array von reellen Zahlen umgewandelt
    $newTableau = [];
    $result = new \stdClass();
    // Die Elemente des Arrays müssen positive Dezimalzahlen oder Null sein
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // Man speichert die Gleitkommazahl in newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // Der Fehler wird vermerkt
        $result->erreur = TRUE;
        // man beendet das Programm
        return $result;
      }
    }
    // Das Ergebnis wird ausgegeben
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // ein einzelner Wert
      $result->value = $newTableau[0];
    } else {
      // eine Liste von Werten
      $result->value = $newTableau;
    }
    return $result;
  }

  // Initialisierung über ein Attributarray
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // übergeordnetes Element
    parent::setFromArrayOfAttributes($arrayOfAttributes);
    // die Attributwerte werden überprüft
    $this->checkAttributes();
    // das Objekt wird zurückgegeben
    return $this;
  }

  // Überprüfung der Attributwerte
  protected function checkAttributes() {
    // Es wird überprüft, ob die Attributwerte reelle Zahlen >= 0 sind
    foreach ($this as $key => $value) {
      if (is_string($value)) {
        // $value muss eine reelle Zahl >= 0 oder ein Array aus reellen Zahlen >= 0 sein
        $result = $this->check($value);
        // Fehler?
        if ($result->erreur) {
          // Es wird eine Ausnahme ausgelöst
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // Der Wert wird notiert
          $this->$key = $result->value;
        }
      }
    }

    // Das Objekt wird zurückgegeben
    return $this;
  }

  // Getter und Setter
  ...

}

Anmerkungen

  • Zeile 5: Die Klasse [TaxAdminData] erweitert die Klasse [BaseEntity], die bereits über die Methode [setFromArrayOfAttributes] verfügt. Da diese nicht geeignet ist, definieren wir sie in den Zeilen 67–75 neu;
  • Zeile 70: Die Methode [setFromArrayOfAttributes] der übergeordneten Klasse wird zunächst verwendet, um die Attribute der Klasse zu initialisieren;
  • Zeile 72: Die Methode [checkAttributes] überprüft, ob die zugehörigen Werte tatsächlich Zahlen sind. Handelt es sich um Zeichenketten, werden diese in Zahlen umgewandelt;
  • Zeile 74: Das zurückgegebene Objekt [$this] ist nun ein Objekt mit Attributen, deren Werte numerisch sind;
  • Zeilen 78–93: Die Methode [checkAttributes] überprüft, ob die den Attributen des Objekts zugeordneten Werte tatsächlich numerisch sind;
  • Zeile 80: Die Liste der Attribute wird durchlaufen;
  • Zeile 81: Wenn der Wert eines Attributs vom Typ [string] ist;
  • Zeile 83: Dann wird überprüft, ob diese Zeichenfolge eine Zahl darstellt;
  • Zeile 90: Ist dies der Fall, wird die Zeichenkette in eine Zahl umgewandelt und dem geprüften Attribut zugewiesen;
  • Zeilen 85–86: Ist dies nicht der Fall, wird eine Ausnahme ausgelöst;
  • Zeilen 32–65: Die Funktion [check] leistet etwas mehr als nötig. Sie verarbeitet sowohl Arrays als auch Einzelwerte. Hier wird sie jedoch nur aufgerufen, um einen Wert vom Typ [string] zu überprüfen. Sie gibt ein Objekt mit den Eigenschaften [erreur, value] zurück, wobei:
    • [erreur] ein boolescher Wert ist, der angibt, ob ein Fehler vorliegt oder nicht;
    • [value] ist der Parameter [value] aus Zeile 32, der je nach Fall in eine Zahl oder ein Zahlenarray umgewandelt wird;

Die Klasse [BaseEntity], die möglicherweise ein Attribut namens [arrayOfAttributes] hatte, wird so geändert, dass dieses Attribut nicht mehr vorhanden ist: Es verunreinigt nämlich die Zeichenkette jSON mit [TaxAdminData]. Die Klasse wird wie folgt umgeschrieben:


<?php

namespace Application;

class BaseEntity {
  
  // Initialisierung aus einer Datei JSON
  public function setFromJsonFile(string $jsonFilename) {
    // Der Inhalt der Steuerdatendatei wird abgerufen
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // Fehler?
    if (!$fileContents) {
      // Der Fehler wird protokolliert
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // Der Code JSON wird aus der Konfigurationsdatei in ein assoziatives Array geladen
      $arrayOfAttributes = \json_decode($fileContents, true);
      // Fehler?
      if ($arrayOfAttributes === FALSE) {
        // Der Fehler wird vermerkt
        $erreur = TRUE;
        $message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // Fehler?
    if ($erreur) {
      // Es wird eine Ausnahme ausgelöst
      throw new ExceptionImpots($message);
    }
    // Initialisierung der Klassenattribute
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // Es wird überprüft, ob alle Attribute vorhanden sind
    $this->checkForAllAttributes($arrayOfAttributes);
    // Das Objekt wird zurückgegeben
    return $this;
  }

  public function checkForAllAttributes($arrayOfAttributes) {
    // Es wird überprüft, ob alle Schlüssel initialisiert wurden
    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) {
    // Es werden bestimmte Attribute der Klasse initialisiert (nicht unbedingt alle)
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // Das Objekt wird zurückgegeben
    return $this;
  }

  // toString
  public function __toString() {
    // Attribute des Objekts
    $arrayOfAttributes = \get_object_vars($this);
    // Zeichenkette jSON des Objekts
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }

}

Anmerkungen

  • Zeile 20: Das Attribut [$this→arrayOfAttributes] wurde in eine Variable umgewandelt, die nun an die Methode [checkForAllAttributes] in Zeile 38 übergeben werden muss, die zuvor auf das Attribut [$this→arrayOfAttributes] angewandt wurde;

Aufgrund dieser Änderung an [BaseEntity] muss auch die Klasse [Database] geringfügig angepasst werden:


<?php

namespace Application;

class Database extends BaseEntity {
  // Attribute
  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
  // Initialisierung
  public function setFromJsonFile(string $jsonFilename) {
    // übergeordnetes Objekt
    parent::setFromJsonFile($jsonFilename);
    // gibt das Objekt zurück
    return $this;
  }

  // Getter und Setter
  ...
}

Anmerkungen

  • Im ursprünglichen Code wurde nach Zeile 30 die Methode [parent::checkForAllAttributes] aufgerufen. Dies ist nun nicht mehr erforderlich, da dies nun automatisch von der Methode [parent::setFromJsonFile($jsonFilename)] übernommen wird;

14.3.4. Tests mit [Postman] auf dem Server

[Postman] wurde im Artikel unter dem Link vorgestellt.

Wir verwenden die folgenden Postman-Tests:

Image

Image

Image

Das Ergebnis jSON dieser letzten Anfrage lautet wie folgt:

Image

  • In [5-8] ist zu erkennen, dass die Attribute der Zeichenkette jSON tatsächlich numerische Werte (und keine Zeichenketten) enthalten. Dieses Ergebnis ermöglicht es der JavaScript-Klasse [Métier], normal ausgeführt zu werden;

14.3.5. Das Hauptskript [main]

Image

Das Hauptskript [main] des JavaScript-Clients lautet wie folgt:


// Importe
import axios from 'axios';

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

// asynchrone Funktion [main]
async function main() {
  // Axios-Konfiguration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // Instanziierung der Schicht [dao]
  const dao = new Dao(axios);
  // Anfragen HTTP
  let taxAdminData;
  try {
    // Sitzung initialisieren
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // Authentifizierung
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // Steuerdaten
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response.réponse;
  } catch (error) {
    // Fehler wird protokolliert
    console.log("erreur=", error.message);
    // Ende
    return;
  }

  // Instanziierung der Schicht [métier]
  const métier = new Métier(taxAdminData);

  // Steuerberechnungen
  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));
  // Liste der Simulationen
  log("-----------liste-des-simulations");
  log(simulations);
  // Löschen einer Simulation
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

// Ausführung
main();

Kommentare

  • Zeilen 5–6: Einbindung der Klassen [Dao] und [Métier];
  • Zeile 9: Die asynchrone Funktion [main], die mithilfe der Klasse [Dao] die Kommunikation mit dem Server organisiert und die Klasse [Métier] mit der Durchführung der Steuerberechnungen beauftragt;
  • Zeilen 10–36: Das Skript ruft nacheinander und blockierend die Methoden [initSession, authentifierUtilisateur, getAdminData] der Schicht [dao] auf;
  • Zeile 38: Die Ebene [dao] wird nicht mehr benötigt. Es liegen nun alle Elemente vor, um die Ebene [métier] des JavaScript-Clients auszuführen;
  • Zeilen 41–46: Es werden drei Steuerberechnungen durchgeführt, deren Ergebnisse in einem Array [simulations] zusammengefasst werden;
  • Zeile 49: Das Simulationsarray wird angezeigt;
  • Zeile 52: Eine davon wird gelöscht;

Die Ergebnisse der Ausführung des Hauptskripts lauten wie folgt:


[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

Image

In diesem Abschnitt stellen wir die Anwendung [Client HTTP 2] gemäß der folgenden Architektur in einem Browser bereit:

Image

Die Portierung erfolgt nicht sofort. Während [node.js] in der Lage ist, JavaScript auszuführen (ES6), ist dies bei Browsern im Allgemeinen nicht der Fall. Daher müssen Tools verwendet werden, die den Code ES6 in einen Code ES5 übersetzen, der von aktuellen Browsern verstanden wird. Glücklicherweise sind diese Tools sowohl leistungsstark als auch relativ einfach zu bedienen.

Wir haben uns dabei an den Artikel [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal] gehalten.

Im Ordner „[client HTTP 3/src]“ haben wir die Elemente „[main.js, Métier.js, Dao2.js]“ der Anwendung „[Client Http 2]“ abgelegt, die wir gerade entwickelt haben.

14.4.1. Initialisierung des Projekts

Wir werden im Ordner „[client http 3]“ arbeiten. Wir öffnen ein Terminal in „[VSCode]“ und wechseln in diesen Ordner:

Image

Wir initialisieren dieses Projekt mit dem Befehl [npm init] und akzeptieren bei den gestellten Fragen die vorgeschlagenen Standardantworten:

Image

  • In [4-5] wird die Projektkonfigurationsdatei [package.json] erstellt, die auf der Grundlage der verschiedenen eingegebenen Antworten generiert wurde;

14.4.2. Installation der Projektabhängigkeiten

Wir werden die folgenden Abhängigkeiten installieren:

  • [@babel/core]: der Kern des Tools [Babel] [https://babeljs.io], das Code aus ES 2015+ in Code umwandelt, der auf neueren und älteren Browsern ausführbar ist;
  • [@babel/preset-env]: ist Teil des Babel-Toolsuits. Wird vor der Transpilation von ES6 → ES5 ausgeführt;
  • [babel-loader]: Diese Abhängigkeit ermöglicht es dem Tool [webpack], das Tool [Babel] aufzurufen;
  • [webpack]: Koordinator. Es ist [webpack], das Babel aufruft, um die Transpilation der Codes ES6 → ES5 durchzuführen, und anschließend alle resultierenden Dateien zu einer einzigen Datei zusammenführt;
  • [webpack-cli]: wird von [webpack] benötigt;
  • [@webpack-cli/init]: wird zur Konfiguration von [webpack] verwendet;
  • [webpack-dev-server]: Stellt einen Entwicklungswebserver bereit, der standardmäßig auf Port 8080 läuft. Bei Änderungen an den Quelldateien wird die Webanwendung automatisch neu geladen;

Die Projektabhängigkeiten werden in einem Terminal von [VSCode] wie folgt installiert:

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

Image

Nach der Installation der Abhängigkeiten hat sich die Datei [package.json] wie folgt verändert:


{
  "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"
  }
}
  • Zeilen 12–19: Die Projektabhängigkeiten sind [devDependencies]: Diese werden während der Entwicklungsphase benötigt, in der Produktionsphase jedoch nicht mehr. In der Produktion wird nämlich die Datei [dist/main.js] verwendet. Sie ist in ES5 programmiert und benötigt die Tools zur Transpilierung von ES6-Code in ES5-Code nicht mehr;

Wir müssen dem Projekt zwei Abhängigkeiten hinzufügen:

  • [core-js]: enthält „Polyfills“ für ECMAScript 2019. Ein Polyfill ermöglicht es, neueren Code wie ECMAScript 2019 (Sept. 2019) in älteren Browsern auszuführen;
  • [regenerator-runtime]: laut der Website der Bibliothek --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];

Diese beiden Abhängigkeiten ersetzen ab Babel 7 die Abhängigkeit [@babel/polyfill], die zuvor diese Rolle spielte und nun (Sept. 2019) veraltet ist. Sie werden wie folgt installiert:

Image

Die Datei [package.json] entwickelt sich dann wie folgt:


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

Die Verwendung der Abhängigkeiten [core-js, regenerator-runtime] erfordert, dass die folgenden [imports] (Zeilen 3–4) in das Hauptskript [src/main.js] eingefügt werden:


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

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

14.4.3. Konfiguration von [webpack]

[webpack] ist das Tool, das Folgendes steuert:

  • die Transpilation von ES6 → ES5 aller JavaScript-Dateien des Projekts;
  • die Zusammenführung der generierten Dateien zu einer einzigen Datei;

Dieses Tool wird über eine Konfigurationsdatei [webpack.config.js] gesteuert, die mithilfe einer Abhängigkeit namens [@webpack-cli/init] (Sept. 2019) generiert werden kann. Diese wurde zusammen mit den anderen im Abschnitt „Link“ installiert.

Wir führen den Befehl [npx webpack-cli init] in einem Terminal [VSCode] aus:

Image

Nachdem wir die verschiedenen Fragen beantwortet haben (wobei wir die meisten der vorgeschlagenen Standardantworten übernehmen können), wird im Stammverzeichnis des Projekts [4] eine Datei namens [webpack.config.js] generiert:

Die Datei [webpack.config.js] sieht wie folgt aus:


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

Ich verstehe nicht alle Details dieser Datei, aber einige Punkte fallen auf:

  • Zeile 1: Die Datei enthält keinen Code für ES6. [Eslint] meldet daher Fehler, die bis zum Stammverzeichnis des Projekts [javascript] zurückreichen. Das ist störend. Um zu verhindern, dass ESLint eine Datei analysiert, reicht es aus, Zeile 1 auszukommentieren;
  • Zeile 31: Wir arbeiten im Modus [développement];
  • Zeile 32: Das Eingabeskript ist hier [src/index.js]. Wir werden dies noch ändern müssen;
  • Zeile 36: Der Ordner, in dem die Ergebnisse von [webpack] gespeichert werden, ist der Ordner [dist];
  • Zeile 46: Wir sehen, dass [webpack] [babel-loader] verwendet, eine der Abhängigkeiten, die wir installiert haben;
  • Zeile 54: Hier ist zu sehen, dass [webpack] [@babel-preset/env] verwendet, eine der Abhängigkeiten, die wir installiert haben;

Die Initialisierung von [webpack] hat die Datei [package.json] geändert (es wird um Berechtigung gebeten):


{
  "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"
  }
}
  • Zeile 4: Sie wurde geändert;
  • Zeilen 8–9, 18–19: Diese wurden hinzugefügt;
  • Zeile 8: die Aufgabe [npm], mit der das Projekt kompiliert werden kann;
  • Zeile 9: die Aufgabe [npm], mit der das Projekt ausgeführt wird;
  • Zeile 18: ?
  • Zeile 19: Ermöglicht die Erstellung einer Datei „[dist/index.html]“, in die das von „[webpack]“ generierte Skript „[dist/main.js]“ automatisch eingebettet wird; dieses Skript wird bei der Ausführung des Projekts verwendet;

Schließlich hat die Konfiguration von [webpack] eine Datei [src/index.js] generiert:

Image

Der Inhalt von [index.js] lautet wie folgt (Sept. 2019):


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

14.4.4. Kompilierung und Ausführung des Projekts

Die Datei „[package.json]“ enthält drei Aufgaben „[npm]“:


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

Diese Aufgaben werden von [VSCode] umfasst, das sie zur Ausführung bereitstellt:

Image

  • In [1-3] wird das Projekt kompiliert;
  • in [4]: Das Projekt wird in [dist/main.hash.js] kompiliert und eine Seite [dist/index.html] wird erstellt;

Die generierte Seite [index.html] sieht wie folgt aus:


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

Diese Seite dient also lediglich dazu, die von [webpack] generierte Datei [main.hash.js] zu kapseln.

Das Projekt wird von der Aufgabe [start] ausgeführt:

Image

Die Seite [dist/index.html] wird dann auf einen Server geladen, der zur Suite [webpack] gehört, auf Port 8080 des lokalen Rechners läuft und vom Standardbrowser des Rechners angezeigt wird:

Image

  • in [2], dem Dienstport des Webservers von [webpack];
  • in [3] ist der Seiteninhalt von [dist/index.html] leer;
  • in [4]: die Registerkarte [console] der Entwicklertools des Browsers, hier Firefox (F12);
  • in [5] das Ergebnis der Ausführung der Datei [src/index.js]. Zur Erinnerung: Der Inhalt dieser Datei lautete wie folgt:
console.log("Hello World from your main file!");

Ändern wir nun diesen Inhalt in die folgende Zeile:

console.log("Bonjour le monde");

Automatisch (ohne Neukompilierung) werden neue Dateien „[main.js, index.html]“ generiert und die neue Datei „[index.html]“ im Browser geladen:

Image

Es ist nicht erforderlich, die Aufgabe [build] vor der Aufgabe [start] auszuführen: Letztere kompiliert zunächst das Projekt. Sie speichert die Ergebnisse dieser Kompilierung nicht im Ordner [dist]. Um dies festzustellen, genügt es, diesen Ordner zu löschen. Man wird dann feststellen, dass die Aufgabe [start] das Projekt kompiliert und ausführt, ohne den Ordner [dist] zu erstellen. Sie scheint ihre Ergebnisse [index.html, main.hash.js] in einem eigenen Ordner von [webpackdev-server] zu speichern. Dieses Verhalten reicht für unsere Tests aus.

Wenn der Entwicklungsserver gestartet ist, löst jede gespeicherte Änderung an einer der Projektdateien eine Neukompilierung aus. Aus diesem Grund deaktivieren wir den Modus [Auto Save] von [VSCode]. Wir möchten nämlich nicht, dass eine Neukompilierung erfolgt, sobald Zeichen in eine der Projektdateien eingegeben werden. Eine Neukompilierung soll erst dann erfolgen, wenn die Änderungen gespeichert werden:

Image

  • in [2] darf die Option [Auto Save] nicht aktiviert sein;

14.4.5. Tests des JavaScript-Clients des Steuerberechnungsservers

Um den JavaScript-Client des Steuerberechnungsservers zu testen, müssen [main.js] und [1] in der Datei [webpack.config.js] und [2-3] als Einstiegspunkt des Projekts festgelegt werden:

Image

Beachten Sie, dass das Skript [main.js] im Vergleich zur Version in [Client http 2] zwei zusätzliche Importe enthalten muss:

Image

Außerdem haben wir den Code leicht angepasst, um Fehler zu behandeln, die der Server möglicherweise zurückgibt:


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

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

// asynchrone Funktion [main]
async function main() {
  // Axios-Konfiguration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // Instanziierung der Schicht [dao]
  const dao = new Dao(axios);
  // Anfragen HTTP
  let taxAdminData;
  try {
    // Sitzung initialisieren
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    if (response.état != 700) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // Authentifizierung
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    if (response.état != 200) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // Steuerdaten
    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) {
    // Fehler wird protokolliert
    console.log("erreur=", error.message);
    // Ende
    return;
  }

  // Instanziierung der Schicht [métier]
  const métier = new Métier(taxAdminData);

  // Steuerberechnungen
  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));
  // Liste der Simulationen
  log("-----------liste-des-simulations");
  log(simulations);
  // Löschen einer Simulation
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

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

// Ausführung
main();

Kommentare

  • In den Zeilen [24-26], [31-33] und [38-40] wird der Code [response.état] überprüft, der in der Antwort jSON vom Server gesendet wurde. Wenn dieser Code einen Fehler anzeigt, wird eine Ausnahme ausgelöst, deren Fehlermeldung die Zeichenfolge jSON aus der Serverantwort [response.réponse] ist;

Anschließend führen wir das Projekt [5-6] aus.

Die Seite [index.html] wird daraufhin generiert und im Browser geladen:

Image

  • In [7] ist zu sehen, dass die Aktion [init-session] aufgrund eines Problems [CORS] (Cross-Origin Resource Sharing) nicht abgeschlossen werden konnte;

Das Problem CORS ist auf die Client-Server-Verbindung zurückzuführen:

  • Unser JavaScript-Client wurde auf den Rechner [http://localhost:8080] heruntergeladen;
  • Der Server für die Steuerberechnung läuft auf dem Rechner [http://localhost:80];
  • Client und Server befinden sich daher nicht in denselben Domänen (gleicher Rechner, aber nicht derselbe Port);
  • der Browser, der den von der Maschine [http://localhost:8080] geladenen JavaScript-Client ausführt, blockiert alle Anfragen, die nicht an [http://localhost:80] gerichtet sind. Dies ist eine Sicherheitsmaßnahme. Daher blockiert er auch die Anfrage des Clients an den Server, der auf dem Rechner [http://localhost:80] läuft;

Tatsächlich blockiert der Browser die Anfrage nicht vollständig. Er wartet vielmehr darauf, dass der Server ihm „mitteilt“, dass er domänenübergreifende Anfragen akzeptiert. Erhält er diese Genehmigung, leitet der Browser die domänenübergreifende Anfrage weiter.

Der Server erteilt seine Genehmigung, indem er bestimmte HTTP-Header sendet:

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
  • Zeile 1: Der JavaScript-Client arbeitet auf der Domain [http://localhost:8080]. Der Server muss ausdrücklich antworten, dass er diese Domain akzeptiert;
  • Zeile 2: Der JavaScript-Client wird in seinen Anfragen die Header HTTP und [Accept, Content-Type] verwenden:
    • [Accept]: Dieser Header wird in jeder Anfrage gesendet;
    • [Content-Type]: Dieser Header wird bei den Operationen POST verwendet, um den Typ der Parameter von POST anzugeben;

Der Server muss diese beiden Header explizit akzeptieren: HTTP;

  • Zeile 3: Der JavaScript-Client verwendet die Anfragen GET und POST. Der Server muss diese beiden Anfragetypen ausdrücklich akzeptieren;
  • Zeile 4: Der JavaScript-Client sendet Sitzungs-Cookies. Der Server akzeptiert diese mit dem Header aus Zeile 4;

Wir müssen also den Server anpassen. Dies tun wir in [Netbeans]. Das Problem mit CORS tritt ausschließlich im Entwicklungsmodus auf. In der Produktion arbeiten Client und Server in derselben Domäne [http://localhost:80], und es tritt kein Problem CORS auf. Wir benötigen daher eine Möglichkeit, die Anfragen CORS über die Serverkonfiguration zuzulassen oder zu verweigern.

Image

Die Änderungen am Server werden an drei Stellen vorgenommen:

  • [1, 4]: in der Konfigurationsdatei [config.json], um dort einen booleschen Wert einzufügen, der steuert, ob domänenübergreifende Anfragen akzeptiert werden oder nicht;
  • [2]: in der Klasse [ParentResponse], die die Antwort an den JavaScript-Client sendet. Diese Klasse sendet die vom Client-Browser erwarteten Header CORS;
  • [3]: in den Klassen [HtmlResponse, JsonResponse, XmlResponse], die die Antworten für die jeweiligen Sitzungen [html, json, xml] generieren. Diese Klassen müssen den in [4] gefundenen booleschen Wert [corsAllowed] an ihre übergeordnete Klasse [2] übergeben. Dies geschieht in [5] durch Übergabe des Bildarrays aus der Datei jSON an [2];

Die Klasse [ParentResponse] [2] entwickelt sich wie folgt:


<?php

namespace Application;

// Symfony-Abhängigkeiten
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ParentResponse {

  // int $statusCode: Der HTTP-Code für den Antwortstatus
  // Zeichenkette $content: der zu sendende Antworttext
  // je nach Fall handelt es sich um eine Zeichenkette JSON, XML oder HTML
  // Array $headers: die an die Antwort anzuhängenden Header HTTP

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

    // Vorbereitung der Textantwort des Servers
    $response = new Response();
    $response->setCharset("utf-8");
    // Statuscode
    $response->setStatusCode($statusCode);
    // Header für domänenübergreifende Anfragen
    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);
    }
    // Sonderfall der Methode [OPTIONS]
    // In diesem Fall sind nur die Header von Bedeutung
    $method = strtolower($request->getMethod());
    if ($method === "options") {
      $content = "";
      $response->setStatusCode(Response::HTTP_OK);
    }
    // Die Antwort wird gesendet
    $response->setContent($content);
    $response->send();
  }

}
  • Zeile 29: Es wird geprüft, ob domänenübergreifende Anfragen verarbeitet werden müssen. Falls ja, werden die Header HTTP und CORS (Zeilen 33–37) generiert, auch wenn die aktuelle Anfrage keine domänenübergreifende Anfrage ist. Im letzteren Fall sind die Header CORS überflüssig und werden vom Client nicht verarbeitet;
  • Zeile 30: Bei einer domänenübergreifenden Anfrage sendet der Client-Browser, der den Server abfragt, die Header HTTP und [Origin: http://localhost:8080] (im konkreten Fall unseres JavaScript-Clients). Zeile 30: Dieser Header HTTP wird in der Anfrage [$request] abgerufen;
  • Zeile 31: Es werden nur domänenübergreifende Anfragen akzeptiert, die von dem Rechner [http://localhost] stammen. Wir weisen darauf hin, dass diese Anfragen nur im Entwicklungsmodus des Projekts stattfinden;
  • Zeilen 32–36: Die Header CORS werden zu den bereits in der Tabelle [$headers] vorhandenen Headern hinzugefügt;
  • Zeilen 45–49: Die Art und Weise, wie der Client-Browser die Berechtigungen CORS anfordert, kann je nach verwendetem Client variieren. Manchmal fordert der Client-Browser diese Berechtigungen mit einem Befehl HTTP [OPTIONS] an. Dies ist eine Neuerung für unseren Server, der ursprünglich ausschließlich für die Bearbeitung von Befehlen vom Typ [GET, POST] konzipiert wurde. Im Falle eines Befehls [OPTIONS] generiert der Server derzeit eine Fehlermeldung. In den Zeilen 46–49 korrigieren wir dies in letzter Minute: Wenn wir in Zeile 46 feststellen, dass es sich bei dem aktuellen Befehl um einen Befehl [OPTIONS] handelt, generieren wir für den Client:
    • Zeilen 47, 51: eine leere Antwort [$content];
    • Zeile 48: einen Statuscode von 200, der angibt, dass der Befehl erfolgreich war. Das Einzige, was bei diesem Befehl wichtig ist, ist das Senden der Header CORS aus den Zeilen 33–36. Das erwartet der Client-Browser;

Nachdem der Server entsprechend korrigiert wurde, läuft das JavaScript-Client-Skript besser, zeigt jedoch einen neuen Fehler an:

Image

  • Bei [1] wird die Sitzung jSON korrekt initialisiert;
  • Bei [2] schlägt die Aktion [authentifier-utilisateur] fehl: Der Server meldet, dass keine aktive Sitzung vorliegt. Das bedeutet, dass der JavaScript-Client das Sitzungs-Cookie, das er bei der Aktion [init-session] gesendet hat, nicht korrekt an den Server zurückgesendet hat;

Sehen wir uns den stattgefundenen Netzwerkverkehr an:

Image

  • in [4] die Anfrage [init-session]. Diese verlief erfolgreich mit einem Statuscode 200 für die Antwort;
  • in [5], die Anfrage [authentifier-utilisateur]. Diese schlägt mit einem Statuscode 400 (Bad Request) [6] fehl;

Betrachtet man die Header HTTP und [7] der Anfrage [5], ist zu erkennen, dass der JavaScript-Client die Header HTTP und [Cookie] nicht gesendet hat, die es ihm ermöglicht hätten, das ursprünglich vom Server gesendete Sitzungs-Cookie zurückzusenden. Aus diesem Grund meldet der Server, dass keine Sitzung vorliegt.

Damit der Client das Sitzungs-Cookie sendet, muss dem Objekt [axios] eine Konfiguration hinzugefügt werden:


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

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

// asynchrone Funktion [main]
async function main() {
  // Axios-Konfiguration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  axios.defaults.withCredentials = true;
  // Instanziierung der Schicht [dao]
  const dao = new Dao(axios);
  // Anfragen HTTP
  let taxAdminData;
...

Zeile 15 sorgt dafür, dass die Cookies in die Header HTTP der Anfrage [axios] aufgenommen werden. Beachten Sie, dass dies in der Umgebung [node.js] nicht erforderlich war. Es gibt also Unterschiede im Code zwischen den beiden Umgebungen.

Sobald dieser Fehler behoben ist, läuft der JavaScript-Client normal ab:

Image

Image

14.5. Verbesserung des Clients HTTP 3

Wenn die vorherige Klasse [Dao2] in einem Browser ausgeführt wird, ist die Verwaltung des Sitzungs-Cookies überflüssig. Denn der Browser, der die Schicht [dao] hostet, verwaltet das Sitzungs-Cookie: Er sendet automatisch jedes Cookie zurück, das der Server ihm übermittelt. Daher kann die Klasse [Dao2] in die folgende Klasse [Dao3] umgeschrieben werden:


"use strict";

// Importe
import qs from "qs";

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

  // Sitzung initialisieren
  async initSession() {
    // Abfrageoptionen HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // Parameter der Abfrage URL
      params: {
        action: "init-session",
        type: "json"
      }
    };
    // Ausführung der Abfrage HTTP
    return await this.getRemoteData(options);
  }

  async authentifierUtilisateur(user, password) {
    // Optionen der Abfrage HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        "Content-type": "application/x-www-form-urlencoded"
      },
      // Hauptteil von POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // Parameter des URL
      params: {
        action: "authentifier-utilisateur"
      }
    };
    // Ausführung der Abfrage HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // Optionen der Abfrage HHTP  [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // Parameter der Abfrage URL
      params: {
        action: "get-admindata"
      }
    };
    // Ausführung der Abfrage HTTP
    const data = await this.getRemoteData(options);
    // Ergebnis
    return data;
  }

  async getRemoteData(options) {
    // Ausführung der Abfrage HTTP
    let response;
    try {
      // asynchrone Abfrage
      response = await this.axios.request("main.php", options);
    } catch (error) {
      // Der Parameter [error] ist eine Ausnahmeinstanz – er kann verschiedene Formen annehmen
      if (error.response) {
        // Die Antwort des Servers befindet sich in [error.response]
        response = error.response;
      } else {
        // Der Fehler wird erneut ausgelöst
        throw error;
      }
    }
    // „response“ ist die gesamte Antwort HTTP des Servers (Header HTTP + die Antwort selbst)
    // Die Antwort des Servers befindet sich in [response.data]
    return response.data;
  }
}

// Export der Klasse
export default Dao3;

Alles, was mit der Verwaltung des Verwaltungs-Cookies zu tun hatte, ist verschwunden.

Wir ändern das vorherige Projekt wie folgt:

Image

Im Ordner [src] haben wir zwei Dateien hinzugefügt:

  • die Klasse [Dao3], die wir gerade vorgestellt haben;
  • die Datei [main3], die für den Start der neuen Version zuständig ist;

Die Datei „[main3]“ bleibt identisch mit der Datei „[main]“ aus der vorherigen Version, verwendet nun jedoch die Klasse „[Dao3]“:


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

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

// asynchrone Funktion [main]
async function main() {
  // Axios-Konfiguration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL =
    "http://localhost/php7/scripts-web/impots/version-14";
  axios.defaults.withCredentials = true;
  // Instanziierung der Schicht [dao]
  const dao = new Dao(axios);
  // Anfragen HTTP
  ...
}

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

// Ausführung
main();

Die Datei [webpack.config] wurde so geändert, dass sie nun das Skript [main3] ausführt:


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

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

Anschließend wird das Projekt ausgeführt, nachdem der Steuerberechnungsserver gestartet wurde:

Image

Die in der Browserkonsole angezeigten Ergebnisse sind identisch mit denen der vorherigen Version.

14.6. Conclusion

Wir verfügen nun über alle Werkzeuge, um den JavaScript-Code einer Webanwendung zu entwickeln. Wir können:

  • den aktuellsten Code ECMAScript verwenden;
  • einzelne Elemente dieses Codes in einer einfacheren Umgebung [node.js] zum Debuggen und Testen prüfen;
  • diesen Code anschließend mithilfe der Tools [babel] und [webpack] in einen Browser zu portieren;