Skip to content

18. Anwendungsübung – Version 8

Wir greifen die Beispielanwendung – Version 5 (Absatz „Link“) wieder auf und bauen daraus eine Client-Server-Anwendung auf.

18.1. Einleitung

Die Architektur von Version 5 sah wie folgt aus:

Image

  • Die Schicht mit der Bezeichnung [dao] (Data Access Objects) ist für den Datenaustausch mit der Datenbank MySQL und dem lokalen Dateisystem zuständig;
  • die Schicht mit der Bezeichnung [métier] führt die Steuerberechnung durch;
  • Das Hauptskript ist der „Dirigent“: Es instanziiert die Schichten [dao] und [métier] und kommuniziert anschließend mit der Schicht [métier], um die erforderlichen Aufgaben auszuführen;

Wir werden diese Architektur auf die folgende Client-Server-Architektur umstellen:

Image

  • In [2] werden wir die Schicht [dao] aus Version 5 übernehmen und dabei die Methoden für den Zugriff auf das lokale Dateisystem entfernen. Diese Methoden werden in die Schicht [dao] des Clients [6, 7] migriert;
  • in [3], die Schicht [métier] bleibt die der Version 5 ohne ihre Methoden [executeBatchImpôts, saveResults], die in die Schicht [dao] und [7] des Clients migriert werden;
  • In [4] muss das Serverskript geschrieben werden: Es muss:
    • die Schichten [métier], [dao] und [3, 2] erstellen;
    • mit dem Client-Skript [5, 7] zu kommunizieren;
  • In [7] muss die Client-Schicht [dao] geschrieben werden:
    • Sie wird ein Client HTTP des Server-Skripts [4, 5] sein;
    • sie übernimmt die Methoden für den Zugriff auf das lokale Dateisystem der Schicht [dao] aus Version 5;
  • In [8] wird die Client-Schicht [métier] die Schnittstelle [InterfaceMetier] der Version 5 einhalten. Ihre Implementierung wird jedoch anders sein. In Version 5 führte die Schicht [métier] die Steuerberechnung durch. Hier übernimmt die Serverschicht [métier] diese Berechnung. Die Schicht [métier] wird daher die Schichten [dao] und [7] aufrufen, um mit dem Server zu kommunizieren und ihn aufzufordern, die Steuer zu berechnen;
  • in [9] muss das Konsolenskript die Client-Schichten [dao, métier] instanziieren und deren Ausführung starten;

18.2. Der Server

Wir befassen uns mit dem Serverseitenteil der Anwendung.

Image

Diese Architektur wird durch die folgenden Skripte implementiert:

Image

18.2.1. Die zwischen den Schichten ausgetauschten Entitäten

Image

Die zwischen den Schichten ausgetauschten Entitäten entsprechen denen der Version 5, die im Abschnitt „Link“ beschrieben sind.

18.2.2. Die Schicht [dao]

Image

Die Schicht [dao] implementiert die folgende Schnittstelle [InterfaceServerDao]:


<?php

// Namensraum
namespace Application;

interface InterfaceServerDao {

  // Auslesen der Daten der Steuerbehörde
  public function getTaxAdminData(): TaxAdminData;
}
  • Zeile 9: Die Methode [getTaxAdminData] ruft die Daten der Steuerbehörde aus einer Datenbank ab;

Die Schnittstelle [InterfaceServerDao] wird durch die folgende Klasse [ServerDao] implementiert:


<?php

// Namensraum
namespace Application;

// Definition einer Klasse ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
  // das Objekt vom Typ TaxAdminData, das die Daten der Steuerklassen enthält
  private $taxAdminData;
  // das Objekt vom Typ [Database], das die Merkmale von BD enthält
  private $database;

  // Hersteller
  public function __construct(string $databaseFilename) {
    // die Konfiguration JSON wird in der Datenbank gespeichert
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // Das Attribut wird vorbereitet
    $this->taxAdminData = new TaxAdminData();
    try {
      // Die Verbindung zur Datenbank wird geöffnet
      $connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
      // Es soll bei jedem Fehler von SGBD eine Ausnahme ausgelöst werden
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // Eine Transaktion wird gestartet
      $connexion->beginTransaction();
      // Die Tabelle der Steuerklassen wird gefüllt
      $this->getTranches($connexion);
      // Die Konstantentabelle wird gefüllt
      $this->getConstantes($connexion);
      // Die Transaktion wird bei Erfolg beendet
      $connexion->commit();
    } catch (\PDOException $ex) {
      // Ist eine Transaktion im Gange?
      if (isset($connexion) && $connexion->inTransaction()) {
        // Die Transaktion wird bei einem Fehler beendet
        $connexion->rollBack();
      }
      // Die Ausnahme wird an den aufrufenden Code weitergeleitet
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // Die Verbindung wird geschlossen
      $connexion = NULL;
    }
  }

  // Daten aus der Datenbank lesen
  private function getTranches($connexion): void {

  }

  // Auslesen der Konstantentabelle
  private function getConstantes($connexion): void {

  }

  // gibt die Daten zurück, die zur Berechnung der Steuer benötigt werden
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }

}

Dieser Code wurde im Abschnitt „Link“ vorgestellt.

18.2.3. Die Schicht [métier]

Image

Image

Die Schicht [métier] implementiert die folgende Schnittstelle [InterfaceServerMetier]:


<?php

// Namensraum
namespace Application;

interface InterfaceServerMetier {

  // Berechnung der Steuern eines Steuerpflichtigen
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}

Die Schnittstelle [InterfaceServerMetier] wird durch die folgende Klasse [ServerMetier] implementiert:


<?php

// Namensraum
namespace Application;

class ServerMetier implements InterfaceServerMetier {
  // Dao-Ebene
  private $dao;
  // Daten der Steuerverwaltung
  private $taxAdminData;

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

  public function __construct(InterfaceServerDao $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
    // Die Ausnahme wird dann an den aufrufenden Code weitergeleitet
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// Steuerberechnung
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // 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 {

    // 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 {

    // Ergebnis
    return floor($revenuImposable);
  }

// berechnet einen eventuellen Abschlag
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // Ergebnis
    return ceil($décôte);
  }

// berechnet einen eventuellen Abschlag
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    ..
    // Ergebnis
    return ceil($réduction);
  }
}

Dieser Code wurde bereits in Version 1 im Abschnitt „Link“ vorgestellt und erläutert. Die objektorientierte Version mit einer Datenbank wurde im Abschnitt „Link“ vorgestellt.

18.2.4. Das Server-Skript

Image

Image

Das Serverskript implementiert die Schicht [web] [4]. Das Skript [impots-server] wird durch die folgende Datei jSON [config-server.json] konfiguriert:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
    "databaseFilename": "Data/database.json",
    "taxAdminDataFileName": "Data/taxadmindata.json",
    "relativeDependencies": [
        "/Entities/BaseEntity.php",
        "/Entities/ExceptionImpots.php",
        "/Entities/TaxAdminData.php",
        "/Entities/Database.php",
        "/Dao/InterfaceServerDao.php",
        "/Dao/ServerDao.php",
        "/Métier/InterfaceServerMetier.php",
        "/Métier/ServerMetier.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ]
}
  • Zeile 1: das Stammverzeichnis, von dem aus die Dateipfade gemessen werden;
  • Zeile 2: die Konfigurationsdatei jSON für die Datenbank MySQL;
  • Zeile 3: die Datei „jSON“ mit den Daten der Steuerbehörde;
  • Zeilen 5–14: die Anwendungsdateien;
  • Zeile 15: die erforderliche Abhängigkeit von Bibliotheken von Drittanbietern, in diesem Fall Symfony;
  • Zeilen 16–20: die Tabelle der Benutzer, die zur Nutzung der Anwendung berechtigt sind;

Die Dateien „jSON“ und „[database.json, taxadmindata.json]“ stammen aus der im Abschnitt „Link“ beschriebenen Version 5.

Das Skript [impots-server] implementiert die Schicht [web] wie folgt:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);

// Namensraum
namespace Application;

// Fehlerbehandlung durch PHP
//ini_set("display_errors", "0");
//
// Pfad zur Konfigurationsdatei
define("CONFIG_FILENAME", "Data/config-server.json");

// die Konfiguration wird abgerufen
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// die für das Skript erforderlichen Abhängigkeiten werden eingebunden
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// Absolute Abhängigkeiten (Bibliotheken von Drittanbietern)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// Definition der Konstanten
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// Symfony-Abhängigkeiten
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;

// Vorbereitung der Antwort JSON vom Server
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// die aktuelle Anfrage wird abgerufen
$request = Request::createFromGlobals();
// Authentifizierung
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// Existiert der Benutzer?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
  $trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
  $i++;
}
// Festlegen des Statuscodes der Antwort
if (!$trouvé) {
  // Nicht gefunden – Code 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
  // Fehlermeldung
  $response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
  $response->send();
  // Ende
  exit;
}
// Es liegt ein gültiger Benutzer vor – die empfangenen Parameter werden überprüft
$erreurs = [];
// Es müssen drei Parameter vorhanden sein: GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// Fehler?
if ($erreur) {
  $erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}

// Der Familienstand wird abgerufen
if (!$request->query->has("marié")) {
  $erreurs[] = "paramètre marié manquant";
} else {
  $marié = trim(strtolower($request->query->get("marié")));
  $erreur = $marié !== "oui" && $marié !== "non";
  // Fehler?
  if ($erreur) {
    $erreurs[] = "paramètre marié [$marié] invalide";
  }
}

// Die Anzahl der Kinder wird abgerufen
if (!$request->query->has("enfants")) {
  $erreurs[] = "paramètre enfants manquant";
} else {
  $enfants = trim($request->query->get("enfants"));
  // Die Anzahl der Kinder muss eine ganze Zahl >= 0 sein
  $erreur = !preg_match("/^\d+$/", $enfants);
  // Fehler?
  if ($erreur) {
    $erreurs[] = "paramètre enfants [$enfants] invalide";
  }
}

// Das Jahresgehalt wird abgerufen
if (!$request->query->has("salaire")) {
  $erreurs[] = "paramètre salaire manquant";
} else {
  // Das Gehalt muss eine ganze Zahl >= 0 sein
  $salaire = trim($request->query->get("salaire"));
  $erreur = !preg_match("/^\d+$/", $salaire);
  // Fehler?
  if ($erreur) {
    $erreurs[] = "paramètre salaire [$salaire] invalide";
  }
}

// Weitere Parameter in der Abfrage?
foreach (\array_keys($request->query->all()) as $key) {
  // Gültiger Parameter?
  if (!\in_array($key, ["marié", "enfants", "salaire"])) {
    $erreurs[] = "paramètre [$key] invalide";}
}

// Fehler?
if ($erreurs) {
  // Es wird ein Fehlercode 400 an den Client gesendet
  $response->setStatusCode(Response::HTTP_BAD_REQUEST);
  $response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// Es ist alles vorhanden, was zum Arbeiten benötigt wird
// Erstellung der Serverarchitektur
$msgErreur = "";
try {
  // Erstellung der Schicht [dao]
  $dao = new ServerDao($config["databaseFilename"]);
  // Erstellung der Schicht [métier]
  $métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// Fehler wird vermerkt
  $msgErreur = utf8_encode($ex->getMessage());
}
// Fehler?
if ($msgErreur) {
  // Es wird ein Fehlercode 500 an den Client gesendet
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  $response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// Steuerberechnung
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// Die Antwort wird zurückgegeben
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();

Kommentare

  • Zeile 16: Die Konfigurationsdatei wird ausgewertet;
  • Zeilen 18–26: Alle Abhängigkeiten werden geladen;
  • Zeile 29: Der Name der Datei „[database.json]“;
  • Zeilen 32–33: Die Klassen der zu verwendenden Drittanbieter-Bibliotheken werden deklariert;
  • Zeilen 36–38: Eine Antwort „jSON“ wird vorbereitet;
  • Zeilen 40–52: Es wird überprüft, ob der Benutzer, der die Anfrage stellt, tatsächlich zu den autorisierten Benutzern gehört;
  • Zeilen 54–63: Ist dies nicht der Fall, wird der Code HTTP 401 gesendet, der eine Zugriffsverweigerung anzeigt. Nach Erhalt dieses Codes und des Headers HTTP [WWW-Authenticate => Basic realm=] zeigen die meisten Browser ein Authentifizierungsfenster an, in dem der Benutzer aufgefordert wird, sich zu authentifizieren;
  • Zeile 59: Die Antwort „jSON“ des Servers erläutert die Ursache des Fehlers. Alle Antworten des Servers bestehen aus der Zeichenfolge „jSON“ aus einer Tabelle „[‘réponse’=>’qq chose’]“;
  • Zeilen 64–117: Die Gültigkeit der Anfrage wird überprüft:
    • eine Anfrage GET mit genau drei Parametern;
    • ein Parameter [marié], dessen Wert „ja“ oder „nein“ sein muss;
    • ein Parameter [enfants], dessen Wert eine ganze Zahl >= 0 sein muss;
    • ein Parameter [salaire], dessen Wert eine ganze Zahl >= 0 sein muss;
  • Zeile 65: Jedes Mal, wenn ein Fehler erkannt wird, wird eine Fehlermeldung zum Array [$erreurs] hinzugefügt;
  • Zeilen 120–126: Liegt ein Fehler vor, wird der Code HTTP [400 Bad Request] an den Client gesendet (Zeile 122);
  • Zeile 123: Die Antwort jSON des Servers erläutert die Ursache des Fehlers;
  • Ab Zeile 132 wurde alles überprüft. Die Schichten [dao, métier] können instanziiert werden. Diese Instanziierung ist mit Kosten verbunden und sollte nur durchgeführt werden, wenn man sicher ist, dass es sich um eine gültige Anfrage handelt;
  • Zeilen 130–138: Die Serverarchitektur wird erstellt. Beim Aufbau der Schicht [dao] kann eine Ausnahme vom Typ [ExceptionImpots] ausgelöst werden. Tritt diese Ausnahme auf, wird der Fehler protokolliert;
  • Zeilen 135–138: Wenn eine Ausnahme aufgetreten ist, wird der Code HTTP 500 an den Client gesendet. Dieser Code bedeutet, dass der Server einen Fehler hatte;
  • Zeile 143: Die Antwort erläutert die Ursache des Fehlers;
  • Zeile 148 : Die Steuerberechnung wird an die Ebene [métier] delegiert;
  • Zeilen 150–151: Versand der Antwort;

Testen wir dieses Skript mit einem Browser. Fordern wir die sichere Seite URL über [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000] an:

Image

  • in [1], die angeforderte gesicherte URL;
  • in [2], die drei Parameter [marié, enfants, salaire];
  • In [3] hat der Apache-Server von Laragon ein selbstsigniertes Zertifikat SSL gesendet. Der Browser hat dies erkannt und zeigt eine Sicherheitswarnung an: Er stuft die Website des Servers als nicht vertrauenswürdig ein;
  • in [4] geht es weiter;

Image

  • in [6] geht es weiter;

Image

  • In [7] zeigt der Browser ein Fenster an, in dem sich der Benutzer authentifizieren kann;
  • in [9,10] geben wir [admin] und [admin] ein;

Image

  • bei [13] die Antwort jSON vom Server;

Führen wir einige Fehlertests durch:

Wir fragen nach URL und [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]

Wir erhalten folgendes Ergebnis:

Image

Wir unterbrechen die Abfrage von SGBD und MySQL und fordern URL und [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000] an:

Image

18.2.5. Tests [Codeception]

Jedes Mal, wenn wir eine neue Version des Servers erstellen, werden wir die Schichten [métier] und [dao] testen, wie es seit Version 04 der Fall ist (siehe Absätze Link und Link).

Zunächst verknüpfen wir das Projekt [scripts-web] mit den Tests [Codeception]. Befolgen Sie dazu die gleiche Vorgehensweise wie für das Projekt [scripts-console] im Abschnitt „Link“. Wir erhalten ein Projekt [scripts-web] mit einem Ordner [Test Files]:

Image

Wir erstellen einen Test für die Ebene [dao] und einen für die Ebene [métier].

18.2.5.1. Tests der Ebene [dao]

Image

Der Test [ServerDaoTest] sieht wie folgt aus:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);

// Namensraum
namespace Application;

// Definition der Konstanten
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// Pfad zur Konfigurationsdatei
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");

// Die Konfiguration wird abgerufen
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// Einbinden der für das Skript erforderlichen Abhängigkeiten
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// Absolute Abhängigkeiten (Bibliotheken von Drittanbietern)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// Test -----------------------------------------------------

class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    // übergeordnet
    parent::__construct();
    // Konfiguration wird abgerufen
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // Erstellung der Ebene [dao]
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // Tests
  public function testTaxAdminData() {

  }

}

Anmerkungen

  • Zeilen 9–24: Es wird dieselbe Arbeitsumgebung wie beim Server [impots-server.php] aufgebaut. Dies geschieht in den Zeilen 9–12 durch die Definition der beiden Konstanten, von denen die Umgebung abhängt;
  • Zeilen 32–40: Es wird eine Instanz der zu testenden Schicht [dao] erstellt, wie es bereits im Serverskript [impots-server.php] geschehen ist;
  • Ab diesem Zeitpunkt herrschen dieselben Bedingungen wie im Server-Skript [impots-server.php]: Die Tests können gestartet werden;
  • Zeilen 43–45: Die Methode [testTaxAdminData] ist die im Abschnitt „Link“ beschriebene;

Die Testergebnisse lauten wie folgt:

Image

18.2.5.2. Tests der Schicht [métier]

Image

Der Test [ServerMetierTest] wird wie folgt ablaufen:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);

// Namensraum
namespace Application;

// Definition der Konstanten
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// Pfad zur Konfigurationsdatei
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// Die Konfiguration wird abgerufen
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// Einbinden der für das Skript erforderlichen Abhängigkeiten
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// Absolute Abhängigkeiten (Bibliotheken von Drittanbietern)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// Testklasse
class ServerMetierTest extends \Codeception\Test\Unit {
  // Geschäftslogikschicht
  private $métier;

  public function __construct() {
    parent::__construct();
    // Konfiguration wird abgerufen
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // Erstellung der Schicht [dao]
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    // Erstellung der Schicht [métier]
    $this->métier = new ServerMetier($dao);
  }

  // Tests
  public function test1() {

  }

  public function test2() {

  }

  ..

  public function test11() {

  }

}

Anmerkungen

  • Zeilen 9–24: Es wird dieselbe Arbeitsumgebung wie die des Servers [impots-server.php] aufgebaut. Dies geschieht in den Zeilen 9–12 durch die Definition der beiden Konstanten, von denen die Umgebung abhängt;
  • Zeilen 30–38: Es wird eine Instanz der zu testenden Schicht [métier] erstellt, wie es bereits im Serverskript [impots-server.php] geschehen ist;
  • Ab diesem Zeitpunkt herrschen dieselben Bedingungen wie im Server-Skript [impots-server.php]: Die Tests können gestartet werden;
  • Zeilen 40–53: Die Methoden von [test1, test2…, test11] sind diejenigen, die im Abschnitt „Link“ beschrieben sind;

Die Testergebnisse lauten wie folgt:

Image

18.3. Der Client

Wir befassen uns mit dem clientseitigen Teil der Anwendung.

Image

Diese Architektur wird durch die folgenden Skripte implementiert:

Image

18.3.1. Die zwischen den Ebenen ausgetauschten Entitäten

Image

Die oben genannten Entitäten wurden alle beschrieben und bereits verwendet:

18.3.2. Die Ebene [dao]

Image

Die Schicht [dao] implementiert die folgende Schnittstelle [InterfaceClientDao]:


<?php

// Namensraum
namespace Application;

interface InterfaceClientDao {

  // Auslesen der Steuerpflichtigen-Daten
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // Berechnung der Steuern eines Steuerpflichtigen
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // Ergebnisse speichern
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
  • Zeile 9: Die Funktion [getTaxPayersData] lädt die Daten der Steuerzahler aus der Datei [$taxPayersFilename] in den Speicher. Sollten Fehler auftreten, werden diese in der Datei [$errorsFilename] protokolliert;
  • Zeile 12: Die Funktion [calculerImpots] berechnet die Steuer eines Steuerpflichtigen;
  • Zeile 15: Die Funktion [saveResults] speichert die Daten der Tabelle [$taxPayersData], die die Ergebnisse mehrerer Steuerberechnungen enthalten, in der Datei [$resultsFilename];

Die Schnittstelle [InterfaceClientDao] wird durch die folgende Klasse [ClientDao] implementiert:


<?php

namespace Application;

// Abhängigkeiten
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // Verwendung eines Traits
  use TraitDao;
  // Attribute
  private $urlServer;
  private $user;

  // Konstruktor
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }

  // Steuerberechnung
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // Ein Kunde wird angelegt HTTP
    $httpClient = HttpClient::create([
        'auth_basic' => [$this->user["login"], $this->user["passwd"]],
        "verify_peer" => false
    ]);
    // Anfrage an den Server
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
    ]]);
    // die Antwort wird abgerufen
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // Protokolle
    // print "$json=json\n";
    // Der Status der Antwort wird abgerufen
    $statusCode = $response->getStatusCode();
    // Fehler?
    if ($statusCode !== 200) {
      // Es liegt ein Fehler vor – es wird eine Ausnahme ausgelöst
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    // Die Antwort wird zurückgegeben
    return $réponse;
  }

}

Anmerkungen

  • Zeile 10: Es wird [TraitDao] eingefügt (siehe Abschnitt „Link“), das die Methoden [getTaxPayersData] und [saveResults] implementiert. Es bleibt also nur noch die Methode [calculerImpots] zu implementieren. Diese ist in den Zeilen 22–49 implementiert;
  • Zeilen 16–19: Der Konstruktor der Klasse [ClientDao] erhält zwei Parameter:
    • den URL [$urlServer] des Steuerberechnungsservers;
    • das Array [$user] mit den Schlüsseln „login“ und „passwd“, das den Benutzer definiert, der die Anfrage stellt;
  • Zeile 22: Die Methode [calculerImpots] empfängt die drei Parameter, die an den Steuerberechnungsserver gesendet werden sollen;
  • Zeilen 24–27: Es wird ein Client HTTP erstellt mit:
    • Zeile 25: die Anmeldedaten des Benutzers, der die Anfrage stellt;
    • Zeile 26: die Option, die bewirkt, dass der Client HTTP die Gültigkeit des vom Server gesendeten Zertifikats SSL nicht überprüft;
  • Zeilen 29–34: Der Server wird mit den drei erwarteten Parametern abgefragt;
  • Zeile 36: Die Antwort jSON vom Server wird abgerufen. Wenn der Parameter [false] nicht an die Methode [Response::getContent] übergeben wird, löst das Objekt [Response] eine Ausnahme aus, sobald versucht wird, den Inhalt der Antwort [Response::getContent] abzurufen, sofern der Status der Serverantwort im Bereich [3xx-5xx] liegt (Fehlerfall), löst das Objekt [Response] eine Ausnahme aus, sobald versucht wird, den Inhalt der Antwort [Response::getContent] oder deren Header HTTP und [Response::getHeaders] abzurufen. Unabhängig vom Status HTTP der Antwort soll hier Zugriff auf deren Inhalt möglich sein, und sei es nur, um ihn zu protokollieren (Zeile 40);
  • Zeilen 37–38: Die Antwort des Servers ist die Zeichenkette jSON aus einem Array [‘réponse’=>qqChose]. Wir extrahieren den Wert [qqChose];
  • Zeile 40: Die Antwort „jSON“ wird im Entwicklungsmodus protokolliert;
  • Zeile 42: Der Statuscode der Antwort wird abgerufen;
  • Zeilen 44–49: Ist der Statuscode HTTP nicht 200, ist auf unserem Server ein Problem aufgetreten. In diesem Fall wird eine Ausnahme vom Typ [ExceptionImpots] ausgelöst, deren Meldung die Antwort jSON des Servers ist, ergänzt um den Code HTTP der Antwort;
  • Zeile 51: Es wird das Ergebnis zurückgegeben, bei dem es sich um ein assoziatives Array mit den Schlüsseln [impôt, surcôte, décôte, réduction, taux] handelt;

18.3.3. Die Ebene [métier]

Image

Image

Die Schicht [métier] [8] implementiert die folgende Schnittstelle [InterfaceClientMetier]:


<?php

// Namensraum
namespace Application;

interface InterfaceClientMetier {

  // Steuerberechnung für einen Steuerpflichtigen
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // Steuerberechnung im Batch-Modus
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • Zeile 9: Die Funktion [calculerImpots] berechnet die Steuer;
  • Zeile 12: Die Funktion [executeBatchImpots] berechnet die Steuer für die Steuerzahler, deren Daten in der Datei [$taxPayersFileName] enthalten sind, speichert die Ergebnisse in der Datei [$resultsFileName] und die aufgetretenen Fehler in der Datei [$errorsFileName];

Die Schnittstelle [InterfaceClientMetier] wird durch die folgende Klasse [ClientMetier] implementiert:


<?php

// Namensraum
namespace Application;

class ClientMetier implements InterfaceClientMetier {
  // Attribut
  private $clientDao;

  // Konstruktor
  public function __construct(InterfaceClientDao $clientDao) {
    // Die Referenz wird auf der Ebene [dao] gespeichert
    $this->clientDao = $clientDao;
  }
  
  // Steuerberechnung
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }

  // Steuerberechnung im Batch-Modus
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // Ausnahmen, die aus der Ebene [dao] stammen, werden weitergeleitet
    // Steuerpflichtige Daten werden abgerufen
    $taxPayersData = $this->clientDao->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());
      // Ergänzung von [$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // das Ergebnis wird in die Ergebnistabelle eingetragen
      $results [] = $taxPayerData;
    }
    // Speichern der Ergebnisse
    $this->clientDao->saveResults($resultsFileName, $results);
  }

}

Anmerkungen

  • Zeilen 11–14: Der Konstruktor der Klasse [ClientMetier] erhält als Parameter eine Referenz auf die Schicht [dao];
  • Zeilen 17–19: Die Steuerberechnung wird an die Schicht [dao] delegiert;
  • Zeilen 20–38: Die Funktion [executeBatchImpots] wurde im Abschnitt „Link“ beschrieben;

18.3.4. Das Hauptskript

Image

Image

Das Client-Skript [MainImpotsClient.php] implementiert die Schichten [console] und [9]. Es wird durch die folgende Datei jSON und [conf-client.json] konfiguriert:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "Entities/BaseEntity.php",
        "Entities/TaxPayerData.php",
        "Entities/ExceptionImpots.php",
        "Utilities/Utilitaires.php",
        "Dao/InterfaceClientDao.php",
        "Dao/TraitDao.php",
        "Dao/ClientDao.php",
        "Métier/InterfaceClientMetier.php",
        "Métier/ClientMetier.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php"
    ],
    "user": {
        "login": "admin",
        "passwd": "admin"
    },
    "urlServer": "https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php"
}
  • Zeile 1: das Stammverzeichnis des Kunden;
  • Zeile 2: die Datei jSON mit den Steuerzahlerdaten;
  • Zeile 3: die Datei „jSON“ mit den Ergebnissen;
  • Zeile 4: die Datei jSON mit den Fehlern;
  • Zeilen 6–19: die verschiedenen Abhängigkeiten des Kundenprojekts;
  • Zeilen 20–23: der Benutzer, der Anfragen an den Steuerberechnungsserver stellt;
  • Zeile 24: die gesicherte Datei „URL“ des Steuerberechnungsservers;

Der Code des Skripts [MainImpotsClient.php] lautet wie folgt:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);

// Namensraum
namespace Application;

// Fehlerbehandlung durch PHP
//ini_set("display_errors", "0");
//
// Pfad zur Konfigurationsdatei
define("CONFIG_FILENAME", "../Data/config-client.json");

// die Konfiguration wird abgerufen
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// die für das Skript erforderlichen Abhängigkeiten werden eingebunden
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// Absolute Abhängigkeiten (Bibliotheken von Drittanbietern)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// Definition der Konstanten
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// Symfony-Abhängigkeiten
use Symfony\Component\HttpClient\HttpClient;

// Erstellung der Schicht [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// Erstellung der Schicht [métier]
$clientMetier = new ClientMetier($clientDao);

// Steuerberechnung im Batch-Modus
try {
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
  // Anzeige des Fehlers
  print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// Ende
print "Terminé\n";
exit;

Kommentare

  • Zeile 13: Pfad zur Konfigurationsdatei;
  • Zeile 16: Auswertung der Konfigurationsdatei;
  • Zeilen 18–26: Laden der Abhängigkeiten;
  • Zeile 37: Erstellung der Ebene [dao]. Dem Konstruktor der Ebene werden die beiden erforderlichen Informationen übergeben:
    • die URL des Steuerberechnungsservers;
    • die Anmeldedaten des Benutzers, der die Abfragen durchführen wird;
  • Zeile 39: Erstellung der Schicht [métier]. Dem Konstruktor der Schicht wird eine Referenz auf die soeben erstellte Schicht [dao] übergeben;
  • Zeile 43: Die Ebene [métier] wird beauftragt,
    • die Steuern aller Steuerzahler aus der Datei $config["taxPayerDataFileName"] zu berechnen;
    • die Ergebnisse in die Datei $config["resultsFileName"] zu schreiben;
    • Die Fehler in die Datei „$config["errorsFileName"]“ schreiben;
  • Zeile 43 kann Ausnahmen auslösen;
  • Zeile 46: Anzeige der Fehlermeldung der Ausnahme;

Die Ausführung des Clients führt zu denselben Ergebnissen wie bei den vorherigen Versionen. Überprüfen Sie die folgenden Dateien:

  • [Data/taxpayersdata.json]: Daten der Steuerpflichtigen, für die der Steuerbetrag berechnet wird;
  • [Data/results.json]: Ergebnisse für die verschiedenen Steuerzahler aus der Datei [Data/taxpayersdata.json];
  • [Data/errors.json]: Fehler, die bei der Verarbeitung der Datei [Data/taxpayersdata.json] aufgetreten sein könnten;

Sehen wir uns die möglichen Fehlerfälle an. Starten wir zunächst den Laragon-Server neu. Die Ergebnisse in der Client-Konsole lauten dann wie folgt:


Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé

Starten wir nun nur den Apache-Server und nicht den SGBD MySQL:

Image

Die Ergebnisse in der Client-Konsole lauten dann wie folgt:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}
Terminé

Starten wir nun MySQL und ändern wir anschließend in [config-client] den Benutzer, der sich anmeldet:

1
2
3
4
    "user": {
        "login": "x",
        "passwd": "x"
},

Die Ergebnisse in der Client-Konsole lauten dann wie folgt:


L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé

18.3.5. Tests [Codeception]

Wie bereits bei den vorherigen Versionen werden wir Tests für [Codeception] für die Version 08 schreiben.

Image

18.3.5.1. Test der Schicht [métier]

Der Test [ClientMetierTest.php] lautet wie folgt:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);

// Namensraum
namespace Application;

// Definition der Konstanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");

// Pfad zur Konfigurationsdatei
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");

// Die Konfiguration wird abgerufen
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// Einbinden der für das Skript erforderlichen Abhängigkeiten
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// Absolute Abhängigkeiten (Bibliotheken von Drittanbietern)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// Testklasse
class ClientMetierTest extends \Codeception\Test\Unit {
  // Geschäftslogikschicht
  private $métier;

  public function __construct() {
    parent::__construct();
    // Die Konfiguration wird abgerufen
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // Erstellung der Schicht [dao]
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // Erstellung der Schicht [métier]
    $this->métier = new ClientMetier($clientDao);
  }

  // Tests
  public function test1() {

  }

  -------------

  public function test11() {

  }

}

Kommentare

  • Zeilen 10–26: Definition der Testumgebung. Wir verwenden dieselbe wie im Hauptskript [MainImpotsClient], das im Abschnitt „Link“ beschrieben ist;
  • Zeilen 33–41: Aufbau der Ebenen [dao] und [métier];
  • Zeile 40: Das Attribut [$this→métier] verweist auf die Ebene [métier];
  • Zeilen 44–51: Die Methoden [test1, test2…, test11] sind die im Abschnitt „Link“ beschriebenen;

Die Testergebnisse lauten wie folgt:

Image