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:

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

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

Diese Architektur wird durch die folgenden Skripte implementiert:

18.2.1. Die zwischen den Schichten ausgetauschten Entitäten

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

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]


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


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:

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

- in [6] geht es weiter;

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

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

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:

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]:

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]

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:

18.2.5.2. Tests der Schicht [métier]

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:

18.3. Der Client
Wir befassen uns mit dem clientseitigen Teil der Anwendung.

Diese Architektur wird durch die folgenden Skripte implementiert:

18.3.1. Die zwischen den Ebenen ausgetauschten Entitäten

Die oben genannten Entitäten wurden alle beschrieben und bereits verwendet:
- [BaseEntity] im Abschnitt „Link“;
- [ExceptionImpots] im Abschnitt „Link“;
- [TaxPayerData] im Abschnitt „Link“;
18.3.2. Die Ebene [dao]

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]


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


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:

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:
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.

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:
