Skip to content

18. Übungsaufgabe – Version 8

Wir werden die Beispielanwendung – Version 5 (Link-Absatz) – noch einmal aufgreifen und sie in eine Client/Server-Anwendung umwandeln.

18.1. Einführung

Die Architektur von Version 5 sah wie folgt aus:

Image

  • Die als [dao] (Data Access Objects) bezeichnete Schicht übernimmt die Interaktion mit der MySQL-Datenbank und dem lokalen Dateisystem;
  • die als [business] bezeichnete Schicht führt die Steuerberechnung durch;
  • Das Hauptskript fungiert als Koordinator: Es instanziiert die Schichten [DAO] und [business logic] und kommuniziert anschließend mit der Schicht [business logic], um die erforderlichen Aufgaben auszuführen;

Wir werden diese Architektur auf die folgende Client/Server-Architektur migrieren:

Image

  • In [2] werden wir die [DAO]-Schicht aus Version 5 wiederverwenden und dabei die Methoden für den Zugriff auf das lokale Dateisystem entfernen. Diese Methoden werden in die [DAO]-Schicht des Clients migriert [6, 7];
  • In [3] bleibt die [Business]-Schicht unverändert gegenüber Version 5, mit Ausnahme der Methoden [executeBatchImpôts, saveResults], die in die [DAO]-Schicht des Clients migriert werden [7];
  • In [4] muss das Server-Skript geschrieben werden: Es muss:
    • die [Business]- und [DAO]-Schichten erstellen [3, 2];
    • mit dem Client-Skript kommunizieren [5, 7];
  • In [7] muss die [dao]-Schicht des Clients geschrieben werden:
    • Es wird ein HTTP-Client des Server-Skripts sein [4, 5];
    • es wird die Methoden für den Zugriff auf das lokale Dateisystem aus der [DAO]-Schicht der Version 5 wiederverwenden;
  • In [8] entspricht die [Business]-Schicht des Clients der [BusinessInterface]-Schnittstelle aus Version 5. Ihre Implementierung wird jedoch anders sein. In Version 5 führte die [business]-Schicht die Steuerberechnung durch. Hier ist es die [business]-Schicht des Servers, die diese Berechnung durchführt. Die [business]-Schicht wird daher die [DAO]-Schicht [7] aufrufen, um mit dem Server zu kommunizieren und ihn aufzufordern, die Steuer zu berechnen;
  • in [9] muss das Konsolenskript die [DAO- und Geschäftsschichten] des Clients instanziieren und deren Ausführung starten;

18.2. Der Server

Wir konzentrieren uns auf die Serverseite der Anwendung.

Image

Diese Architektur wird durch die folgenden Skripte implementiert:

Image

18.2.1. Zwischen den Ebenen ausgetauschte Entitäten

Image

Die zwischen den Schichten ausgetauschten Entitäten sind diejenigen aus Version 5, die im verlinkten Abschnitt beschrieben sind.

18.2.2. Die [dao]-Schicht

Image

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


<?php
 
// namespace
namespace Application;
 
interface InterfaceServerDao {
 
  // reading tax administration data
  public function getTaxAdminData(): TaxAdminData;
}
  • Zeile 9: Die Methode [getTaxAdminData] ruft Daten der Steuerverwaltung aus einer Datenbank ab;

Die Schnittstelle [InterfaceServerDao] wird von der folgenden Klasse [ServerDao] implementiert:


<?php
 
// namespace
namespace Application;
 
// definition of a ImpotsWithDataInDatabase class
class ServerDao implements InterfaceServerDao {
  // the TaxAdminData object containing tax bracket data
  private $taxAdminData;
  // the [Database] type object containing the characteristics of the BD
  private $database;
 
  // manufacturer
  public function __construct(string $databaseFilename) {
    // store the JSON configuration of the bd
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // we prepare the attribute
    $this->taxAdminData = new TaxAdminData();
    try {
      // open the database connection
      $connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
      // we want every SGBD error to trigger an exception
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // start a transaction
      $connexion->beginTransaction();
      // fill in the tax bracket table
      $this->getTranches($connexion);
      // fill in the constants table
      $this->getConstantes($connexion);
      // the transaction is completed successfully
      $connexion->commit();
    } catch (\PDOException $ex) {
      // is there a transaction in progress?
      if (isset($connexion) && $connexion->inTransaction()) {
        // transaction ends in failure
        $connexion->rollBack();
      }
      // trace the exception back to the calling code
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // close the connection
      $connexion = NULL;
    }
  }
 
  // reading data from the database
  private function getTranches($connexion): void {

  }
 
  // reading the constants table
  private function getConstantes($connexion): void {

  }
 
  // returns data for tax calculation
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }
 
}

Dieser Code wurde im verlinkten Abschnitt vorgestellt.

18.2.3. Die [Business]-Schicht

Image

Image

Die [business]-Schicht implementiert die folgende [InterfaceServerMetier]-Schnittstelle:


<?php
 
// namespace
namespace Application;
 
interface InterfaceServerMetier {
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}

Die Schnittstelle [InterfaceServerMetier] wird von der folgenden Klasse [ServerMetier] implementiert:


<?php
 
// namespace
namespace Application;
 
class ServerMetier implements InterfaceServerMetier {
  // dao layer
  private $dao;
  // tax administration data
  private $taxAdminData;
 
  //---------------------------------------------
  // setter couche [dao]
  public function setDao(InterfaceServerDao $dao) {
    $this->dao = $dao;
    return $this;
  }
 
  public function __construct(InterfaceServerDao $dao) {
    // a reference is stored on the [dao] layer
    $this->dao = $dao;
    // recover data for tax calculation
    // method [getTaxAdminData] may throw a ExceptionImpots exception
    // we then let it go back to the calling code
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }
 
// tAX CALCULATION
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // result
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }
 
// --------------------------------------------------------------------------
  private function calculerImpot2(string $marié, int $enfants, float $salaire): array {

    // result
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }
 
  // revenuImposable=annualwage-discount
  // the allowance has a minimum and a maximum
  private function getRevenuImposable(float $salaire): float {

    // result
    return floor($revenuImposable);
  }
 
// calculates any discount
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // result
    return ceil($décôte);
  }
 
// calculates any reduction
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    ..
    // result
    return ceil($réduction);
  }
}

Dieser Code wurde bereits in Version 1 im verlinkten Abschnitt behandelt. Seine objektorientierte Version mit einer Datenbank wurde im verlinkten Abschnitt vorgestellt.

18.2.4. Das Server-Skript

Image

Image

Das Server-Skript implementiert die [Web]-Schicht [4]. Das Skript [impots-server] wird durch die folgende JSON-Datei [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 Dateipfade gemessen werden;
  • Zeile 2: die JSON-Konfigurationsdatei für die MySQL-Datenbank;
  • Zeile 3: die JSON-Datei mit den Steuerverwaltungsdaten;
  • Zeilen 5–14: die Anwendungsdateien;
  • Zeile 15: die Abhängigkeit von Bibliotheken von Drittanbietern, in diesem Fall Symfony;
  • Zeilen 16–20: das Array der Benutzer, die zur Nutzung der Anwendung berechtigt sind;

Die JSON-Dateien [database.json, taxadmindata.json] stammen aus Version 5, wie im verlinkten Abschnitt beschrieben.

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


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// definition of constants
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
 
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
 
// retrieve the current query
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// does the user exist?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
  $trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
  $i++;
}
// set the response status code
if (!$trouvé) {
  // not found - code 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
  // error msg
  $response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
  $response->send();
  // end
  exit;
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// mistake?
if ($erreur) {
  $erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}

// marital status is restored
if (!$request->query->has("marié")) {
  $erreurs[] = "paramètre marié manquant";
} else {
  $marié = trim(strtolower($request->query->get("marié")));
  $erreur = $marié !== "oui" && $marié !== "non";
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre marié [$marié] invalide";
  }
}
 
// the number of children
if (!$request->query->has("enfants")) {
  $erreurs[] = "paramètre enfants manquant";
} else {
  $enfants = trim($request->query->get("enfants"));
  // number of children must be an integer >=0
  $erreur = !preg_match("/^\d+$/", $enfants);
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre enfants [$enfants] invalide";
  }
}
 
// we recover the annual salary
if (!$request->query->has("salaire")) {
  $erreurs[] = "paramètre salaire manquant";
} else {
  // salary must be an integer >=0
  $salaire = trim($request->query->get("salaire"));
  $erreur = !preg_match("/^\d+$/", $salaire);
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre salaire [$salaire] invalide";
  }
}
 
// other parameters in the query?
foreach (\array_keys($request->query->all()) as $key) {
  // valid parameter?
  if (!\in_array($key, ["marié", "enfants", "salaire"])) {
    $erreurs[] = "paramètre [$key] invalide";}
}
 
// mistakes?
if ($erreurs) {
  // an error code 400 is sent to the customer
  $response->setStatusCode(Response::HTTP_BAD_REQUEST);
  $response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// we have everything you need to work
// server architecture creation
$msgErreur = "";
try {
  // creation of the [dao] layer
  $dao = new ServerDao($config["databaseFilename"]);
  // creation of the [business] layer
  $métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// we note the error
  $msgErreur = utf8_encode($ex->getMessage());
}
// mistake?
if ($msgErreur) {
  // an error code 500 is sent to the customer
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  $response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();

Kommentare

  • Zeile 16: Wir laden die Konfigurationsdatei;
  • Zeilen 18–26: Wir laden alle Abhängigkeiten;
  • Zeile 29: Der Name der Datei [database.json];
  • Zeilen 32–33: Deklaration der Klassen aus den verwendeten Bibliotheken von Drittanbietern;
  • Zeilen 36–38: Wir bereiten eine JSON-Antwort vor;
  • Zeilen 40–52: Wir überprüfen, ob der Benutzer, der die Anfrage stellt, tatsächlich ein autorisierter Benutzer ist;
  • Zeilen 54–63: Falls nicht, senden wir einen HTTP-401-Code, der den Zugriff verweigert. Nach Erhalt dieses Codes und des HTTP-Headers [WWW-Authenticate => Basic realm=] zeigen die meisten Browser ein Authentifizierungsfenster an, in dem der Benutzer zur Anmeldung aufgefordert wird;
  • Zeile 59: Die JSON-Antwort des Servers erläutert die Ursache des Fehlers. Alle Serverantworten sind eine JSON-Zeichenkette in Form eines Arrays [‘response’=>’something’];
  • Zeilen 64–117: Wir überprüfen die Gültigkeit der Anfrage:
    • eine GET-Anfrage mit genau drei Parametern;
    • ein Parameter [married], dessen Wert „yes“ oder „no“ lauten muss;
    • ein Parameter [children], dessen Wert eine ganze Zahl >= 0 sein muss;
    • ein Parameter [salary], dessen Wert eine ganze Zahl >= 0 sein muss;
  • Zeile 65: Jedes Mal, wenn ein Fehler erkannt wird, wird eine Fehlermeldung zum Array [$errors] hinzugefügt;
  • Zeilen 120–126: Tritt ein Fehler auf, wird der HTTP-Code [400 Bad Request] an den Client gesendet (Zeile 122);
  • Zeile 123: Die JSON-Antwort des Servers erläutert die Ursache des Fehlers;
  • Ab Zeile 132 ist alles überprüft. Wir können die [dao, business]-Schichten instanziieren. Diese Instanziierung ist mit Aufwand verbunden und sollte nur erfolgen, wenn wir sicher sind, dass wir eine gültige Anfrage haben;
  • Zeilen 130–138: Die Serverarchitektur wird erstellt. Beim Aufbau der [DAO]-Schicht kann eine [ExceptionImpots]-Ausnahme ausgelöst werden. Tritt diese Ausnahme auf, wird der Fehler protokolliert;
  • Zeilen 135–138: Wenn eine Ausnahme aufgetreten ist, senden wir den HTTP-Statuscode 500 an den Client. Dieser Code zeigt an, dass der Server abgestürzt ist;
  • Zeile 143: Die Antwort erläutert die Ursache des Fehlers;
  • Zeile 148 : Die Steuerberechnung wird an die [business]-Schicht delegiert;
  • Zeilen 150–151: Senden der Antwort;

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

Image

  • in [1] die angeforderte sichere URL;
  • in [2] die drei Parameter [married, children, salary];
  • in [3] hat Laragons Apache-Server ein selbstsigniertes SSL-Zertifikat gesendet. Der Browser hat dies erkannt und zeigt eine Sicherheitswarnung an: Er stuft die Website des Servers als nicht vertrauenswürdig ein;
  • in [4] fahren wir fort;

Image

  • in [6] fahren wir fort;

Image

  • In [7] zeigt der Browser ein Fenster an, in dem sich der Benutzer anmelden kann;
  • Geben Sie in [9,10] [admin] und [admin] ein;

Image

  • in [13] die JSON-Antwort des Servers;

Führen wir einige Fehlertests durch:

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

Wir erhalten das folgende Ergebnis:

Image

Wir fahren das MySQL-DBMS herunter und rufen die URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000] auf:

Image

18.2.5. [Codeception]-Tests

Jedes Mal, wenn wir eine neue Version des Servers erstellen, testen wir die [Business]- und [DAO]-Schichten, 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 [Codeception]-Tests. Gehen Sie dazu genauso vor wie beim Projekt [scripts-console] im Abschnitt mit dem Link. Am Ende erhalten wir ein Projekt [scripts-web], das einen Ordner [Test Files] enthält:

Image

Wir erstellen einen Test für die [dao]-Schicht und einen für die [business]-Schicht.

18.2.5.1. Tests für die [dao]-Schicht

Image

Der [ServerDaoTest] sieht wie folgt aus:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
 
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// test -----------------------------------------------------
 
class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;
 
  public function __construct() {
    // parent
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  public function testTaxAdminData() {

  }
 
}

Kommentare

  • Zeilen 9–24: Wir richten dieselbe Arbeitsumgebung ein wie die des Servers [impots-server.php]. Dies geschieht in den Zeilen 9–12 durch die Definition der beiden Konstanten, von denen die Umgebung abhängt;
  • Zeilen 32–40: Wir erstellen eine Instanz der zu testenden [dao]-Schicht, wie es auch im Server-Skript [impots-server.php] geschehen ist;
  • Ab diesem Punkt befinden wir uns unter denselben Bedingungen wie das Server-Skript [impots-server.php]: Wir können mit den Tests beginnen;
  • Zeilen 43–45: Die Methode [testTaxAdminData] ist diejenige, die im verlinkten Abschnitt beschrieben wird;

Die Testergebnisse lauten wie folgt:

Image

18.2.5.2. Tests der [business]-Schicht

Image

Der Test [ServerMetierTest] wird wie folgt aussehen:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// test class
class ServerMetierTest extends \Codeception\Test\Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    // creation of the [business] layer
    $this->métier = new ServerMetier($dao);
  }
 
  // tests
  public function test1() {

  }
 
  public function test2() {

  }
 
  ..
 
  public function test11() {

  }
 
}

Kommentare

  • Zeilen 9–24: Wir richten dieselbe Arbeitsumgebung ein wie beim Server [impots-server.php]. Dies geschieht in den Zeilen 9–12 durch die Definition der beiden Konstanten, von denen die Umgebung abhängt;
  • Zeilen 30–38: Wir erstellen eine Instanz der zu testenden [business]-Schicht, wie es bereits im Server-Skript [impots-server.php] geschehen ist;
  • Ab diesem Punkt gelten für uns dieselben Bedingungen wie für das Server-Skript [impots-server.php]: Wir können mit den Tests beginnen;
  • Zeilen 40–53: Die Methoden [test1, test2…, test11] sind diejenigen, die im verlinkten Abschnitt beschrieben sind;

Die Testergebnisse lauten wie folgt:

Image

18.3. Der Client

Wir konzentrieren uns auf die clientseitige Anwendung.

Image

Diese Architektur wird durch die folgenden Skripte implementiert:

Image

18.3.1. Zwischen den Schichten ausgetauschte Entitäten

Image

Die oben aufgeführten Entitäten wurden alle beschrieben und sind bereits im Einsatz:

  • [BaseEntity] im Abschnitt „Link“;
  • [ExceptionImpots] im Link-Abschnitt;
  • [TaxPayerData] im Link-Abschnitt;

18.3.2. Die [dao]-Schicht

Image

Die [dao]-Ebene implementiert die folgende [InterfaceClientDao]-Schnittstelle:


<?php
 
// namespace
namespace Application;
 
interface InterfaceClientDao {
 
  // reading taxpayer data
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
 
  // recording results
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
  • Zeile 9: Die Funktion [getTaxPayersData] lädt die Steuerzahlerdaten aus der Datei [$taxPayersFilename] in den Arbeitsspeicher. Sollten Fehler auftreten, werden diese in der Datei [$errorsFilename] protokolliert;
  • Zeile 12: Die Funktion [calculateTaxes] berechnet die Steuer eines Steuerzahlers;
  • Zeile 15: Die Funktion [saveResults] speichert die Daten aus dem Array [$taxPayersData] – das die Ergebnisse mehrerer Steuerberechnungen enthält – in der Datei [$resultsFilename];

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


<?php
 
namespace Application;
 
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
 
class ClientDao implements InterfaceClientDao {
  // using a Trait
  use TraitDao;
  // attributes
  private $urlServer;
  private $user;
 
  // manufacturer
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }
 
  // tAX CALCULATION
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // create a HTTP customer
    $httpClient = HttpClient::create([
        'auth_basic' => [$this->user["login"], $this->user["passwd"]],
        "verify_peer" => false
    ]);
    // make a request to the server
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
    ]]);
    // the answer is retrieved
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // logs
    // print "$json=json\n";
    // retrieve response status
    $statusCode = $response->getStatusCode();
    // mistake?
    if ($statusCode !== 200) {
      // we have an error - we throw an exception
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    // we return the answer
    return $réponse;
  }
 
}

Kommentare

  • Zeile 10: Wir fügen [TraitDao] ein (siehe verlinkten Absatz), das die Methoden [getTaxPayersData] und [saveResults] implementiert. Damit muss nur noch die Methode [calculateTaxes] implementiert werden. Diese wird in den Zeilen 22–49 implementiert;
  • Zeilen 16–19: Der Konstruktor der Klasse [ClientDao] nimmt zwei Parameter entgegen:
    • die 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 [calculateTaxes] empfängt die drei Parameter, die an den Steuerberechnungsserver gesendet werden sollen;
  • Zeilen 24–27: Es wird ein HTTP-Client erstellt mit:
    • Zeile 25: die Anmeldedaten des Benutzers, der die Anfrage stellt;
    • Zeile 26: der Option, die verhindert, dass der HTTP-Client die Gültigkeit des vom Server gesendeten SSL-Zertifikats überprüft;
  • Zeilen 29–34: Der Server wird mit den drei erwarteten Parametern abgefragt;
  • Zeile 36: Wir rufen die JSON-Antwort vom Server ab. Wenn wir den Parameter [false] bei der Methode [Response::getContent] nicht setzen, löst das [Response]-Objekt eine Ausnahme aus, sobald wir versuchen, den Antwortinhalt [Response::getContent] oder dessen HTTP-Header [Response::getHeaders] abzurufen, sofern der Antwortstatus des Servers im Bereich [3xx-5xx] liegt (Fehlerfall). Hier möchten wir unabhängig vom HTTP-Status der Antwort auf deren Inhalt zugreifen können, und sei es nur, um ihn zu protokollieren (Zeile 40);
  • Zeilen 37–38: Die Antwort des Servers ist eine JSON-Zeichenkette in Form eines Arrays [‘response’=>something]. Wir rufen das [something] ab;
  • Zeile 40: Wir protokollieren die JSON-Antwort im Entwicklungsmodus;
  • Zeile 42: Wir rufen den Antwortstatuscode ab;
  • Zeilen 44–49: Wenn der HTTP-Statuscode nicht 200 ist, ist auf unserem Server ein Problem aufgetreten. Wir lösen dann eine [ExceptionImpots]-Ausnahme aus, deren Meldung aus der JSON-Antwort des Servers besteht, an die der HTTP-Statuscode angehängt ist;
  • Zeile 51: Wir geben das Ergebnis zurück, bei dem es sich um ein assoziatives Array mit den Schlüsseln [tax, surcharge, discount, reduction, rate] handelt;

18.3.3. Die [Geschäfts-]Ebene

Image

Image

Die [Business]-Schicht [8] implementiert die folgende [BusinessClientInterface]:


<?php
 
// namespace
namespace Application;
 
interface InterfaceClientMetier {
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • Zeile 9: Die Funktion [calculateTaxes] berechnet die Steuer;
  • Zeile 12: Die Funktion [executeBatchImpots] berechnet die Steuer für Steuerzahler, deren Daten in der Datei [$taxPayersFileName] enthalten sind, schreibt die Ergebnisse in die Datei [$resultsFileName] und schreibt auftretende Fehler in die Datei [$errorsFileName];

Die Schnittstelle [BusinessClientInterface] wird durch die folgende Klasse [BusinessClient] implementiert:


<?php
 
// namespace
namespace Application;
 
class ClientMetier implements InterfaceClientMetier {
  // attribute
  private $clientDao;
 
  // manufacturer
  public function __construct(InterfaceClientDao $clientDao) {
    // the reference is stored on the [dao] layer
    $this->clientDao = $clientDao;
  }
  
  // tAX CALCULATION
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // we let the exceptions coming from the [dao] layer flow upwards
    // retrieve taxpayer data
    $taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // results table
    $results = [];
    // we exploit them
    foreach ($taxPayersData as $taxPayerData) {
      // tax calculation
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // complete [$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // put the result in the results table
      $results [] = $taxPayerData;
    }
    // recording results
    $this->clientDao->saveResults($resultsFileName, $results);
  }
 
}

Kommentare

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

18.3.4. Das Hauptskript

Image

Image

Das Client-Skript [MainImpotsClient.php] implementiert die [console]-Schicht [9]. Es wird durch die folgende JSON-Datei [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 Clients;
  • Zeile 2: die JSON-Datei mit den Steuerzahlerdaten;
  • Zeile 3: die JSON-Datei mit den Ergebnissen;
  • Zeile 4: die JSON-Datei mit den Fehlern;
  • Zeilen 6–19: die verschiedenen Abhängigkeiten des Client-Projekts;
  • Zeilen 20–23: der Benutzer, der Anfragen an den Steuerberechnungsserver sendet;
  • Zeile 24: die sichere URL des Steuerberechnungsservers;

Der Code für das Skript [MainImpotsClient.php] lautet wie folgt:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "../Data/config-client.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// definition of constants
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// symfony dependencies
use Symfony\Component\HttpClient\HttpClient;
 
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$clientMetier = new ClientMetier($clientDao);
 
// tax calculation in batch mode
try {
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
  // error is displayed
  print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;

Kommentare

  • Zeile 13: Pfad zur Konfigurationsdatei;
  • Zeile 16: Verarbeitung der Konfigurationsdatei;
  • Zeilen 18–26: Laden der Abhängigkeiten;
  • Zeile 37: Erstellung der [dao]-Schicht. Wir übergeben die beiden Informationen, die der Schichtkonstruktor erwartet:
    • die URL des Steuerberechnungsservers;
    • die Anmeldedaten des Benutzers, der die Anfragen stellen wird;
  • Zeile 39: Erstellung der [Business]-Schicht. Wir übergeben eine Referenz auf die soeben erstellte [DAO]-Schicht an den Konstruktor der Schicht;
  • Zeile 43: Wir weisen die [business]-Schicht an:
    • die Steuern für alle Steuerzahler in der Datei $config["taxPayerDataFileName"] zu berechnen;
    • die Ergebnisse in die Datei $config["resultsFileName"] zu schreiben;
    • Fehler in die Datei $config["errorsFileName"] zu schreiben;
  • Zeile 43 kann Ausnahmen auslösen;
  • Zeile 46: Zeige die Fehlermeldung der Ausnahme an;

Die Ausführung des Clients liefert die gleichen Ergebnisse wie frühere Versionen. Überprüfen Sie die folgenden Dateien:

  • [Data/taxpayersdata.json]: Steuerzahlerdaten, für die der Steuerbetrag berechnet wird;
  • [Data/results.json]: Ergebnisse für die verschiedenen Steuerzahler in der Datei [Data/taxpayersdata.json];
  • [Data/errors.json]: Fehler, die möglicherweise bei der Verarbeitung der Datei [Data/taxpayersdata.json] aufgetreten sind;

Sehen wir uns die möglichen Fehlerfälle an. Stoppen wir zunächst den Laragon-Server. 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 das MySQL-DBMS:

Image

Die Ergebnisse in der Client-Konsole lauten 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 dann den Benutzer, der sich in [config-client] anmeldet:

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

Die Ergebnisse in der Client-Konsole lauten 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 früheren Versionen werden wir [Codeception]-Tests für Version 08 schreiben.

Image

18.3.5.1. Testen der [Business]-Schicht

Der Test [ClientMetierTest.php] sieht wie folgt aus:


<?php

// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
 
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// test class
class ClientMetierTest extends \Codeception\Test\Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // creation of the [business] layer
    $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 verlinkten Abschnitt beschrieben wird;
  • Zeilen 33–41: Aufbau der Schichten [dao] und [business];
  • Zeile 40: Das Attribut [$this→business] verweist auf die [business]-Schicht;
  • Zeilen 44–51: Die Methoden [test1, test2…, test11] sind diejenigen, die im verlinkten Abschnitt beschrieben sind;

Die Testergebnisse lauten wie folgt:

Image