Skip to content

19. Anwendungsübung – Version 9

In dieser Version werden wir den Server wie folgt verbessern:

  • Derzeit werden bei jeder Anfrage die Daten der Steuerbehörde in der Datenbank abgefragt. Wir werden eine Sitzung verwenden:
    • Bei der ersten Anfrage eines Benutzers werden die Daten der Steuerbehörde aus der Datenbank abgerufen und in der Sitzung gespeichert;
    • Bei den folgenden Anfragen desselben Benutzers werden die Daten der Steuerbehörde aus der Sitzung abgerufen. Wir können eine leichte Verkürzung der Ausführungszeit erwarten, da Datenbankabfragen ressourcenintensiv sind;
  • Der Server protokolliert wichtige Ereignisse in einer Textdatei:
    • die erfolgreiche oder fehlgeschlagene Authentifizierung;
    • die Gültigkeit oder Ungültigkeit der vom Client gesendeten Parameter;
    • das Ergebnis der Steuerberechnung;
    • die verschiedenen Fehlerfälle;
  • im Falle eines schwerwiegenden Fehlers wird eine E-Mail an den Administrator der Anwendung gesendet;

Auch der Client muss angepasst werden, um das ihm zugesandte Sitzungs-Cookie zu verarbeiten.

19.1. Der Server

Wir befassen uns mit dem serverseitigen Teil der Anwendung.

Image

Diese Architektur wird durch die folgenden Skripte implementiert:

Image

19.1.1. Dienstprogramme

Image

19.1.1.1. Die Klasse [Logger]

Die Klasse [Logger] wird zum Schreiben von Protokollen in eine Textdatei verwendet:


<?php

namespace Application;

class Logger {
  // Attribut
  private $resource;

  // Konstruktor
  public function __construct(string $logsFilename) {
    // Datei öffnen
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  // Eintrag einer Meldung in die Protokolle
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }

  // Schließen der Protokolldatei
  public function close() {
    fclose($this->resource);
  }

}

Kommentare

  • Zeile 7: Die Ressource der Protokolldatei;
  • Zeile 10: Der Konstruktor der Klasse erhält als Parameter den Namen der Protokolldatei;
  • Zeile 12: Die Textdatei wird im Anfüge-Modus (a+) geöffnet: Die Datei wird geöffnet, ihr Inhalt bleibt erhalten. Die Schreibvorgänge erfolgen am Ende des aktuellen Inhalts;
  • Zeilen 13–15: Wenn das Öffnen nicht möglich war, wird eine Ausnahme ausgelöst;
  • Zeilen 19–21: Mit der Methode [write] wird die Meldung [$message] in die Protokolldatei geschrieben, der das Datum und die Uhrzeit vorangestellt werden;
  • Zeilen 24–16: Mit der Methode [close] wird die Protokolldatei geschlossen;

Hinweis: Die Serveranwendung kann möglicherweise mehrere Clients gleichzeitig bedienen. Es gibt jedoch nur eine einzige Protokolldatei für alle. Daher besteht die Gefahr von konkurrierenden Zugriffen beim Schreiben in die Datei. Die Schreibvorgänge müssten daher synchronisiert werden, um zu verhindern, dass sie sich vermischen. Zu diesem Zweck verfügt PHP über die Semaphoren [https://www.php.net/manual/fr/book.sem.php]. Wir werden die Synchronisation der Schreibvorgänge hier außer Acht lassen, sollten uns des Problems jedoch bewusst bleiben.

19.1.1.2. Die Klasse [SendAdminMail]

Die Klasse [SendAdminMail] ermöglicht es, im Falle eines Absturzes der Anwendung eine E-Mail an den Administrator der Anwendung zu senden:


<?php

namespace Application;

class SendAdminMail {
  // Attribute
  private $config;
  private $logger;

  // Konstruktor
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }

  public function send() {
    // sendet $this->config['message'] an den SMTP-Server $this->config['smtp-server'] am Port $infos[smt-port]
    // Wenn $this->config['tls'] wahr ist, wird das Medium TLS verwendet
    // Die E-Mail wird im Namen von $this->config['from'] versendet
    // an den Empfänger $this->config['to']
    // Die E-Mail hat den Betreff $this->config['subject']
    // An die E-Mail werden die Anhänge von $this->config['attachments'] angehängt
    // das Ergebnis der Methode
    try {
      // Erstellung der Nachricht
      $message = (new \Swift_Message())
        // Betreff der Nachricht
        ->setSubject($this->config["subject"])
        // Absender
        ->setFrom($this->config["from"])
        // Empfänger mit einem Verzeichnis (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // Nachrichtentext
        ->setBody($this->config["message"])
      ;
      // Anhänge
      foreach ($this->config["attachments"] as $attachment) {
        // Pfad zum Anhang
        $fileName = __DIR__ . $attachment;
        // Es wird überprüft, ob die Datei vorhanden ist
        if (file_exists($fileName)) {
          // Das Dokument wird an die Nachricht angehängt
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // Fehler
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // Protokoll TLS?
      if ($this->config["tls"] === "TRUE") {
        // TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
          ->setUsername($this->config["user"])
          ->setPassword($this->config["password"]);
      } else {
        // kein TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // der Versandmanager
      $mailer = new \Swift_Mailer($transport);
      // Versenden der Nachricht
      $mailer->send($message);
      // Ende
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // Fehler
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }

}

Anmerkungen

  • Zeile 11: Der Konstruktor erhält zwei Parameter:
    • [$config]: ein assoziatives Array, das alle für den Versand der E-Mail erforderlichen Informationen enthält;
    • [$logger]: ein Logger, mit dem wichtige Zeitpunkte beim Versand der E-Mail protokolliert werden können;

Das assoziative Array hat folgende Form:

1
2
3
4
5
6
7
8
9
{
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
}
  • Zeilen 16–76: Die Methode [send] ermöglicht den Versand der E-Mail. Dieser Code wurde im Abschnitt „Link“ vorgestellt und beschrieben;

19.1.2. Die Schicht [dao]

Image

Das Skript [ServeurDaoWithSession.php] lautet wie folgt:


<?php

// Namensraum
namespace Application;

// Definition einer Klasse ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {

  // Konstruktor
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // einfachster Fall
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // die Kontrolle wird an die übergeordnete Klasse übergeben
      parent::__construct($databaseFilename);
    }
  }

}

Anmerkungen

  • Zeile 7: Die Klasse [ServerDaoWithSession] der Version 09 erweitert die Klasse [ServerDao] der Version 08. Die Klasse [ServerDao] kann nämlich die Datenbank nutzen. Nun müssen wir nur noch den Fall berücksichtigen, in dem die Daten der Steuerbehörde bereits erfasst wurden:
  • Zeile 10: Der Konstruktor erhält nun zwei Parameter:
    • [string $databaseFilename]: Name der Datei, die die Informationen für die Verbindung zur Datenbank enthält, falls die Daten der Steuerbehörde noch nicht abgerufen wurden; andernfalls NULL;
    • [TaxAdminData $taxAdminData]: die Daten der Steuerbehörde, sofern diese bereits abgerufen wurden; andernfalls NULL;

Beim Start einer Websitzung wird die Schicht [dao] mit einem Objekt [$databaseFilename] (kein NULL) und einem Objekt [taxAdminData] (NULL) aufgebaut. Die Daten der Steuerbehörde werden dann in der Datenbank abgerufen und in der Sitzung gespeichert. Bei nachfolgenden Abfragen derselben Sitzung wird die Schicht [dao] mit einem Objekt [databaseFilename], NULL und einem Objekt [taxAdminData] aus der Sitzung aufgebaut, nicht jedoch mit NULL. Es findet also keine Datenbankabfrage statt.

19.1.3. Das Server-Skript

Das Serverskript [impots-server.php] wird durch die folgende Datei jSON [config-server.json] konfiguriert:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
    "databaseFilename": "Data/database.json",
    "relativeDependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Entities/TaxAdminData.php",
        "/../version-08/Entities/Database.php",
        "/../version-08/Dao/InterfaceServerDao.php",
        "/../version-08/Dao/ServerDao.php",
        "/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Data/logs.txt"
}

Das Server-Skript [impots-server.php] entwickelt sich 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);

// Einbindung 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";
}
//
// Symfony-Abhängigkeiten
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

// Sitzung
$session = new Session();
$session->start();

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

// Erstellung der Protokolldatei
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // Interner Serverfehler
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // abgeschlossen
  exit;
}

// 1. Log
$logger->write("\n---nouvelle requête\n");

// die aktuelle Anfrage wird abgerufen
$request = Request::createFromGlobals();
// Authentifizierung nur beim ersten Mal
if (!$session->has("user")) {
  // Protokoll
  $logger->write("Autentification en cours…\n");
  // Authentifizierung

  }
  // Wurde der Benutzer gefunden?
  if (!$trouvé) {
    // nicht gefunden – Code 401 HTTP_UNAUTHORIZED
    sendResponse(
      $response,
      ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
      Response::HTTP_UNAUTHORIZED,
      ["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
      $logger
    );
    // abgeschlossen
    exit;
  } else {
    // In der Sitzung wird vermerkt, dass der Benutzer authentifiziert wurde
    $session->set("user", TRUE);
    // Protokoll
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // Protokoll
  $logger->write("Authentification prise en session…\n");
}
// Es liegt ein gültiger Benutzer vor – die empfangenen Parameter werden überprüft
$erreurs = [];
// Es müssen drei Parameter vorhanden sein GET


// Fehler?
if ($erreurs) {
// Wir senden einen Fehlercode 400 HTTP_BAD_REQUEST an den Client
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // abgeschlossen
  exit;
} else {
  // Protokolle
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// Es ist alles vorhanden, was für die Arbeit benötigt wird
// Erstellung der Ebene [dao]
if (!$session->has("taxAdminData")) {
  // Die Daten werden aus der Datenbank abgerufen
  $logger->write("données fiscales prises en base de données\n");
  try {
    // Erstellung der Ebene [dao]
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    // Die Daten werden in die Sitzung geladen
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // Der Fehler wird vermerkt
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // abgeschlossen
    exit;
  }
} else {
  // Die Daten werden in die Sitzung übernommen
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // Protokolle
  $logger->write("données fiscales prises en session\n");
}
// Erstellung der Ebene [métier]
$métier = new ServerMetier($dao);
// Steuerberechnung
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// die Antwort wird ausgegeben
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// Ende
exit;

function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // E-Mail an den Administrator senden
  // SendAdminMail fängt alle Ausnahmen ab und protokolliert sie selbst
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // Es wird ein 500-Fehlercode an den Client gesendet
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}

// Funktion zum Senden der Antwort HTTP an den Kunden
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $response: Antwort HTTP
  // $result: Ergebnistabelle
  // $statusCode: Status HTTP der Antwort
  // $headers: HTTP-Kopfzeilen, die in die Antwort aufgenommen werden sollen
  // $logger: der Logger der Anwendung
  //
  // Status HTTTP
  $response->setStatusCode($statusCode);
  // Hauptteil
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // Header
  $response->headers->add($headers);
  // Senden
  $response->send();
  // Protokoll
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
}

Kommentare

  • Zeilen 34–35: Es wird eine Sitzung gestartet;
  • Zeilen 38–40: Eine Antwort jSON wird vorbereitet;
  • Zeilen 42–50: Es wird versucht, die Protokolldatei zu erstellen. Tritt eine Ausnahme auf, wird die Methode [doInternalServer] (Zeilen 132–140) aufgerufen;
  • Zeile 132: Die Methode [doInternalServer] akzeptiert vier Parameter:
    • [$message]: Die zu protokollierende Meldung. Muss im Format UTF-8 codiert sein;
    • [$response]: das Objekt [Response], das die Antwort des Servers an seinen Client kapselt;
    • [$logger]: das Objekt [Logger], das die Protokollierung ermöglicht;
    • [$infos]: die Informationen, die das Versenden einer E-Mail an den Administrator der Anwendung ermöglichen;
  • Zeilen 135–137: Es wird eine E-Mail an den Administrator der Anwendung gesendet;
  • Zeile 139: Die Antwort wird an den Kunden gesendet:
    • $response: Antwort HTTP;
    • $result: Der Server sendet die Zeichenfolge jSON aus dem Array [‘réponse’=>["erreur" => $message]];
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], Code 500;
    • $headers: [], keine HTTP-Header, die der Antwort hinzugefügt werden müssen;
    • $logger: der Logger der Anwendung;
  • Zeile 58: Dank der eingerichteten Sitzung wird die Authentifizierung des Kunden nur einmal durchgeführt:
    • Sobald der Client authentifiziert ist, wird ein Schlüssel [user] in die Sitzung geschrieben (Zeile 78);
    • Bei der nächsten Anfrage desselben Kunden verhindert Zeile 58 eine nun überflüssige Authentifizierung;
  • Zeile 103: Dank der eingerichteten Sitzung werden die Daten nur einmal in der Datenbank abgefragt:
    • Bei der ersten Anfrage erfolgt die Suche in der Datenbank (Zeile 108). Die abgerufenen Daten werden anschließend in die Sitzung aufgenommen (Zeile 110) und dem Schlüssel [taxAdminData] zugeordnet;
    • Bei den folgenden Anfragen wird der Schlüssel [taxAdminData] in der Sitzung gefunden (Zeile 103), und die Daten auf der Festplatte werden dann direkt an die Ebene [dao] übermittelt (Zeile 119);
  • Zeilen 111–116: Die Suche nach Steuerdaten in der Datenbank kann fehlschlagen. In diesem Fall wird dem Kunden der Code [500 Internal Server Error] gesendet;
  • Zeile 113: Die Fehlermeldung der Ausnahme des Treibers MySQL ist als ISO 8859-1 codiert. Sie wird in UTF-8 umgewandelt, um korrekt protokolliert zu werden;
  • Der Rest des Codes ist nahezu identisch mit dem der vorherigen Version;
  • Zeilen 143–164: Die Funktion [sendResponse] sendet alle Antworten an den Client;
  • Zeilen 144–148: Bedeutung der Parameter;
  • Zeile 153: Die Antwort ist immer die Zeichenkette „jSON“ aus einem Array „[‘résultat’=>qqChose]“;
  • Zeile 156: Manchmal müssen der Antwort Header vom Typ HTTP hinzugefügt werden. Dies ist in Zeile 71 der Fall;
  • Zeile 158: Die Antwort wird gesendet;
  • Zeilen 160–163: Die Antwort wird protokolliert und der Logger geschlossen;

19.1.4. Tests [Codeception]

Image

Wir werden nur die Ebene „[dao]“ testen, da sich nur diese geändert hat.

Der Testcode für [ServerDaoTest] lautet wie folgt:


<?php

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

// Namensraum
namespace Application;

// Definition von Konstanten
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// 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 ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // Tests
  public function testTaxAdminData() {

  }

}
  • Zeilen 9–24: Es wird eine Ausführungsumgebung erstellt, die mit der des Serverskripts [impots-server] identisch ist;
  • Zeile 38: Um die Schicht [dao] zu erstellen, wird die Klasse [ServerDaoWithSession] instanziiert;

Das Testergebnis lautet wie folgt:

Image

19.2. Der Client

Wir befassen uns mit dem clientseitigen Teil der Anwendung.

Image

Diese Architektur wird durch die folgenden Skripte implementiert:

Image

In der neuen Version ändern sich lediglich:

  • die Konfigurationsdatei [config-client.json];
  • die Client-Schicht [dao];

19.2.1. die Schicht [dao]

Die Schicht [Dao] entwickelt sich wie folgt weiter:


<?php

namespace Application;

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

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

  // 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 {
    // Sitzungs-Cookie?
    if (!$this->sessionCookie) {
      // Ein Kunde wird angelegt HTTP
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // Anfrage an den Server ohne Sitzungs-Cookie
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // Anfrage an den Server mit dem Session-Cookie
      // Es wird ein Client HTTP erstellt
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // 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);
    }
    if (!$this->sessionCookie) {
      // Das Session-Cookie wird abgerufen
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // Sitzungs-Cookie?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // Die Antwort wird zurückgegeben
    return $réponse;
  }

}

Kommentare

Die Änderung der Ebene [dao] besteht darin, nun eine Sitzung zu verwalten:

  • Zeile 14: das Sitzungs-Cookie;
  • Zeilen 25–39: Bei der ersten Anfrage existiert dieses Cookie noch nicht: Daher wird die Anfrage an den Server gesendet, wobei die Authentifizierungsdaten übermittelt werden (Zeile 28);
  • Zeilen 40–53: Bei den folgenden Anfragen liegt normalerweise das Sitzungs-Cookie vor. Die Authentifizierungsdaten werden daher nicht gesendet (Zeilen 42–44);
  • Zeilen 69–82: Die Antwort des Servers auf die erste Anfrage enthält ein Sitzungs-Cookie. Dieses wird abgerufen. Dieser Code wurde bereits im Abschnitt „Link“ verwendet und erläutert;
  • Zeile 78: Das abgerufene Session-Cookie wird im Klassenattribut [$sessionCookie] gespeichert;

Hinweis: Man hätte die alte Version der Schicht [dao] beibehalten und die Authentifizierung bei jeder Anfrage durchführen können, da deren Aufwand vernachlässigbar ist. Aus didaktischen Gründen wollten wir jedoch verdeutlichen, wie ein Client HTTP eine Sitzung verwalten kann.

19.2.2. Die Konfigurationsdatei

Die Konfigurationsdatei jSON entwickelt sich wie folgt:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/TaxPayerData.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Utilities/Utilitaires.php",
        "/../version-08/Dao/InterfaceClientDao.php",
        "/../version-08/Dao/TraitDao.php",
        "/Dao/ClientDao.php",
        "/../version-08/Métier/InterfaceClientMetier.php",
        "/../version-08/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-09/impots-server.php"
}

Nur der Wert URL in Zeile 24 ändert sich.

19.3. Einige Tests

19.3.1. Test 1

Zunächst führen wir den Client in einer fehlerfreien Umgebung aus. Die Ergebnisse entsprechen weiterhin denen der vorherigen Versionen. Auf der Serverseite liegt nun jedoch eine Protokolldatei mit dem Namen [logs.txt] vor:


04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête

  • Zeilen 3–7: Bei der ersten Anfrage erfolgt eine Authentifizierung und eine Abfrage der Daten aus der Datenbank;
  • Zeilen 9–14: Bei der folgenden Anfrage findet keine Authentifizierung mehr statt und die Daten werden aus der Sitzung abgerufen. Dies wiederholt sich bei den folgenden Anfragen (Zeilen 15 und weiter);

19.3.2. Test 2

Nun schalten wir die Datenbank MySQL ab. Auf der Client-Seite erhalten wir folgende Konsolenausgabe:


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é

Auf der Serverseite erhalten wir die folgenden Protokolle: [logs.txt]:


04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête

Um die E-Mail abzurufen, die der Administrator der Anwendung erhalten hat, verwendet man das Skript [imap-03.php] aus dem Abschnitt „Verknüpfung mit der Konfigurationsdatei [config-imap-01.json]“ wie folgt:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Man erhält folgendes Ergebnis:

Image

Die Datei [message_1.txt] enthält den folgenden Text:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.

19.3.3. Test 3

Nun sorgen wir dafür, dass die Datei „[logs.txt]“ nicht erstellt werden kann. Dazu reicht es aus, einen Ordner „[logs.txt]“ anzulegen:

Image

Nachdem dies erledigt ist, führen wir den Client aus.

Auf der Client-Seite erhalten wir folgende Konsolenergebnisse:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé

Auf der Serverseite gibt es keine Protokolle, aber der Administrator erhält die folgende E-Mail:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

Echec lors de la création du fichier de logs [Data/logs.txt]

19.3.4. Test 4

Dieses Mal geben wir in der Konfigurationsdatei des Clients falsche Anmeldedaten für den sich verbindenden Client an.

Der Client zeigt die folgenden Konsolenergebnisse an:


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

Auf der Serverseite erscheinen die folgenden Protokolleinträge:


---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}

19.3.5. Test 5

Tragen wir den korrekten Benutzer „[admin, admin]“ wieder in die Konfigurationsdatei des Clients ein.

Rufen wir nun die Adresse URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] des Servers direkt in einem Browser auf, ohne Parameter zu übergeben:

In der Protokolldatei [logs.txt] des Servers finden sich folgende Zeilen:


---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}

19.4. Tests [Codeception]

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

Image

19.4.0.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 von Konstanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09");

// 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";
}
//
// Verwendet
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;

// Testklasse
class ClientMetierTest extends Unit {
  
}

Anmerkungen

  • Im Vergleich zur Testklasse der Version 08 ändert sich lediglich Zeile 10, in der das Stammverzeichnis des zu testenden Clients angegeben wird;

Die Testergebnisse lauten wie folgt:

Image

Es ist interessant, einen Blick in die Protokolle des Servers [logs.txt] zu werfen:


04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête

---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}

Es ist festzustellen, dass die Daten der Steuerbehörde immer aus der Datenbank und niemals aus der Sitzung abgerufen werden. Kehren wir zum Code des ausgeführten Tests zurück:


<?php

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

// Namensraum
namespace Application;



// Testklasse
class ClientMetierTest extends 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 test2() {

  }

  public function test3() {

  }



}

In einer Testklasse [Codeception] wird der Konstruktor für jeden Test ausgeführt.

  • Zeile 21: Für jeden Test wird somit ein neues [ClientDao] mit einem Sitzungs-Cookie NULL erstellt. Dies erklärt, warum dieser Client nicht von einer Sitzung profitiert;

Dieses Beispiel zeigt uns, dass die Sitzung nicht der richtige Ort ist, um Daten der Steuerbehörde zu speichern. Diese Daten sind nämlich für alle Nutzer der Anwendung gleich. Hier werden sie jedoch in jeder ihrer Sitzungen dupliziert.

In der Webprogrammierung unterscheidet man drei Arten der Sichtbarkeit für gemeinsam genutzte Daten:

  • Daten, die von allen Benutzern der Webanwendung gemeinsam genutzt werden. Dabei handelt es sich in der Regel um schreibgeschützte Daten. PHP verfügt von Haus aus nicht über diesen Speicher;
  • Daten, die von den Anfragen desselben Clients gemeinsam genutzt werden. Diese Daten werden in der Sitzung gespeichert. Man spricht dabei von einer Client-Sitzung, um den Speicher des Clients zu bezeichnen. Alle Anfragen eines Clients haben Zugriff auf diese Sitzung. Sie können dort Informationen speichern und auslesen. In den vorangegangenen Skripten wird diese Sitzung durch das Symfony-Objekt [HttpFoundation\Session\Session] implementiert;
  • der Anforderungsspeicher oder Anforderungskontext. Die Anfrage eines Benutzers kann durch mehrere aufeinanderfolgende Aktionen verarbeitet werden. Der Anfragekontext ermöglicht es einer Aktion 1, Informationen an eine Aktion 2 weiterzugeben. In den vorangegangenen Skripten wird die Anfrage durch das Symfony-Objekt [HttpFoundation\Request] und ihr Speicher durch das Attribut [HttpFoundation\Request::attributes] implementiert;

Image

Es gibt Bibliotheken von Drittanbietern, um PHP einen Anwendungsspeicher zuzuweisen. Die neue Version der Anwendungsübung zeigt die Verwendung einer dieser Bibliotheken.