23. Praxisübung – Version 12
In diesem Kapitel werden wir eine Webanwendung schreiben, die der MVC-Architektur (Model-View-Controller) folgt. Die Anwendung wird in der Lage sein, Antworten in drei Formaten zurückzugeben: JSON, XML und HTML. Der Komplexitätsgrad steigt im Vergleich zu dem, was wir bisher gemacht haben, deutlich an. Wir werden die meisten der bisher behandelten Konzepte wiederverwenden und alle Schritte, die zur endgültigen Anwendung führen, detailliert beschreiben.
23.1. MVC-Architektur
Wir werden das MVC-Architekturmuster (Model–View–Controller) wie folgt implementieren:

Die Verarbeitung einer Client-Anfrage verläuft wie folgt:
- 1 – Anfrage
Die angeforderten URLs haben die Form http://machine:port/contexte/….?action=anAction¶m1=v1¶m2=v2&… Der [Hauptcontroller] verwendet eine Konfigurationsdatei, um die Anfrage an den richtigen Controller und die richtige Aktion innerhalb dieses Controllers weiterzuleiten. Dazu nutzt er das Feld [action] in der URL. Der Rest der URL [param1=v1¶m2=v2&…] besteht aus optionalen Parametern, die an die Aktion übergeben werden. Das C in MVC steht hier für die Kette [Hauptcontroller, Controller / Aktion]. Wenn kein Controller die angeforderte Aktion verarbeiten kann, antwortet der Webserver, dass die angeforderte URL nicht gefunden wurde.
- 2 – Verarbeitung
- Die ausgewählte Aktion [2a] kann die Parameter verwenden, die ihr vom [Haupt-Controller] übergeben wurden. Diese können aus verschiedenen Quellen stammen:
- dem Pfad [/param1/param2/…] der URL,
- die URL-Parameter [param1=v1¶m2=v2],
- Parameter, die vom Browser mit seiner Anfrage gesendet wurden;
- Bei der Verarbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die [Business]-Schicht [2b]. Sobald die Anfrage des Clients verarbeitet wurde, kann dies verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
- eine Fehlerantwort, wenn die Anfrage nicht korrekt verarbeitet werden konnte;
- ansonsten eine Bestätigungsantwort;
- der [Controller / Action] sendet seine Antwort [2c] zusammen mit einem Statuscode an den Hauptcontroller zurück. Diese Statuscodes geben den Zustand der Anwendung eindeutig wieder. Es handelt sich dabei entweder um Erfolgscodes oder um Fehlercodes;
- Die ausgewählte Aktion [2a] kann die Parameter verwenden, die ihr vom [Haupt-Controller] übergeben wurden. Diese können aus verschiedenen Quellen stammen:
- 3 – Antwort
- Je nachdem, ob der Client eine JSON-, XML- oder HTML-Antwort angefordert hat, instanziiert der [Haupt-Controller] [3a] den entsprechenden Antworttyp und weist ihn an, die Antwort an den Client zu senden. Der [Haupt-Controller] übergibt ihm sowohl die Antwort als auch den Statuscode, der vom ausgeführten [Controller/Action] bereitgestellt wurde;
- Wenn die gewünschte Antwort vom Typ JSON oder XML ist, formatiert die ausgewählte Antwort die Antwort vom [Controller/Action], die ihr bereitgestellt wurde, und sendet sie [3c]. Der Client, der diese Antwort verarbeiten kann, kann ein PHP-Konsolenskript oder ein in eine HTML-Seite eingebettetes JavaScript-Skript sein;
- Wenn die gewünschte Antwort vom Typ HTML ist, wählt die ausgewählte Antwort [3b] anhand des ihr bereitgestellten Statuscodes eine der HTML-Ansichten [Vuei] aus. Dies ist das „V“ in MVC. Eine einzelne Ansicht entspricht einem Statuscode. Diese Ansicht V zeigt die Antwort des [Controller/ -Action] an, die ausgeführt wurde. Sie verpackt die Daten dieser Antwort in HTML, CSS und JavaScript. Diese Daten werden als View-Modell bezeichnet. Dies ist das M in MVC. Der Client ist meist ein Browser;
Lassen Sie uns nun die Beziehung zwischen der MVC-Webarchitektur und der Schichtenarchitektur klären. Je nachdem, wie das Modell definiert ist, können diese beiden Konzepte miteinander in Verbindung stehen oder auch nicht. Betrachten wir eine einschichtige MVC-Webanwendung:

Im obigen Beispiel enthalten sowohl der [Controller] als auch die [Action] Teile der [Business-] und der [DAO-]Schicht. In der [Web-]Schicht haben wir zwar eine MVC-Architektur, doch die Anwendung als Ganzes weist keine Schichtenarchitektur auf. Hier gibt es nur eine einzige Schicht, die alle Aufgaben übernimmt.
Betrachten wir nun eine mehrschichtige Webarchitektur:

Die [Web]-Schicht kann implementiert werden, ohne dem MVC-Modell zu folgen. Wir haben dann eine mehrschichtige Architektur, aber die Web-Schicht implementiert das MVC-Modell nicht.
In der .NET-Welt kann die oben genannte [Web]-Schicht beispielsweise mit ASP.NET MVC implementiert werden, was zu einer mehrschichtigen Architektur mit einer [Web]-Schicht im MVC-Stil führt. Anschließend können wir diese ASP.NET MVC-Schicht durch eine klassische ASP.NET-Schicht (WebForms) ersetzen, während der Rest (Geschäftslogik, DAO, Treiber) unverändert bleibt. Wir haben dann eine Schichtenarchitektur mit einer [Web]-Schicht, die nicht mehr MVC-basiert ist.
In MVC haben wir gesagt, dass das M-Modell das der V-Ansicht sei, d. h. die Menge der von der V-Ansicht angezeigten Daten. Es gibt eine weitere Definition des M-Modells in MVC:

Viele Autoren sind der Ansicht, dass das, was rechts von der [Web]-Schicht liegt, das M-Modell von MVC bildet. Um Mehrdeutigkeiten zu vermeiden, können wir uns auf Folgendes beziehen:
- das Domänenmodell, wenn wir uns auf alles rechts von der [Web-]Schicht beziehen;
- das View-Modell, wenn wir uns auf die von einer Ansicht V angezeigten Daten beziehen;
23.2. NetBeans-Projektstruktur
Für das NetBeans-Projekt werden wir eine Architektur verwenden, die das MVC-Modell widerspiegelt:

- [3]: [main.php] ist der Hauptcontroller unseres MVC-Modells. Es ist das C in MVC;
- [4]: Der Ordner [Controllers] enthält die sekundären Controller. Jeder davon verarbeitet eine bestimmte Aktion. Diese Aktion wird in der URL angegeben, zum Beispiel […/main.php?action=authenticate-user]. Mit dieser Aktion wählt der [Hauptcontroller] [main.php] einen [sekundären Controller] aus, in diesem Fall [AuthentifierUtilisateurController], um die angeforderte Aktion zu verarbeiten. Diese Controller sind ebenfalls Teil des C in MVC;
- [5]: Der Ordner [Model] enthält die [Business]- und [DAO]-Schichten der Anwendung. Gemäß der zuvor festgelegten Terminologie stellen diese Elemente das Domänenmodell dar und können gemäß der für das M in MVC verwendeten Terminologie das M in MVC repräsentieren;
- [6]: Der Ordner [Responses] enthält die Klassen, die für das Senden der Antwort an den Client zuständig sind. Es gibt eine Klasse pro gewünschtem Antworttyp:
- [JsonResponse]: für eine JSON-Antwort;
- [XmlResponse]: für eine XML-Antwort;
- [HtmlResponse]: für eine HTML-Antwort;
- [7]: Der Ordner [Views] enthält die HTML-Ansichten, wenn eine HTML-Antwort gewünscht wird. Dies ist das V in MVC. Sie werden von der Klasse [HtmlResponse] aktiviert, die ihnen die anzuzeigenden Daten übergibt. Diese Daten sind das View-Modell. Je nach der für das M gewählten Terminologie können diese Daten das M in MVC darstellen;
- [8]: Der Ordner [Utilities] enthält Hilfsprogramme:
- [Logger]: die Klasse, mit der Sie Protokolle in eine Textdatei schreiben können;
- [Sendmail]: die Klasse, mit der Sie E-Mails versenden können;
- [9]: Der Ordner [Logs] enthält die Protokolldatei [logs.txt];
- [10]: Der Ordner [Entities] enthält Klassen, die von den verschiedenen Controllern verwendet werden;
Anhand dieser Verzeichnisstruktur können wir den Verarbeitungsablauf für eine vom Client angeforderte Aktion beschreiben:
- [main.php] [3] empfängt die Anfrage;
- nach Durchführung einiger Vorabprüfungen (gehört die Aktion zu den akzeptierten Aktionen?) leitet es die Anfrage an den sekundären Controller [4] weiter, der für die Verarbeitung dieser Aktion zuständig ist;
- der sekundäre Controller führt seine Aufgabe aus. Dabei benötigt er möglicherweise die Schichten [business] und [DAO] [5] sowie Entitäten aus dem Ordner [10]. Er sendet seine Antwort an den Hauptcontroller [main.php] zurück, der ihn aktiviert hat;
- Je nach Art der vom Client angeforderten Antwort [JSON, XML, HTML] aktiviert der Hauptcontroller [main.php] eine der Antworten aus dem Ordner [Responses] [6];
- die Antworten [JsonResponse] und [XmlResponse] senden die JSON- bzw. XML-Antwort an den Client;
- Die [HtmlResponse] verwendet eine der Ansichten aus dem Ordner [Views] [7], um eine HTML-Antwort an den Client zu senden;
- Die verschiedenen Controller haben Zugriff auf die Klasse [Logger] im Ordner [8], um Protokolle in die Protokolldatei im Ordner [9] zu schreiben. Folgendes wird protokolliert:
- die angeforderte Aktion;
- die Antwort des Controllers. Diese wird unabhängig vom angeforderten Typ [JSON, XML, HTML] im JSON-Format aufgezeichnet;
- im Falle eines schwerwiegenden Fehlers (HTTP_INTERNAL_SERVER_ERROR) sendet der Hauptcontroller [main.php] mithilfe der Klasse [SendMail] im Ordner [8] eine E-Mail an den Administrator;
23.3. Anwendungsaktionen
Der Client sendet die auszuführende Aktion als [action]-Parameter in der URL [/main.php?action=xxx] an den Webserver. Die zulässigen Aktionen sind in der Datei [config.json] aufgeführt, die den Hauptcontroller [main.php] konfiguriert:
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
- Zeile 1: Der Schlüssel [actions] des JSON-Wörterbuchs;
- Zeilen 3–9: ein [action:controller]-Wörterbuch. Jede Aktion ist mit dem sekundären Controller verknüpft, der für ihre Verarbeitung zuständig ist;
- Zeile 3: [init-session]: Startet eine Sitzung mit Steuerberechnungssimulationen. Diese Aktion gibt den gewünschten Antworttyp an [JSON, XML, HTML];
- Zeile 4: Sobald der Sitzungstyp festgelegt ist, muss sich der Client mithilfe der Aktion [authenticate-user] authentifizieren. Bis zur Authentifizierung des Clients sind alle anderen Aktionen mit Ausnahme von [init-session] untersagt;
- Zeile 5: Nach der Authentifizierung kann der Client mithilfe der Aktion [calculate-tax] eine Reihe von Steuerberechnungen durchführen;
- Zeile 6: Der Client kann jederzeit mit der Aktion [list-simulations] die Liste der von ihm durchgeführten Simulationen anzeigen lassen;
- Zeile 7: Er kann einige davon mit der Aktion [delete-simulation] löschen;
- Zeile 8: Der Client beendet seine Simulationssitzung mit der Aktion [end-session]. Ab diesem Zeitpunkt muss er sich erneut anmelden, wenn er die Anwendung nutzen möchte;
- Zeile 9: In der HTML-Anwendung zeigt die Aktion [display-tax-calculation] das Formular zur Steuerberechnung an;
23.4. Konfiguration der Webanwendung
Die Anwendung wird über die folgende JSON-Datei [config.json] konfiguriert:
{
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/Simulation.php",
"/Entities/Database.php",
"/Entities/TaxAdminData.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Logger.php",
"/Utilities/SendAdminMail.php",
"/Model/InterfaceServerDao.php",
"/Model/ServerDao.php",
"/Model/ServerDaoWithSession.php",
"/Model/InterfaceServerMetier.php",
"/Model/ServerMetier.php",
"/Responses/InterfaceResponse.php",
"/Responses/ParentResponse.php",
"/Responses/JsonResponse.php",
"/Responses/XmlResponse.php",
"/Responses/HtmlResponse.php",
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
"users": [
{
"login": "admin",
"passwd": "admin"
}
],
"adminMail": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "plantage du serveur de calcul d'impôts",
"tls": "FALSE",
"attachments": []
},
"logsFilename": "Logs/logs.txt",
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
Kommentare
- Zeile 2: Name der JSON-Datei, die die Konfiguration für den Datenbankzugriff enthält;
- Zeilen 3–39: Konfiguration der Projektabhängigkeiten. Hier werden alle PHP-Skripte im Projektverzeichnisbaum aufgelistet;
- Zeilen 40–44: der zur Nutzung der Anwendung berechtigte Benutzer;
- Zeilen 46–54: E-Mail-Adresse des Anwendungsadministrators;
- Zeile 55: Pfad zur Protokolldatei;
- Zeilen 56–65: Zuordnungen [Aktion => sekundärer Controller, der für deren Bearbeitung zuständig ist];
- Zeilen 66–70: Zuordnungen [Antworttyp => Antwortklasse, die für das Senden der Antwort an den Client zuständig ist];
- Zeilen 71–75: Zuordnungen [HTML-Ansicht => Tabelle der Statuscodes, die zu dieser Ansicht führen];
- Zeile 76: Die Ansicht [error-view] wird in einer HTML-Sitzung angezeigt, sobald ein schwerwiegender Fehler auftritt:
- Eine JSON- oder XML-Anwendung wird typischerweise über einen programmierten Client abgefragt. Dieser Client übergibt Parameter an den Server, die möglicherweise fehlen oder falsch sind. Die Controller behandeln diese Fälle und geben Fehlercodes an den Client zurück. Alle möglichen Fehlerfälle müssen behandelt werden;
- Bei einer HTML-Anwendung verhält es sich etwas anders. Im Normalbetrieb nutzt die Webanwendung nur eine Teilmenge der möglichen Anwendungsfälle für JSON- und XML-Clients. Nehmen wir ein Beispiel: Die Aktion [calculate-tax] erwartet drei übermittelte Parameter (gesendet über eine POST-Anfrage): [married, children, salary].
- Wenn wir einen JSON-Client haben, der die manuelle Eingabe von URLs erlaubt, können wir die Aktion [calculate-tax] mit einer GET-Anfrage anstelle einer POST-Anfrage anfordern oder mit einer POST-Anfrage, die keine Parameter enthält, obwohl drei erforderlich sind, usw. Der JSON-Server muss all diese Fälle behandeln;
- Bei einer Webanwendung wird die Aktion [calculate-tax] über ein Webformular aufgerufen, bei dem keiner der beiden vorherigen Fälle möglich ist: Die Aktion [calculate-tax] wird über eine POST-Anfrage mit allen drei Parametern [married, children, salary] aufgerufen. Einige dieser Parameter können einen falschen Wert haben, aber sie sind vorhanden. Der Benutzer kann jedoch bestimmte Fehler reproduzieren, indem er URLs selbst in den Browser eingibt. Aus Sicherheitsgründen müssen wir diesen Fall behandeln;
- die [error-view] wird immer dann angezeigt, wenn ein sekundärer Controller einen Statuscode zurückgibt, der mit der Webanwendung nicht kompatibel ist, d. h. einen Statuscode, der nicht in den Zeilen 72–74 der Konfigurationsdatei aufgeführt ist. Wir entscheiden uns aus didaktischen Gründen für diese Lösung. Eine andere Möglichkeit wäre, nichts zu unternehmen und einfach die aktuell im Browser des Clients angezeigte Ansicht erneut darzustellen, sodass der Benutzer den Eindruck gewinnt, der Server reagiere nicht auf seine manuell erstellten URLs;
23.5. Installation von Tools und Bibliotheken
23.5.1. Postman
[Postman] ist das Tool, mit dem wir die verschiedenen URLs unserer Webanwendung abfragen können. Es ermöglicht uns:
- beliebige URLs zu verwenden: Diese werden manuell erstellt;
- den Webserver mit GET, POST, PUT, OPTIONS usw. abfragen;
- GET- oder POST-Parameter anzugeben;
- die HTTP-Header für die Anfrage festzulegen;
- eine Antwort im JSON-, XML- oder HTML-Format empfangen;
- auf die HTTP-Header der Antwort zugreifen. Dadurch erhalten wir Zugriff auf die vollständige HTTP-Antwort des Servers;
Da wir die abzufragenden URLs manuell erstellen, können wir alle möglichen Fehlerszenarien testen und sehen, wie der Server darauf reagiert.
[Postman] ist unter der URL [https://www.getpostman.com/downloads/] verfügbar. Die im Juni 2019 verfügbare Version ist 7.2. Diese Version weist einen Fehler auf: Bei aufeinanderfolgenden Anfragen an den Webserver gibt der [Postman 7.2]-Client die vom Server gesendeten Cookies, insbesondere das Session-Cookie, nicht automatisch zurück. Um die Sitzung aufrechtzuerhalten, müssen Sie das Session-Cookie manuell in die HTTP-Header nachfolgender Anfragen kopieren. Das ist nicht sehr kompliziert, aber auch nicht besonders praktisch. Dieser Fehler war in früheren Versionen nicht vorhanden. Das [Postman]-Team ist sich des Fehlers bewusst und hat ihn in einer Alpha-Version (die möglicherweise instabil ist) namens [Postman Canary] behoben, die unter der URL [https://www.getpostman.com/downloads/canary] verfügbar ist. Dies ist die hier verwendete Version. Wir werden beschreiben, wie man sie installiert. Wenn eine stabile Version [Postman 7.3] oder höher verfügbar ist, können Sie diese herunterladen: Der Fehler dürfte dann behoben sein.
Fahren Sie mit der Installation Ihrer Version von [Postman] fort. Während der Installation werden Sie aufgefordert, ein Konto zu erstellen: Dies ist hier nicht erforderlich. Das [Postman]-Konto dient dazu, verschiedene Geräte zu synchronisieren, sodass die Konfiguration eines Geräts auf einem anderen repliziert wird. All dies ist hier nicht erforderlich.
Nach der Installation zeigt [Postman] die folgende Benutzeroberfläche an:

- In [2-3] können Sie auf die Produkteinstellungen zugreifen;

- in [6] die in diesem Dokument verwendete Version;
- Wenn Sie ein Konto erstellt haben, findet eine Synchronisierung zwischen Ihrem Computer und einem Remote-[Postman]-Server statt. Dies wird durch das sich drehende Rad [7] angezeigt, das erscheint, sobald Sie Änderungen am [Postman]-Projekt vornehmen. Um diese unnötige Synchronisierung zu beenden, melden Sie sich über [8-9] ab;
23.5.2. Die Symfony / Serializer-Bibliothek
Um Objekte in JSON und XML zu serialisieren, verwenden wir die [Symfony / Serializer]-Bibliothek. Sie bietet hier zwei Vorteile:
- Sie ist bei der Serialisierung in JSON oder XML konsistent: Dadurch muss man nicht zwei Bibliotheken mit unterschiedlichen APIs (Application Programming Interfaces) erlernen;
- sie kann Objekte nativ in JSON oder XML serialisieren, selbst wenn deren Attribute privat sind. Erinnern Sie sich daran, dass in JSON die Klasse eines Objekts die Schnittstelle [\JsonSerializable] implementieren musste, um das Objekt zu serialisieren. Das Ergebnis war eine JSON-Zeichenkette in Form eines assoziativen Arrays, dessen Schlüssel die Attribute der Klasse waren. Bei der Deserialisierung dieser JSON-Zeichenkette erhielten wir das ursprüngliche assoziative Array zurück, das dann in ein Objekt der serialisierten Klasse konvertiert werden musste. Mit [Symfony / Serializer] erzeugt die Deserialisierung sofort ein Objekt der serialisierten Klasse. Das ist einfacher;
Die Dokumentation zur [Symfony / Serializer]-Bibliothek ist unter der URL [https://symfony.com/doc/current/components/serializer.html] (Juni 2019) verfügbar.
Um diese Bibliothek zu installieren, öffnen Sie ein Laragon-Terminal (siehe Abschnitt „Links“) und geben Sie den folgenden Befehl ein:

- in [1] den Befehl zur Installation der Bibliothek [symfony/serializer];
- in [2] eine weitere für unser Projekt erforderliche Bibliothek: ermöglicht die Objekt-Serialisierung;

23.6. Anwendungsentitäten

Die Entitäten [BaseEntity, Database, ExceptionImports, TaxAdminData] werden seit Version 08 des Webdienstes verwendet (siehe Abschnitt „Links“).
Die Klasse [Simulation] wird verwendet, um die Elemente einer Steuerberechnungssimulation zu kapseln:
<?php
namespace Application;
class Simulation extends BaseEntity {
// attributes of a tax calculation simulation
protected $marié;
protected $enfants;
protected $salaire;
protected $impôt;
protected $surcôte;
protected $décôte;
protected $réduction;
protected $taux;
// getters
public function getMarié() {
return $this->marié;
}
public function getEnfants() {
return $this->enfants;
}
public function getSalaire() {
return $this->salaire;
}
public function getImpôt() {
return $this->impôt;
}
public function getSurcôte() {
return $this->surcôte;
}
public function getDécôte() {
return $this->décôte;
}
public function getRéduction() {
return $this->réduction;
}
public function getTaux() {
return $this->taux;
}
}
Kommentare
- Zeile 5: Die Klasse [Simulation] erweitert die Klasse [BaseEntity] und erbt daher die folgenden Methoden:
- [setFromArrayOfAttributes($arrayOfAttributes)]: Damit können Sie die Attribute der Klasse initialisieren;
- [__toString]: gibt die JSON-Zeichenkette des Objekts zurück;
- Zeilen 7–14: die Attribute der Simulation;
- Zeilen 16–47: die Getter der Klasse;
23.7. Anwendungshilfsmittel
![]()
Die Klasse [Logger] ermöglicht es Ihnen, Ereignisse in einer Textdatei zu protokollieren. Diese Klasse wird im verlinkten Abschnitt beschrieben.
Die Klasse [SendAdminMail] ermöglicht es Ihnen, eine E-Mail an den Anwendungsadministrator zu senden. Diese Klasse wird im verlinkten Abschnitt beschrieben.
23.8. Die Schichten [business] und [DAO]


Die Klassen und Schnittstellen der [business]- und [DAO]-Schichten sind im Ordner [Model] zusammengefasst. Sie wurden alle bereits in früheren Versionen definiert und verwendet:
ExceptionImports | Die Klasse für Ausnahmen, die von der [DAO]-Schicht ausgelöst werden. Definiert im verlinkten Abschnitt. |
InterfaceServerDao | Schnittstelle, die von der [DAO]-Schicht des Servers implementiert wird. Definiert im verlinkten Abschnitt. |
ServerDao | Implementierung der Schnittstelle [InterfaceServerDao]. Implementiert die [dao]-Schicht des Servers. Definiert im Abschnitt „Link“. |
ServerDaoWithSession | Implementierung der [InterfaceServerDao]-Schnittstelle. Implementiert die [dao]-Schicht des Servers. Definiert im Abschnitt „link“. |
InterfaceServerMetier | Schnittstelle, die von der [business]-Schicht des Servers implementiert wird. Definiert im Abschnitt „link“. |
ServerBusiness | Implementierung der Schnittstelle [InterfaceMetier]. Implementiert die [business]-Schicht des Servers. Definiert im Abschnitt „link“. |
Die derzeit in Entwicklung befindliche Anwendung nutzt in großem Umfang bereits vorgestellte und verwendete Elemente:
- die [business]- und [DAO]-Schichten;
- die Hilfsprogramme [Logger] und [SendAdminMail];
- die Entitäten [ExceptionImports, TaxAdminData, Database];
Wir werden uns auf die [Web-]Ebene der Anwendung konzentrieren:

23.9. Der Haupt-Controller [main.php]
23.9.1. Einführung

- [1-2]: Der Hauptcontroller [main.php] [1] wird durch die Datei [config.json] [2] konfiguriert;
Sehen wir uns die Position des Hauptcontrollers in unserer MVC-Architektur an:

In [1] ist der Hauptcontroller [main.php] das erste Element der MVC-Architektur, das die Anfrage des Clients verarbeitet. Er hat mehrere Aufgaben:
- Zunächst führt er grundlegende Prüfungen durch:
- Existiert seine Konfigurationsdatei und ist sie gültig?
- Laden aller Projektabhängigkeiten. Dies entspricht dem Laden aller Elemente der MVC-Architektur;
- Wurde die angeforderte Aktion angegeben? Wenn ja, ist sie gültig?
- Wenn die angeforderte Aktion gültig ist, wähle [2a] den sekundären Controller aus, der sie verarbeiten soll, und übergebe ihm die benötigten Informationen: die HTTP-Anfrage, die Sitzung und die Anwendungskonfiguration;
- Rufen Sie [2c] die Antwort vom sekundären Controller ab. Wählen Sie je nach dem vom Client angeforderten Anwendungstyp (JSON, XML, HTML) [3a] die Antwort (JsonResponse, XmlResponse, HtmlResponse) aus, die für das Senden der Antwort an den Client zuständig ist, und übergeben Sie ihr alle benötigten Informationen (die HTTP-Anfrage, die Sitzung, die Anwendungskonfiguration, die Antwort vom sekundären Controller);
- Sobald diese Antwort gesendet wurde [3c], geben Sie alle Ressourcen frei, die möglicherweise für die Verarbeitung der Anfrage zugewiesen wurden;
23.9.2. [main.php] - 1
Der Code für den Hauptcontroller [main.php] lautet wie folgt:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// error handling by PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// we retrieve the configuration
$configFilename = "config.json";
$fileContents = \file_get_contents($configFilename);
$erreur = FALSE;
// mistake?
if (!$fileContents) {
// we note the error
$état = 131;
$erreur = TRUE;
$message = "Le fichier de configuration [$configFilename] n'existe pas";
}
if (!$erreur) {
// retrieve the JSON code from the configuration file in an associative array
$config = \json_decode($fileContents, true);
// mistake?
if (!$config) {
// we note the error
$erreur = TRUE;
$état = 132;
$message = "Le fichier de configuration [$configFilename] n'a pu être exploité correctement";
}
}
// mistake?
if ($erreur) {
// preparation of JSON server response
// you can't use the configuration file
// symfony dependencies
require_once "C:/myprograms/laragon-lite/www/vendor/autoload.php";
// response preparation
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// status code
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
// content
$response->setContent(json_encode(["action" => "", "état" => $état, "réponse" => $message], JSON_UNESCAPED_UNICODE));
// shipping
$response->send();
// end
exit;
}
…
Kommentare
- Zeilen 10–12: Der Hauptcontroller verwendet die folgenden Symfony-Objekte:
- [Request]: die aktuell verarbeitete HTTP-Anfrage;
- [Session]: die Sitzung der Webanwendung;
- [Response]: die HTTP-Antwort an den Client;
- Zeile 15: Während der gesamten Entwicklung bleibt diese Zeile auskommentiert: PHP-Fehler werden dann in den an den Client gesendeten Textstrom aufgenommen. Handelt es sich bei dem Client um einen Browser, kann der Benutzer so die vom Server festgestellten Fehler sehen. Dies hilft bei der Fehlersuche;
- Zeile 16: Alle Fehler werden gemeldet (E_ALL), mit Ausnahme von Warnungen (! E_WARNING) und nicht schwerwiegenden Hinweisen (! E_NOTICE). Wenn beispielsweise eine Datei nicht geöffnet werden kann, generiert PHP einen [E_NOTICE]-Fehler. Wenn in Zeile 15 die Fehleranzeige aktiviert ist, erscheint der Fehler beim Öffnen der Datei im Browser des Clients. Das ist in Ordnung, wenn Sie vergessen haben, das Ergebnis des Öffnens der Datei zu testen, aber weniger gut, wenn Sie dies vorhatten: Eine [notice]-Zeile überfrachtet dann die Antwort des Servers an den Client. Während der Entwicklung sollte auch Zeile 16 auskommentiert werden: Sie möchten schließlich keinen Fehler übersehen;
- Zeile 19: Die Konfigurationsdatei wird gelesen;
- Zeilen 22–27: Wenn dieser Lesevorgang fehlschlägt, wird der Fehler protokolliert (Zeile 25), die Anwendung wird in den Status [131] versetzt und eine Fehlermeldung vorbereitet;
- Zeile 30: Die JSON-Zeichenkette aus der Konfigurationsdatei wird dekodiert;
- Zeilen 32–37: Wenn die Dekodierung fehlschlägt, wird der Fehler protokolliert (Zeile 34), die Anwendung wird in den Status [132] versetzt und eine Fehlermeldung vorbereitet;
- Zeilen 40–57: Wenn beim Lesen der Konfigurationsdatei ein Fehler auftritt, können wir nicht fortfahren. Wir bereiten dann eine JSON-Antwort für den Client vor:
- Zeile 44: Da die Konfigurationsdatei nicht gelesen wurde, muss die von [Symfony] benötigte [autoload]-Datei manuell importiert werden;
- Zeilen 46–47: Eine JSON-Antwort wird vorbereitet;
- Zeile 50: Der HTTP-Statuscode der Antwort lautet 500 INTERNAL_SERVER_ERROR;
- Zeile 52: Wir legen den JSON-Inhalt der Antwort fest. Alle von der betrachteten Webanwendung generierten Antworten werden drei Schlüssel enthalten:
- [action]: die vom Client angeforderte Aktion;
- [status]: der Status der Anwendung nach Ausführung dieser Aktion;
- [response]: die Antwort des Webservers;
- Zeile 54: Die JSON-Antwort wird an den Client gesendet;
23.9.3. [Postman] Tests – 1
Wir werden das Verhalten des Servers überprüfen, wenn die Konfigurationsdatei fehlt oder fehlerhaft ist:

Wir werden die verschiedenen Anfragen, die unser [Postman]-Client an den Steuer-Server sendet, in Sammlungen organisieren.
- Erstellen Sie in [1] eine neue Sammlung;
- Geben Sie ihr in [2] einen Namen;
- In [3] ist die Beschreibung optional;

- In den Sammlungen [4] erscheint nun eine Sammlung mit dem Namen [impots-server-tests-version12] [5];
- In [6] können Sie der Sammlung eine neue Anfrage hinzufügen;

- In [7] wird der Abfrage ein Name gegeben;
- in [8] ist die Beschreibung optional;

- In [9–11] wird die Anfrage zur Sammlung hinzugefügt;
- in [12] wählen Sie den Anfragetyp aus; hier eine [GET]-Anfrage. In [19] die verschiedenen verfügbaren Anfragetypen;
- in [13] geben Sie hier die URL des Servers ein;
- in [14] geben Sie hier die zur URL hinzugefügten Parameter ein; dies sind GET-Parameter. Der Vorteil der Eingabe hier statt direkt in der URL besteht darin, dass [Postman] diese URL-kodiert. Wenn Sie sie direkt in die URL eingeben, müssen Sie sie selbst URL-kodieren;
- in [15] wird [Authorization] verwendet, um den Benutzer zu definieren, der sich anmeldet. Wir werden diese Option nicht benötigen;
- in [16] die HTTP-Header, die der Anfrage beigefügt werden. Eine Reihe von Headern wird automatisch in die Anfrage aufgenommen. Hier können Sie neue hinzufügen;
- In [17] bezieht sich [Body] auf die Parameter einer [POST]-Operation. Wir müssen diese Option verwenden;
Wir führen den folgenden Test durch:
- In [main.php] geben wir an, dass die Konfigurationsdatei [config2.json] ist, die jedoch nicht existiert:

- Zeile 16 des Codes muss auskommentiert werden;
- Zeile 18: der Fehler bezüglich des Namens der Konfigurationsdatei;
Öffnen wir [Postman] [13, 20], geben wir die URL des Webservers für die Steuerberechnung ein und führen sie aus [21]:

Die vom Server zurückgegebene Antwort (Laragon muss natürlich laufen) lautet wie folgt:

- in [22] gab der Server einen HTTP-Code [500 Internal Server Error] zurück;
- in [23] bezieht sich [Body] auf den Hauptteil der Antwort, d. h. das vom Server hinter den HTTP-Headern gesendete Dokument [28];
- in [26] sehen wir, dass [Postman] eine JSON-Antwort erhalten hat;
- in [27] die formatierte JSON-Antwort;
- in [28] die rohe JSON-Antwort ohne Formatierung;
- in [29] wird der [Preview]-Modus verwendet, wenn die Antwort HTML ist. Der [Preview]-Modus zeigt dann die empfangene Seite an;
- In [30] die JSON-Antwort vom Server. Dies ist tatsächlich die Antwort, die wir erwartet haben;
In [25] lauten die in der Antwort des Servers gesendeten HTTP-Header wie folgt:

- in [32] der JSON-Typ der Antwort;
Dieser erste Test hat uns gezeigt, dass wir:
- jede Art von Anfrage an den getesteten Server senden können;
- die GET- oder POST-Parameter festlegen können;
- die gesamte Antwort erhalten: HTTP-Header und das auf diese Header folgende Dokument [Body];
Führen wir nun einen zweiten Test durch:

- In [1-3] ist die Datei [config3.json] eine syntaktisch fehlerhafte JSON-Datei;
- In [4] ist [main.php] so konfiguriert, dass es [config3.json] verwendet;
Wir fügen in [Postman] eine neue Anfrage hinzu:

- [1-3], klicken Sie mit der rechten Maustaste auf [2] und wählen Sie die Option [Duplizieren], um die Anfrage [2] zu duplizieren;
- In [4] hat die neue Anfrage einen Standardnamen, den wir in [5] ändern;

- In [6] die umbenannte Anfrage;
- In [9-10] senden wir denselben GET-Request wie zuvor;

- in [11] die JSON-Antwort des Servers;
Hier haben wir gezeigt, wie die verschiedenen Aktionen des Webdienstes zur Steuerberechnung getestet werden.
23.9.4. [main.php] – 2
Wir setzen unsere Untersuchung des Codes des Haupt-Controllers [main.php] fort:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// error handling by PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// we retrieve the configuration
$configFilename = "config.json";
…
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require_once "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require_once "$dependency";
}
// log file creation
try {
$logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
// log file could not be created - internal server error
$état = 133;
(new JsonResponse())->send(
NULL, NULL, $config,
Response::HTTP_INTERNAL_SERVER_ERROR,
["action" => "non déterminée", "état" => $état, "réponse" => "Le fichier de logs [{$config['logsFilename']}] n'a pu être créé"],
[]);
// completed
exit;
}
Kommentare
- Zeile 18: Wir haben nun eine Konfigurationsdatei [config.json], die vorhanden und syntaktisch korrekt ist. Wir sollten auch überprüfen, ob die erwarteten Schlüssel in dieser Datei vorhanden sind. Wir gehen davon aus, dass dies Teil des normalen Debugging-Prozesses des Entwicklers ist. Wir hätten dieselbe Argumentation auch auf die beiden vorherigen Fehler anwenden können;
- Zeilen 20–28: Wir binden alle für das Webprojekt erforderlichen Abhängigkeiten ein. Diesen Code sind wir bereits mehrmals begegnet;
- Zeilen 31–43: Wir versuchen, das [Logger]-Objekt zu erstellen, mit dem wir Ereignisse in die Datei [$config['logsFilename']] protokollieren können. Diese Erstellung kann fehlschlagen;
- Zeilen 33–43: Behandlung des Fehlers beim Erstellen des [Logger]-Objekts;
- Zeile 35: Wir legen eine Statusnummer fest;
- Zeilen 36–40: Wir senden eine JSON-Antwort;
- Zeile 42: Wir beenden das Skript;
Alle an den Client gesendeten Antworten implementieren die folgende [InterfaceResponse]-Schnittstelle:

Der Code für die Schnittstelle [InterfaceResponse] lautet wie folgt:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session: the web application session
// array $config: application configuration
// int statusCode: HTTP response status code
// array $content: server response
// array $headers: HTTP headers to be added to the response
// Logger $logger: the logger for writing logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
- Zeilen 19–27: Die Schnittstelle [InterfaceResponse] verfügt über eine einzige Methode [send], um die Antwort an den Client zu senden;
- Zeilen 11–17: die Bedeutung der verschiedenen Parameter der Methode [send];
- Zeilen 23–25: Die Parameter [$statusCode, $content, $headers] sind Teil der Standardausgabe der sekundären Controller der Anwendung. Die Antwort benötigt jedoch möglicherweise zusätzliche Informationen. Daher stellen wir ihr die ersten drei Parameter (Zeilen 20–22) zur Verfügung, die ihr Zugriff auf alle Informationen bezüglich der Anfrage, der Sitzung und der Konfiguration gewähren;
- Zeile 26: Die Antwort benötigt den [Logger], da sie die an den Client gesendete Antwort protokolliert;
Die Klasse [JsonResponse] implementiert die Schnittstelle [InterfaceResponse] wie folgt:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session: the web application session
// array $config: application configuration
// int statusCode: HTTP response status code
// array $content: server response
// array $headers: HTTP headers to be added to the response
// Logger $logger: the logger for writing logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// symfony serializer preparation
$serializer = new Serializer(
[
// required for object serialization
new ObjectNormalizer()],
// encoder jSON
// for options, make OU between the different options
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// serialization jSON
$json = $serializer->serialize($content, 'json');
// headers
$headers = array_merge($headers, ["content-type" => "application/json"]);
// sending reply
parent::sendResponse($statusCode, $json, $headers);
// log
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
Kommentare
- Zeile 13: Die Klasse implementiert die Schnittstelle [InterfaceResponse];
- Zeile 13: Die Klasse erweitert die Klasse [ParentResponse]. Alle [Response]-Typen erweitern diese Klasse. Es ist diese übergeordnete Klasse, die die Antwort an den Client sendet (Zeile 46). Da dieser Code allen [Response]-Typen gemeinsam war, wurde er in eine übergeordnete Klasse ausgelagert;
- Zeilen 33–40: Instanziierung des [Symfony]-Serialisierers, der die Serverantwort [$content] in eine JSON-Zeichenkette umwandelt (Zeile 42);
- Zeilen 34–36: Der erste Parameter des [Serializer]-Konstruktors ist ein Array. Darin platzieren wir eine Instanz der Klasse [ObjectNormalizer], die für die Objektserialisierung benötigt wird. In dieser Anwendung geschieht dies mit einer Liste von Simulationen, wobei jede Simulation eine Instanz der Klasse [Simulation] ist;
- Zeile 39: Der zweite Parameter des [Serializer]-Konstruktors ist ebenfalls ein Array: Er enthält alle bei der Serialisierung verwendeten Encoder (XML, JSON, CSV usw.);
- Zeile 39: Hier wird es nur einen Encoder vom Typ [JsonEncoder] geben. Der parameterlose Konstruktor hätte ausgereicht. Hier haben wir dem Konstruktor einen [JsonEncode]-Parameter übergeben, ausschließlich um JSON-Kodierungsoptionen zu übergeben;
- Zeile 39: Der Konstruktorparameter [JsonEncode] ist ein Array von Optionen. Hier verwenden wir die Option [JSON_UNESCAPED_UNICODE], um festzulegen, dass die UTF-8-Zeichen in der JSON-Zeichenkette nativ dargestellt und nicht „escaped“ werden sollen;
- Zeile 42: Der Hauptteil der HTTP-Antwort wird mithilfe des zuvor genannten Serializers in JSON serialisiert;
- Zeile 44: Wir fügen den HTTP-Header hinzu, der dem Client mitteilt, dass wir JSON senden;
- Zeile 46: Die übergeordnete Klasse wird aufgefordert, die Antwort an den Client zu senden;
- Zeilen 48–50: Wir protokollieren die JSON-Antwort;
Der Code für die übergeordnete Klasse [ParentResponse] lautet wie folgt:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode: HTTP response status code
// string $content: the body of the response to be sent
// depending on the case, this is a jSON, XML, HTML string
// array $headers: HTTP headers to be added to the response
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// preparing the server's text response
$response = new Response();
$response->setCharset("utf-8");
// status code
$response->setStatusCode($statusCode);
// headers
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// we send the answer
$response->setContent($content);
$response->send();
}
}
Kommentare
- Zeilen 10–13: Die Bedeutung der drei Parameter der [send]-Methode;
- Zeile 17: Beachten Sie, dass der Antworttext vom Typ [string] ist und somit versandbereit ist (Zeile 30);
- Zeile 22: Die Antwort enthält UTF-8-Zeichen;
- Zeile 24: HTTP-Statuscode der Antwort;
- Zeilen 26–28: Hinzufügen der vom aufrufenden Code bereitgestellten HTTP-Header;
- Zeilen 30–31: Senden der Antwort an den Client;
Wir haben den gesamten Lebenszyklus einer JSON-Antwort detailliert beschrieben. Wir werden später nicht noch einmal darauf zurückkommen. Sie müssen sich lediglich die Signatur der Schnittstelle [InterfaceResponse] merken:
interface InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session: the web application session
// array $config: application configuration
// int statusCode: HTTP response status code
// array $content: server response
// array $headers: HTTP headers to be added to the response
// Logger $logger: the logger for writing logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
Der Haupt-Controller [main.php] muss diese Signatur jedes Mal einhalten, wenn er die Übermittlung einer Antwort an den Client anfordert.
23.9.5. Tests [Postman] – 2
Wir ändern die Datei [config.json] wie folgt:

- In [1] geben wir an, dass die Protokolldatei [Logs] ist, bei der es sich um einen Ordner [2] handelt. Die Erstellung der Datei [Logs] sollte daher fehlschlagen;
Wir erstellen eine neue [Postman]-Anfrage [3] mit dem Namen [error-133]:

- [2-4]: Wir definieren dieselbe Anfrage wie in den beiden vorherigen Tests;
- [5-7]: Wir erhalten erfolgreich die erwartete JSON-Antwort;
23.9.6. [main.php] – 3
Schauen wir uns nun den Haupt-Controller [main.php] genauer an:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
// error handling by PHP
…
// log file creation
…
// 1st log
$logger->write("\n---nouvelle requête\n");
// current query
$request = Request::createFromGlobals();
// session
$session = new Session();
$session->start();
// error list
$erreurs = [];
$erreur = FALSE;
// we manage the requested action
if (!$request->query->has("action")) {
$erreurs[] = "paramètre [action] manquant";
$erreur = TRUE;
$état = 101;
$action = "";
} else {
// memorize the action
$action = strtolower($request->query->get("action"));
}
// we log the action
$logger->write("action [$action] demandée\n");
// does the action exist?
if (!$erreur && !array_key_exists($action, $config["actions"])) {
$erreurs[] = "action [$action] invalide";
$erreur = TRUE;
$état = 102;
}
// the session type must be known before performing certain actions
if (!$erreur && !$session->has("type") && $action !== "init-session") {
$erreurs[] = "pas de session en cours. Commencer par action [init-session]";
$erreur = TRUE;
$état = 103;
}
// some actions require authentication
if (!$erreur && !$session->has("user") && $action !== "authentifier-utilisateur" && $action !== "init-session") {
$erreurs[] = "action demandée par utilisateur non authentifié";
$erreur = TRUE;
$état = 104;
}
// mistakes?
if ($erreurs) {
// we prepare the answer without sending it
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// execute the action using its controller
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- we send the answer
// cas de l'erreur fatale HTTP_INTERNAL_SERVER_ERROR
// send an e-mail to the administrator if you can
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// the answer depends on the session type
if ($session->has("type")) {
// the session type is in the session
$type = $session->get("type");
} else {
// if no type in session, then the default response is jSON
$type = "json";
}
// we add the keys [action, state] to the controller response
$content = ["action" => $action, "état" => $état] + $content;
// instantiate the [Response] object responsible for sending the response to the client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// the reply has been sent - resources are released
$logger->close();
exit;
Kommentare
- Sobald die ersten Prüfungen durchgeführt wurden und der Hauptcontroller weiß, dass er fortfahren kann, konzentriert er sich auf die von ihm angeforderte Aktion: Er muss bestimmte Bedingungen erfüllen;
- Zeile 21: Wir protokollieren, dass wir eine neue Anfrage haben. Das konnten wir zuvor nicht tun, da wir nicht sicher waren, ob wir eine gültige Protokolldatei hatten;
- Zeile 23: Wir kapseln alle Informationen aus der Anfrage des Clients in das Symfony-Objekt [Request];
- Zeile 26: Wir starten eine neue Sitzung oder rufen die vorhandene Sitzung ab, falls eine vorhanden ist;
- Zeile 27: Die Sitzung wird aktiviert;
- Zeile 29: ein Array mit Fehlermeldungen;
- Zeile 30: Ein boolescher Wert, der uns während der Ausführung der Tests mitteilt, ob ein Fehler aufgetreten ist oder nicht;
- Zeile 32: Der Parameter [action] muss in der URL in der Form [main.php?action=someAction] enthalten sein. Der Parameter [action] wird dann in die Parameter von [$request→query] aufgenommen;
- Zeilen 33–36: Fall, in dem der Parameter [action] in der URL fehlt. Der Fehler wird protokolliert und ihm wird der Statuscode [101] zugewiesen;
- Zeile 39: Wenn der Parameter [action] in der URL vorhanden ist, wird er gespeichert;
- Zeile 42: Der Aktionstyp wird protokolliert;
- Zeilen 45–49: Wenn der Parameter [action] vorhanden ist, muss er gültig sein. Alle autorisierten Aktionen sind im assoziativen Array [$config["actions"]] definiert;
- Zeilen 46–48: Wenn die Aktion ungültig ist, wird der Fehler protokolliert und der Status [102] zugewiesen;
- Zeilen 52–56: Wir haben eine gültige Aktion. Sie muss noch weitere Bedingungen erfüllen. Die Webanwendung bietet drei Antworttypen (JSON, XML, HTML). Dieser Typ wird durch die Aktion [init-session] festgelegt. Diese Aktion speichert den Sitzungstyp im Schlüssel [type];
- Zeile 52: Außerhalb der Aktion [init-session] muss jede andere Aktion mit einem [type]-Schlüssel in der Sitzung erfolgen;
- Zeilen 53–55: Ist dies nicht der Fall, wird der Fehler protokolliert und der Status [103] zugewiesen;
- Zeilen 58–63: Außerhalb der Aktionen [init-session] und [authenticate-user] müssen alle anderen Aktionen nach der Authentifizierung erfolgen. Dies geschieht mithilfe der Aktion [authenticate-user], die bei erfolgreicher Authentifizierung einen [user]-Schlüssel in die Sitzung einfügt;
- Zeile 59: Wenn die Aktion weder [init-session] noch [authenticate-user] ist und der Schlüssel [user] nicht in der Sitzung vorhanden ist, tritt ein Fehler auf;
- Zeilen 60–62: Der Fehler wird protokolliert und erhält den Status [104];
- Zeilen 66–71: Wir prüfen, ob das Array [$errors] nicht leer ist. Ist dies der Fall, ist die angeforderte Aktion oder ihr Ausführungskontext fehlerhaft;
- Zeilen 68–70: Die Antwort für den Client wird vorbereitet, aber noch nicht gesendet;
- Zeile 68: HTTP-Statuscode;
- Zeile 69: Antworttext;
- Zeile 70: an die Antwort anzuhängende Header; hier keine;
- Zeile 73: Wir haben eine gültige Aktion. Wir werden ihren (sekundären) Controller bitten, sie zu verarbeiten;
- Zeile 74: Wir bilden den Namen der auszuführenden Controller-Klasse. [__NAMESPACE__] ist der Namespace, in dem wir uns befinden, hier [Application] (Zeile 7);
- die Namen der sekundären Controller-Klassen befinden sich in der Datei [config.json]:
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
Jede Aktion entspricht einem sekundären Controller. Wenn die Aktion [authenticate-user] lautet, hat die Variable [$controller] in Zeile 74 daher den Wert [Application/AuthentifierUtilisateurController];
- Zeile 75: Wir protokollieren den Namen des sekundären Controllers zur Überprüfung während der Entwicklung;
- Zeile 76: Der sekundäre Controller wird ausgeführt. Wir werden etwas später auf sekundäre Controller zurückkommen;
- Zeile 76: Alle sekundären Controller geben denselben Ergebnistyp zurück, nämlich ein Array:
- Das erste Element des Arrays [$statusCode] ist der HTTP-Statuscode der zu sendenden Antwort;
- das zweite Element [$state] ist der Zustand der Anwendung nach Ausführung des Controllers;
- das dritte Element [$content] ist ein assoziatives Array mit einem einzigen Schlüssel [response], der den Hauptteil der an den Client zu sendenden Antwort darstellt;
- das vierte Element [$headers] ist ein Array von HTTP-Headern, die der an den Client gesendeten Antwort hinzugefügt werden sollen;
- Zeile 79: Wir kommen hier an:
- entweder weil ein Fehler aufgetreten ist (Zeilen 68–70);
- oder nach der Ausführung eines Controllers (Zeilen 72–76);
- in beiden Fällen sind die Elemente [$statusCode, $status, $content, $headers], die zum Erstellen der Antwort an den Client benötigt werden, bekannt;
- Zeilen 82–87: Behandeln den speziellen Fall des Statuscodes [500 Internal Server Error]. Wenn ein Controller diesen Statuscode gesetzt hat, bedeutet dies, dass die Anwendung nicht funktionieren kann. Dies ist beispielsweise bei Steuerberechnungen der Fall, wenn das verwendete DBMS nicht gestartet wurde oder nicht mehr reagiert. Es wird dann eine E-Mail an den Anwendungsadministrator gesendet, um ihn zu benachrichtigen. Wir werden diesen Code nicht speziell kommentieren. Die Verwendung der Klasse [SendAdminMail] wurde bereits vorgestellt (siehe verlinkten Abschnitt);
- Zeilen 89–95: Wir ermitteln den Typ [jSON, XML, HTML] der Webanwendung. Wenn die Aktion [init-session] erfolgreich ausgeführt wurde, ist dieser Typ in der Sitzung unter dem Schlüssel [type] gespeichert (Zeile 91). Ist dies nicht der Fall, legen wir willkürlich einen Typ für die Antwort fest, nämlich den JSON-Typ (Zeile 94);
- Zeile 97: [$content] ist ein Array mit einem einzigen Schlüssel [response] und einem einzigen Wert, dem Hauptteil der an den Client zu sendenden Antwort. Die Schlüssel [action] und [status] werden hinzugefügt. Der Schlüssel [action] erleichtert die Nachverfolgung der Protokolle in der Datei [logs.txt]. Der Schlüssel [status] erfüllt zwei Zwecke:
- Er ermöglicht es JSON- und XML-Clients, den Zustand zu erkennen, in den die ausgeführte Aktion die Webanwendung versetzt hat;
- im Falle einer HTML-Antwort ermöglicht er uns die Auswahl der HTML-Ansicht, die an den Browser des Clients gesendet werden soll;
- Zeile 99: Wir wählen den Klassentyp [Response] aus, der ausgeführt werden soll, um die Antwort an den Client zu senden;
Wir haben die Klasse [JsonResponse] bereits im vorigen Abschnitt vorgestellt. Sie implementiert die Schnittstelle [InterfaceResponse] und erweitert die Klasse [ParentResponse]. Dies gilt auch für die beiden anderen Klassen, [XmlResponse] und [HtmlResponse].
Die Antworten sind im Ordner [Responses] zusammengefasst:

Alle diese Klassen implementieren die Schnittstelle [InterfaceResponse], die ebenfalls im verlinkten Abschnitt vorgestellt wird:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session: the web application session
// array $config: application configuration
// int statusCode: HTTP response status code
// array $content: server response
// array $headers: HTTP headers to be added to the response
// Logger $logger: the logger for writing logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
Diese Schnittstelle verfügt über eine einzige Methode, [send], die für das Senden der Antwort an den Client zuständig ist. Diese Methode hat die 7 Parameter, die in den Zeilen 11–17 beschrieben sind. Alle Klassen und Schnittstellen im Ordner [Responses] befinden sich im Namespace [Application] (Zeile 3).
Kehren wir zum Code in [main.php] zurück:
…
// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
- Zeile 5: Wir instanziieren die [Response]-Klasse, die dem Anwendungstyp entspricht. Diese Klassen sind in der Datei [config.json] wie folgt definiert:
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
- Zeile 5: Dem Klassennamen wird sein Namespace vorangestellt;
- Zeile 6: Die Klasse [Response] wird instanziiert und ihre Methode [send] mit den 7 erwarteten Parametern aufgerufen. Diese Parameter entsprechen denen der Schnittstelle [InterfaceResponse], die alle Antwortklassen implementieren. Dadurch wird die Antwort an den Client gesendet;
- Zeile 9: Die Protokolldatei wird geschlossen;
- Zeile 10: Der Hauptcontroller hat seine Arbeit beendet;
23.9.7. [Postman]-Tests – 3
Wir werden verschiedene Fehlerfälle für den Parameter [action] der URL testen.

- in [1]:
- [error-101]: Fall, in dem der Parameter [action] in der URL fehlt;
- [error-102]: Fall, in dem der Parameter [action] in der URL vorhanden ist, aber nicht erkannt wird;
- [Fehler-103]: Fall, in dem der Parameter [action] in der URL vorhanden ist und erkannt wird, der erwartete Antworttyp [json, xml, html] jedoch nicht definiert wurde;
Jede Anfrage wird ausgeführt. Wir stellen die Ergebnisse direkt dar:
Oben:
- in [2-4] eine Anfrage ohne den Parameter [action] in der URL [4];
- in [5-7] das JSON-Ergebnis;

Oben:
- in [5–9] eine Anfrage mit einem ungültigen [action]-Parameter;
- in [10-13], die JSON-Antwort;

Oben:
- in [14-19], eine erkannte Aktion, deren Typ (json, xml, html) jedoch noch nicht angegeben wurde;
- in [20-23], die JSON-Antwort des Servers;
23.10. Sekundäre Controller
Jede Aktion wird von einem der Controller im Ordner [Controllers] ausgeführt:


In der allgemeinen Architektur der oben genannten Anwendung befinden sich die sekundären Controller in [2a].
Jeder Controller implementiert die folgende [InterfaceController]-Schnittstelle:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceController {
// $config is the application configuration
// traitement d'une requête Request
// session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos=NULL): array;
}
Kommentare
- Alle sekundären Controller werden über die Methode [execute] in Zeile 17 ausgeführt. Wir übergeben die bekannten Informationen vom Haupt-Controller an diese Methode:
- Zeile 18: [array $config], das die Anwendungskonfiguration enthält;
- Zeile 19: [Request $request], das ist die derzeit verarbeitete HTTP-Anfrage;
- Zeile 20: [Session $session], die aktuelle Sitzung der Webanwendung;
- Zeile 21: [array $infos=NULL], ein zusätzliches Array mit Informationen für den Controller, falls die ersten drei Parameter der Methode nicht ausreichen. In dieser Anwendung wurde dieser Parameter noch nie verwendet. Er ist als Vorsichtsmaßnahme enthalten;
- Zeile 21: Die Methode [execute] gibt das Array [$statusCode, $status, $content, $headers] zurück
- [int $statusCode]: der HTTP-Antwortstatuscode;
- [int $state]: der Status der Anwendung am Ende der Ausführung;
- [array $content]: ein assoziatives Array [response=>result], wobei [result] einen beliebigen Typ haben kann: Dies ist das vom Controller erzeugte Ergebnis, das nach der Serialisierung als String an den Client gesendet wird;
- [array $headers]: die Liste der HTTP-Header, die in die HTTP-Antwort des Servers aufgenommen werden sollen;
Jeder sekundäre Controller wird durch den folgenden Code im Hauptcontroller aufgerufen:
// on exécute l'action à l'aide de son contrôleur
$controller = __NAMESPACE__ . $config["actions"][$action];
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
In Zeile 3 sehen wir, dass der vierte Parameter [array $infos=NULL] der Methode [execute] nicht verwendet wird.
23.11. Aktionen
Wir werden nun die verschiedenen möglichen Aktionen des Webdienstes betrachten:
Aktion | Rolle | Ausführungskontext |
init-session | Wird verwendet, um den Typ (json, xml, html) der gewünschten Antworten festzulegen | GET-Anfrage main.php?action=init-session&type=x kann jederzeit gesendet werden |
authenticate-user | Autorisiert oder verweigert die Anmeldung eines Benutzers | POST-Anfrage main.php?action=authenticate-user Die Anfrage muss zwei übermittelte Parameter enthalten [user, password] Kann nur gesendet werden, wenn der Sitzungstyp (json, xml, html) bekannt ist |
calculate-tax | Führt eine Simulation der Steuerberechnung durch | POST-Anfrage an main.php?action=calculate-tax Die Anfrage muss drei übermittelte Parameter enthalten [married, children, salary] Kann nur ausgeführt werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist |
list-simulations | Anfrage zum Anzeigen der Liste der seit Beginn der Sitzung durchgeführten Simulationen | GET-Anfrage an main.php?action=list-simulations Die Anfrage akzeptiert keine weiteren Parameter Kann nur ausgegeben werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist |
delete-simulation | Löscht eine Simulation aus der Liste der Simulationen | GET-Anfrage main.php?action=list-simulations&number=x Die Anfrage akzeptiert keine weiteren Parameter Kann nur ausgeführt werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist |
end-session | Beendet die Simulationssitzung. | Technisch gesehen wird die alte Web-Sitzung gelöscht und eine neue Sitzung erstellt Kann nur ausgegeben werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist |
Alle sekundären Controller verfahren auf die gleiche Weise:
- Sie überprüfen ihre Parameter. Diese befinden sich im Objekt [Request→query] für Parameter, die in der URL vorhanden sind, und im Objekt [Request→request] für diejenigen, die gesendet wurden (POST-Anfrage);
- Ein Controller ähnelt einer Funktion oder Methode, die die Gültigkeit ihrer Parameter überprüft. Für den Controller ist es jedoch etwas komplizierter:
- Die erwarteten Parameter können fehlen;
- die erwarteten Parameter sind alle Zeichenfolgen, während eine Funktion den Typ ihrer Parameter angeben kann. Wenn der erwartete Parameter eine Zahl ist, müssen Sie überprüfen, ob die Parameterzeichenfolge tatsächlich die einer Zahl ist;
- Sobald überprüft wurde, dass die erwarteten Parameter vorhanden und syntaktisch korrekt sind, müssen Sie sicherstellen, dass sie im aktuellen Ausführungskontext gültig sind. Dieser Kontext ist in der Sitzung vorhanden. Das Authentifizierungsbeispiel ist ein Beispiel für einen Ausführungskontext. Bestimmte Aktionen sollten erst verarbeitet werden, wenn der Client authentifiziert wurde. Im Allgemeinen gibt ein Schlüssel in der Sitzung an, ob diese Authentifizierung stattgefunden hat oder nicht;
- Sobald die vorangegangenen Prüfungen abgeschlossen sind, kann der sekundäre Controller fortfahren. Dieser Prozess der Parameterüberprüfung ist sehr wichtig. Wir können nicht akzeptieren, dass ein Client uns zu irgendeinem Zeitpunkt während des Lebenszyklus der Anwendung beliebige Daten sendet. Wir müssen die volle Kontrolle über den Lebenszyklus der Anwendung behalten;
- Sobald die Arbeit erledigt ist, gibt der sekundäre Controller das Array [$statusCode, $state, $content, $headers] zurück, das der Hauptcontroller erwartet, der ihn aufgerufen hat;
Wir werden nun die verschiedenen Controller betrachten – oder, mit anderen Worten, die verschiedenen Aktionen, die den Lebenszyklus der Webanwendung steuern.
23.11.1. Die Aktion [init-session]
Die Aktion [init-session] wird vom folgenden [InitSessionController] verarbeitet:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
class InitSessionController implements InterfaceController {
// $config is the application configuration
// traitement d'une requête Request
// session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// you must have a GET and a single parameter other than [action]
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
if ($erreur) {
$état = 701;
$message = "méthode GET exigée avec paramètres [action, type] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// retrieve the GET parameters
$erreur = FALSE;
// type
if (!$request->query->has("type")) {
$erreur = TRUE;
$état = 702;
$message = "paramètre [type] manquant";
} else {
$type = strtolower($request->query->get("type"));
}
// type verification
if (!$erreur && !array_key_exists($type, $config["types"])) {
$erreur = TRUE;
$état = 703;
$message = "paramètre type [$type] invalide";
}
// mistake?
if ($erreur) {
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// put the session type in the session
$session->set("type", $type);
// message of success
$message = "session démarrée avec type [$type]";
$état = 700;
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
Kommentare
- Wir erwarten eine Anfrage vom Typ [GET main.php?action=init-session&type=xxx]
- Zeilen 25–26: Wir prüfen, ob es sich bei der Anfrage um eine GET-Anfrage mit zwei Parametern in der URL handelt;
- Zeilen 27–31: Ist dies nicht der Fall, protokollieren wir den Fehler und senden eine Antwort [$statusCode, $status, $content, $headers] an den Hauptcontroller;
- Zeilen 35–39: Wir prüfen, ob der Parameter [type] in der URL vorhanden ist. Ist dies nicht der Fall, protokollieren wir den Fehler;
- Zeile 40: Protokollieren des Sitzungstyps;
- Zeilen 43–47: Wir prüfen, ob der Sitzungstyp einer der Begriffe (json, xml, html) ist. Ist dies nicht der Fall, protokollieren wir den Fehler;
- Zeilen 49–51: Wenn ein Fehler aufgetreten ist, wird ein Ergebnis [$statusCode, $status, $content, $headers] an den Hauptcontroller gesendet;
- Zeile 53: Der Sitzungstyp wird in der Sitzung der Webanwendung gespeichert;
- Zeilen 55–57: Der Controller hat seine Arbeit beendet. Eine Erfolgsmeldung [$statusCode, $status, $content, $headers] wird an den Hauptcontroller gesendet;
Schauen wir uns an, was der Hauptcontroller mit den Antworten der sekundären Controller macht:
// erreurs ?
if ($erreurs) {
// on prépare la réponse sans l'envoyer
$statusCode = Response::HTTP_BAD_REQUEST;
$content = ["réponse" => $erreurs];
$headers = [];
} else {
// ---------------------------
// on exécute l'action à l'aide de son contrôleur
$controller = __NAMESPACE__ . $config["actions"][$action];
$logger->write("contrôleur : $controller\n");
list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
// --------------------- on envoie la réponse
// cas de l'erreur fatale HTTP_INTERNAL_SERVER_ERROR
// on envoie un mail à l'administrateur si on peut
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
$infosMail = $config['adminMail'];
$infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
$sendAdminMail = new SendAdminMail($infosMail, $logger);
$sendAdminMail->send();
}
// la réponse dépend du type de la session
if ($session->has("type")) {
// le type de session est dans la session
$type = $session->get("type");
} else {
// si pas de type dans session, alors par défaut ce sera une réponse en jSON
$type = "json";
}
// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
- Zeile 12: Der Hauptcontroller ruft das Ergebnis vom sekundären Controller ab;
- Zeilen 35–36: Nach einigen Überprüfungen sendet er die Antwort, indem er je nach Typ (JSON, XML, HTML) der aktuellen Sitzung eine der Klassen [JsonResponse, XmlResponse, HtmlResponse] instanziiert;
Als Nächstes führen wir [Postman]-Tests im Rahmen einer Simulationssitzung unter Verwendung des [json]-Typs durch. Die Funktionalität der Klasse [JsonResponse] wurde im verlinkten Abschnitt vorgestellt.
23.11.2. [Postman]-Tests

Oben:
- in [2], drei neue Tests;
- in [3–7] die Aktion [init-session] ohne den Parameter [type];
- in [8–11] die JSON-Antwort des Servers;

Oben:
- in [1-7], die [init-session]-Aktion mit einem falschen [type]-Parameter;
- in [8–11] die JSON-Antwort des Servers;

Oben:
- in [1-8] die Aktion [init-session] mit dem Typ JSON;
- in [9-12], die JSON-Antwort des Servers;
23.11.3. Die Aktion [authenticate-user]
Die Aktion [authenticate-user] wird vom folgenden Controller [AuthentifierUtilisateurController] ausgeführt:
<?php
namespace Application;
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class AuthentifierUtilisateurController implements InterfaceController {
// $config is the application configuration
// traitement d'une requête Request
// session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// you must have a POST and a single GET parameter
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
$état = 201;
$message = "méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user,password]";
// return the result to the main controller
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// retrieve POST parameters
$erreurs = [];
// user
$état = 210;
if (!$request->request->has("user")) {
$état += 2;
$erreurs[] = "paramètre [user] manquant";
} else {
$user = $request->request->get("user");
}
// password
if (!$request->request->has("password")) {
$état += 4;
$erreurs[] = "paramètre [password] manquant";
} else {
$password = trim($request->request->get("password"));
}
// mistake?
if ($erreurs) {
// return the result to the main controller
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// verification of user credentials
// does the user exist?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($user === $users[$i]["login"] && $users[$i]["passwd"] === $password);
$i++;
}
// found?
if (!$trouvé) {
// error message
$message = "Echec de l'authentification [$user, $password]";
$état = 221;
// return the result to the main controller
return [Response::HTTP_UNAUTHORIZED, $état, ["réponse" => $message], []];
} else {
// we note in the session that we have authenticated the user
$session->set("user", TRUE);
// message of success
$message = "Authentification réussie [$user, $password]";
$état = 200;
// return the result to the main controller
return [Response::HTTP_OK, $état, ["réponse" => $message], []];
}
}
}
Kommentare
- Wir erwarten eine [POST main.php?action=authentifier-utilisateur]-Anfrage mit zwei Parametern [user, password];
- Zeilen 24–25: Wir überprüfen, ob wir eine POST-Anfrage mit einem einzigen Parameter in der URL haben;
- Zeilen 26–31: Wenn ein Fehler auftritt, protokollieren wir ihn und geben ein Ergebnis [$statusCode, $status, $content, $headers] an den Hauptcontroller zurück;
- Zeilen 36–39: Wir prüfen, ob der Parameter [user] in den übermittelten Werten vorhanden ist. Ist dies nicht der Fall, protokollieren wir den Fehler;
- Zeilen 43–45: Wir prüfen, ob der Parameter [password] in den übermittelten Werten vorhanden ist. Ist dies nicht der Fall, protokollieren wir den Fehler;
- Zeilen 50–53: Wenn einer der übermittelten Werte fehlt, wird ein Ergebnis [$statusCode, $status, $content, $headers] an den Hauptcontroller zurückgegeben;
- Zeilen 56–62: Wir prüfen, ob das abgerufene Paar [$user,$password] im Array [$config[‘users’]] in der Konfigurationsdatei vorhanden ist;
- Zeilen 64–69: Ist dies nicht der Fall, wird der Fehler protokolliert. Der HTTP-Statuscode wird auf [Response::HTTP_UNAUTHORIZED] gesetzt und das Ergebnis [$statusCode, $status, $content, $headers] wird an den Hauptcontroller zurückgegeben;
- Zeile 72: Die Authentifizierung war erfolgreich. Dies wird in der Sitzung durch Setzen des Schlüssels [user] vermerkt. Das Vorhandensein dieses Schlüssels zeigt eine erfolgreiche Authentifizierung an;
- Zeilen 73–77: Ein Erfolgsergebnis [$statusCode, $status, $content, $headers] wird an den Hauptcontroller zurückgegeben;
23.11.4. [Postman]-Tests
Wir führen [Postman]-Tests am Controller [AuthentifierUtilisateurController] im JSON-Modus durch;

Oben:
- in [1-6] die Aktion [authenticate-user] mit einem GET [2], obwohl ein POST erforderlich ist;
- in [7–10] die JSON-Antwort des Servers;
Ersetzen wir den GET-Aufruf durch einen POST-Aufruf [2], ohne Parameter in den Antworttext [7] aufzunehmen:

Oben:
- in [1–7] der POST-Befehl ohne Parameter, gesendet in [7];
- in [8-11] die JSON-Antwort des Servers;
Fügen wir nun einen Parameter [password] zum Anfragetext [4] hinzu:

Oben:
- in [1-6] eine POST-Anfrage [2] mit einem [password]-Parameter [4-6]. Gesendete Parameter müssen dem Anfragetext [4] hinzugefügt werden. Es gibt mehrere Möglichkeiten, Werte an den Server zu senden. Wir wählen die Methode [x-www-form-urlencoded] [5];
- in [8–10] die JSON-Antwort vom Server;
Definieren wir nun den Parameter [user] ohne den Parameter [password]:

Oben:
- in [1–7] eine POST-Anfrage ohne den Parameter [password] [4–7];
- in [8-11] die JSON-Antwort des Servers;
Legen wir nun die beiden Parameter [user, password] fest, jedoch mit Werten, die dazu führen, dass die Authentifizierung fehlschlägt:

Oben:
- in [1-9] eine POST-Anfrage mit falschen [user, password]-Parametern;
- in [10–13] die JSON-Antwort des Servers. Beachten Sie den Statuscode [401 Unauthorized] [10] in der Antwort;
Nun eine POST-Anfrage mit gültigen Anmeldedaten:

Oben:
- in [1–9] die POST-Anfrage [2] mit gültigen Anmeldedaten [6–9];
- in [10–13] die JSON-Antwort des Servers. Beachten Sie den HTTP-Statuscode [200 OK] in [10];
23.11.5. Die Aktion [calculate-tax]
Die Aktion [calculer-impot] wird vom folgenden Controller [CalculerImpotController] verarbeitet:
<?php
namespace Application;
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// layer alias [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class CalculerImpotController implements InterfaceController {
// $config is the application configuration
// traitement d'une requête Request
// session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// you must have one GET parameter and three POST parameters
$method = strtolower($request->getMethod());
$erreur = $method !== "post" || $request->query->count() != 1;
if ($erreur) {
// we note the error
$message = "il faut utiliser la méthode [post] avec [action] dans l'URL et les paramètres postés [marié, enfants, salaire]";
$état = 301;
// return result to main controller
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// retrieve POST parameters
$erreurs = [];
$état = 310;
// marital status
if (!$request->request->has("marié")) {
$état += 2;
$erreurs[] = "paramètre [marié] manquant";
} else {
$marié = trim(strtolower($request->request->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
if ($erreur) {
$état += 4;
$erreurs[] = "valeur [$marié] invalide pour le paramètre [marié]";
}
}
// the number of children
if (!$request->request->has("enfants")) {
$état += 8;
$erreurs[] = "paramètre [enfants] manquant";
} else {
$enfants = trim($request->request->get("enfants"));
$erreur = !preg_match("/^\d+$/", $enfants);
if ($erreur) {
$état += 9;
$erreurs[] = "valeur [$enfants] invalide pour le paramètre [enfants]";
}
}
// we recover the annual salary
if (!$request->request->has("salaire")) {
$erreurs[] = "paramètre [salaire] manquant";
$état += 16;
} else {
$salaire = trim($request->request->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
if ($erreur) {
$état += 17;
$erreurs[] = "valeur [$salaire] invalide pour le paramètre [salaire]";
}
}
// mistake?
if ($erreurs) {
// return result to main controller
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
}
// we have everything you need to work
// Redis
\Predis\Autoloader::register();
try {
// customer [predis]
$redis = new \Predis\Client();
// connect to the server to see if it's there
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// it didn't go well
// return result with error to main controller
$état = 350;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// we have valid parameters
// creation of the [dao] layer
if (!$redis->get("taxAdminData")) {
try {
// retrieve tax data from the database
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// put the recovered data into redis
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// it didn't go well
// return result with error to main controller
$état = 340;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// tax data are taken from the [application] scope memory
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// istanciation of the [dao] layer
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
}
// creation of the [business] layer
$métier = new ServerMetier($dao);
// we have everything we need to work - tax calculation
$résultat = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we add the simulation just run to the session
$simulation = new Simulation();
$résultat = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $résultat;
$simulation->setFromArrayOfAttributes($résultat);
// is there a list of in-session simulations?
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// add simulation to simulation list
$simulations[] = $simulation;
// simulations are put back into session
$session->set("simulations", $simulations);
// return result to main controller
$état = 300;
return [Response::HTTP_OK, $état, ["réponse" => $résultat], []];
}
}
Kommentare
- Die erwartete Anfrage lautet [POST main.php?action=calculate-tax] mit drei übermittelten Parametern [married, children, salary]:
- [married] muss entweder [yes] oder [no] sein;
- [children, salary] müssen positive ganze Zahlen oder Null sein;
- Zeilen 26–27: Wir überprüfen, ob wir eine POST-Anfrage mit einem einzigen Parameter in der URL haben;
- Zeilen 28–34: Ist dies nicht der Fall, wird ein Fehlerergebnis an den Hauptcontroller gesendet;
- Zeile 36: Wir sammeln die Fehlermeldungen im Array [$errors];
- Zeilen 39–41: Wir prüfen, ob der Parameter [verheiratet] vorhanden ist. Ist dies nicht der Fall, wird der Fehler protokolliert;
- Zeilen 43–49: Wir prüfen, ob [married] einen Wert in [yes, no] hat. Ist dies nicht der Fall, wird der Fehler protokolliert;
- Zeilen 51–54: Wir prüfen, ob der Parameter [children] vorhanden ist. Ist dies nicht der Fall, wird ein Fehler protokolliert;
- Zeilen 55–61: Wir prüfen, ob der Wert des Parameters [children] eine positive Zahl oder Null ist. Ist dies nicht der Fall, wird ein Fehler protokolliert;
- Zeilen 63–66: Wir prüfen, ob der Parameter [salary] vorhanden ist. Ist dies nicht der Fall, wird ein Fehler protokolliert;
- Zeilen 67–72: Wir prüfen, ob der Wert des Parameters [salary] eine positive Zahl oder Null ist. Ist dies nicht der Fall, wird ein Fehler protokolliert;
- Zeilen 75–78: Ist das Array [$errors] nicht leer, bedeutet dies, dass Fehler aufgetreten sind. Wir fügen das Fehler-Array in die Antwort ein und geben das Ergebnis an den Haupt-Controller zurück;
- Zeile 80: Wir haben gültige Parameter. Wir können die Steuer berechnen. Dazu müssen wir die Schichten [dao] und [business] erstellen, die wissen, wie diese Berechnung durchzuführen ist;
- Zeilen 82–94: Wir erstellen einen [Redis]-Client;
- Zeilen 88–94: Wenn wir keine Verbindung zum [Redis]-Server herstellen konnten, senden wir einen [500 Internal Server Error]-Code an den Client;
- Zeile 98: Wir prüfen, ob der [Redis]-Server den Schlüssel [taxAdminData] enthält. Dieser Schlüssel steht für die Steuerverwaltungsdaten. Ist der Schlüssel nicht vorhanden, müssen die Steuerdaten aus der Datenbank abgerufen werden;
- Zeile 101: Aufbau der [dao]-Schicht, wenn Steuerdaten aus der Datenbank abgerufen werden müssen. Die Klasse [ServerDaoWithRedis] wurde im verlinkten Abschnitt beschrieben;
- Zeile 103: Die aus der Datenbank abgerufenen Daten werden in [Redis] unter dem Schlüssel [taxAdminData] gespeichert;
- Zeilen 104–110: Wenn die Datenbankabfrage fehlgeschlagen ist, wird der von der [dao]-Schicht zurückgegebene Fehler protokolliert und in das an den Hauptcontroller zurückgesendete Ergebnis aufgenommen;
- Zeile 109: Die von der [PDO]-Schicht zurückgegebene Fehlermeldung ist in [iso-8859-1] kodiert. Sie wird in [utf-8] kodiert;
- Zeilen 111–117: Wenn der Schlüssel [taxAdminData] im [Redis]-Speicher vorhanden ist, werden die Steuerdaten direkt an den Konstruktor der [DAO]-Schicht übergeben;
- Zeile 119: Die [Business]-Schicht wird erstellt. Die Klasse [ServerMetier] wurde im Abschnitt „Links“ beschrieben;
- Zeilen 124–126: Mit dem berechneten Steuerbetrag wird ein [Simulation]-Objekt erstellt. Die Klasse [Simulation] kapselt die Daten einer Simulation und wurde im Abschnitt „Links“ beschrieben;
- Zeilen 128–132: Die soeben erstellte Simulation muss der Liste der bereits berechneten Simulationen hinzugefügt werden. Diese Liste befindet sich in der Sitzung, sofern noch keine Simulation durchgeführt wurde;
- Zeilen 133–136: Die Simulation wird zur Liste der Simulationen hinzugefügt, und die Liste wird an die Sitzung zurückgegeben;
- Zeilen 137–139: Das Ergebnis wird an den Hauptcontroller zurückgegeben;
23.11.6. [Postman]-Tests
Wir führen [Postman]-Tests am [CalculerImpotController]-Controller im JSON-Modus durch;

Oben:
- In [1-7] senden wir eine [GET]-Anfrage anstelle einer [POST]-Anfrage;
- In [8–11] die JSON-Antwort des Servers;
Verwenden wir nun die [POST]-Methode, mit oder ohne übermittelte Parameter sowie mit ungültigen übermittelten Parametern:

Oben:
- Wir senden eine [POST]-Anfrage [2] mit ungültigen übermittelten Parametern [6–11] [married, children, salary]. Sie können einen dieser Parameter weglassen, indem Sie das entsprechende Kästchen in [16] deaktivieren. So können Sie verschiedene Szenarien testen. Im obigen Screenshot sind alle drei Parameter vorhanden und alle sind ungültig;
- in [12–15] die JSON-Antwort des Servers;
Deaktivieren wir nun zwei der drei übermittelten Parameter:

Oben,
- in [5-8] wird nur der Parameter [salary] gesendet, und dieser ist zudem ungültig;
- in [9-11] das JSON-Ergebnis vom Server;
Führen wir nun eine Steuerberechnung mit gültigen Parametern durch:

Oben:
- in [11-18] eine Anfrage mit gültigen Parametern [6-8];
- in [12–14] die JSON-Antwort des Servers;
23.11.7. Die Aktion [lister-simulations]
Die Aktion [lister-simulations] wird vom folgenden sekundären Controller [ListerSimulationsController] verarbeitet:
<?php
namespace Application;
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class ListerSimulationsController {
// $config is the application configuration
// traitement d'une requête Request
// useful session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// you must have a single parameter GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
$état = 501;
$message = "GET requis, avec l'unique paramètre [action] dans l'URL";
// return an error result to the main controller
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// retrieve the list of simulations in the session
if (!$session->has("simulations")) {
$simulations = [];
} else {
$simulations = $session->get("simulations");
}
// a successful result is returned to the main controller
$état = 500;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
Kommentare
- request [GET main.php?action=list-simulations];
- Zeilen 24–25: Wir prüfen, ob wir eine GET-Anfrage mit einem einzigen Parameter haben;
- Zeilen 26–31: Ist dies nicht der Fall, wird ein Fehlerergebnis an den Hauptcontroller zurückgegeben;
- Zeilen 33–37: Die Liste der Simulationen wird aus der Sitzung abgerufen, sofern vorhanden (Zeile 36), andernfalls ist die Liste leer (Zeile 34);
- Zeilen 39–40: Die Liste der Simulationen wird an den Hauptcontroller zurückgegeben;
23.11.8. Tests [Postman]
Wir erstellen zwei Tests, einen für einen Fehler und einen für einen Erfolg.

Oben:
- In [1–8] senden wir eine [GET]-Anfrage mit einem zusätzlichen Parameter [param1] in der URL [3, 7–8];
- In [9–12] die JSON-Antwort des Servers;
Nun senden wir eine gültige Anfrage:

Oben:
- in [1-5], eine gültige Anfrage;
Das Ergebnis der Anfrage lautet wie folgt:

- in [3-6] die JSON-Antwort des Servers. Vor diesem Test wurde der [Postman]-Test [calculate-tax-300] mehrmals ausgeführt, um Simulationen in der Websitzung des Servers zu erstellen;
23.11.9. Die Aktion [delete-simulation]
Die Aktion [delete-simulation] wird vom folgenden sekundären Controller [DeleteSessionController] verarbeitet:
<?php
namespace Application;
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class SupprimerSimulationController {
/// $config is the application configuration
// traitement d'une requête Request
// useful session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// you must have two GET parameters
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 2;
$état = 600;
if ($erreur) {
$état += 2;
$message = "GET requis, avec les paramètres [action, numéro]";
}
// parameter [number] must exist
if (!$erreur) {
$état += 4;
$erreur = !$request->query->has("numéro");
if ($erreur) {
$message = "paramètre [numéro] manquant";
}
}
// parameter [number] must be valid
if (!$erreur) {
$état += 8;
$numéro = $request->query->get("numéro");
$erreur = !preg_match("/^\d+$/", $numéro);
if ($erreur) {
$message = "paramètre [$numéro] invalide";
}
}
// parameter [number] must be in the range [0,n-1]
// if n is the number of simulations
if (!$erreur) {
$numéro = (int) $numéro;
$erreur = !$session->has("simulations");
if (!$erreur) {
$simulations = $session->get("simulations");
$erreur = $numéro < 0 || $numéro >= count($simulations);
}
if ($erreur) {
$état += 16;
$message = "la simulation n° [$numéro] n'existe pas";
}
}
// mistake?
if ($erreur) {
// return the result to the main controller
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// delete the $numéro simulation
unset($simulations[$numéro]);
$simulations = array_values($simulations);
// put the simulations back in the session
$session->set("simulations", $simulations);
// we return the list of simulations to the customer
$état = 600;
return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
}
}
Kommentare
- request [GET main.php?action=delete-simulation&number=x];
- Zeilen 24–30: Wir prüfen, ob eine GET-Anfrage mit zwei Parametern vorliegt;
- Zeilen 32–38: Wir prüfen, ob der Parameter [number] in den URL-Parametern vorhanden ist;
- Zeilen 40–47: Wir überprüfen, ob der Wert des Parameters [number] syntaktisch korrekt ist;
- Zeilen 50–61: Wir überprüfen, ob die Simulation #[number] tatsächlich existiert. Es gibt zwei Fehlerfälle:
- Die Liste der Simulationen kann in der Sitzung nicht gefunden werden (Zeile 52);
- die zu löschende Simulationsnummer [number] ist in der Liste der Simulationen nicht vorhanden;
- Zeilen 63–66: Im Fehlerfall wird ein Fehlerergebnis an den Hauptcontroller zurückgegeben;
- Zeile 68: Die Simulation #[number] wird gelöscht;
- Zeile 69: Die Operation [unset] ändert die Indizes [0, n-1] der Liste nicht. Um sie zu aktualisieren, rufen wir die Werte aus dem Array [$simulations] ab, um die fehlende Simulation zu entfernen;
- Zeile 71: Das neue Array der Simulationen wird wieder in die Sitzung zurückgesetzt;
- Zeilen 73–74: Die neue Liste der Simulationen wird an den Hauptcontroller zurückgegeben;
23.11.10. [Postman] Tests
Wir führen Erfolgs- und Fehlertests durch:

Oben:
- in [1–6] eine GET-Anfrage ohne den Parameter [number];
- in [7–10] die JSON-Antwort des Servers;
Nun eine Anfrage mit einer syntaktisch falschen Zahl:

Oben:
- in [1-5], eine GET-Anfrage mit einem ungültigen [number]-Parameter [3, 5];
- in [6–9] die JSON-Antwort des Servers;
Nun eine Anfrage mit einer Simulationsnummer, die nicht existiert:

Oben:
- in [1-5] eine Anfrage mit einer Simulationsnummer von 100, die in der Liste der Simulationen nicht vorhanden ist;
- in [6–9] die JSON-Antwort des Servers;
Nun werden wir die Simulation Nr. 0 aus der Liste entfernen, also die erste Simulation. Zunächst fordern wir diese Liste erneut mit der Anfrage [lister-simulations-500] an:

- In [1] befinden sich derzeit 2 Simulationen;
Wir löschen die erste Simulation (Nummer 0):

Oben:
- in [1-5] löschen wir Simulation Nr. 0 [5];
- in [6–9] die JSON-Antwort des Servers. Wir sehen, dass Simulation Nr. 0 entfernt wurde;
Wiederholen wir diesen Schritt:

Oben:
- In [1] sind keine Simulationen mehr in der Websitzung des Servers vorhanden;
23.11.11. Die Aktion [end-session]
Die Aktion [end-session] wird vom folgenden sekundären Controller [FinSessionController] verarbeitet:
<?php
namespace Application;
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class FinSessionController implements InterfaceController {
// $config is the application configuration
// traitement d'une requête Request
// session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// you must have a single parameter GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
// mistake?
if ($erreur) {
$état = 401;
// result to main controller
$message = "GET requis avec le seul paramètre [action] dans l'URL";
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// memorize the session type
$type = $session->get("type");
// the current session is invalidated
$session->invalidate();
// put the type back in the new session
$session->set("type", $type);
// reply sent
$état = 400;
// result to main controller
$content = ["réponse" => "session supprimée"];
return [Response::HTTP_OK, $état, $content, []];
}
}
Kommentare
- Anfrage [GET main.php?action=end-session];
- Zeilen 25–33: Wir überprüfen, ob es sich bei der Aktion um einen GET-Aufruf mit dem einzigen Parameter [end-action] handelt;
- Zeile 38: Die aktuelle Sitzung wird ungültig gemacht. Dadurch werden die darin gespeicherten Daten gelöscht und eine neue Sitzung gestartet;
- Zeile 36: Bevor wir die Sitzung beenden, speichern wir ihren Typ [json, xml, html];
- Zeile 40: Der Typ der vorherigen Sitzung wird in der neuen Sitzung festgelegt. Schließlich fahren wir mit einer neuen Sitzung fort, die den einzigen Schlüssel [type] enthält;
- Zeilen 44–45: Das Ergebnis wird an den Hauptcontroller zurückgegeben;
23.11.12. Tests [Postman]
Wir führen einen Fehlertest und einen Erfolgstest durch:

Oben:
- In [1–5] fordern wir das Ende der Sitzung [5] mit einem POST [2] anstelle des erwarteten GET an;
- In [6–9] die JSON-Antwort des Servers;
Nun ein Beispiel für einen erfolgreichen Test. Betrachten wir zunächst das Sitzungs-Cookie, das während des zuletzt durchgeführten Tests zwischen dem Client [Postman] und dem Server ausgetauscht wurde:

Oben:
- in [3], das vom Client [Postman] an den Server gesendete Sitzungscookie;
Sehen wir uns nun die HTTP-Header an, die der Server in seiner Antwort gesendet hat:

Oben:
- In [3-4] ist das Session-Cookie nicht in der Antwort des Servers enthalten. Das ist normal. Der Server sendet es nur einmal: zu Beginn einer neuen Websitzung;
Führen wir nun eine gültige [logout]-Aktion aus:

Oben:
- in [1-3] eine gültige [end-session]-Aktion;
- in [4–7] die JSON-Antwort des Servers;
Sehen wir uns die HTTP-Header an, die in der Antwort des Servers gesendet wurden:

- in [3] sendet der Server den [Set-Cookie]-Header, der anzeigt, dass eine neue Websitzung beginnt;
23.12. Arten von Serverantworten
23.12.1. Einführung
Werfen wir noch einmal einen Blick auf die Gesamtarchitektur der Anwendung:

Wir stellen die möglichen Antworttypen [3a] vor. Diese sind im Ordner [Responses] des Projekts zusammengefasst:

Die Klasse [JsonResponse] haben wir bereits im verlinkten Abschnitt vorgestellt. Sie implementiert die Schnittstelle [InterfaceResponse] und erweitert die Klasse [ParentResponse]. Dies gilt auch für die beiden anderen Klassen, [XmlResponse] und [HtmlResponse].
Sehen wir uns die Definition der Schnittstelle [InterfaceResponse] noch einmal an:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
interface InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session: the web application session
// array $config: application configuration
// int statusCode: HTTP response status code
// array $content: server response
// array $headers: HTTP headers to be added to the response
// Logger $logger: the logger for writing logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void;
}
- Zeilen 19–27: Die Schnittstelle [InterfaceResponse] verfügt über eine einzige Methode [send], um die Antwort an den Client zu senden;
- Zeilen 11–17: Die Bedeutung der verschiedenen Parameter der Methode [send];
- Zeilen 23–25: Die Parameter [$statusCode, $content, $headers] sind die Standardantwort der sekundären Controller der Anwendung. Die Antwort benötigt jedoch möglicherweise zusätzliche Informationen. Daher stellen wir ihr die ersten drei Parameter (Zeilen 20–22) zur Verfügung, die ihr Zugriff auf alle Informationen bezüglich der Anfrage, der Sitzung und der Konfiguration gewähren;
- Zeile 26: Die Antwort benötigt den [Logger], da dieser die an den Client gesendete Antwort protokolliert;
Sehen wir uns nun den Code für die Klasse [ParentResponse] an, die übergeordnete Klasse der drei Antworttypen, die deren Gemeinsamkeiten abstrahiert: das eigentliche Senden einer Textantwort an den Client:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Response;
class ParentResponse {
// int $statusCode: HTTP response status code
// string $content: the body of the response to be sent
// depending on the case, this is a jSON, XML, HTML string
// array $headers: HTTP headers to be added to the response
public function sendResponse(
int $statusCode,
string $content,
array $headers): void {
// preparing the server's text response
$response = new Response();
$response->setCharset("utf-8");
// status code
$response->setStatusCode($statusCode);
// headers
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// we send the answer
$response->setContent($content);
$response->send();
}
}
Kommentare
- Zeilen 10–13: Die Bedeutung der drei Parameter der [send]-Methode;
- Zeile 17: Beachten Sie, dass der Antworttext vom Typ [string] ist und somit versandbereit ist (Zeile 30);
- Zeile 22: Die Antwort enthält UTF-8-Zeichen;
- Zeile 24: HTTP-Statuscode der Antwort;
- Zeilen 26–28: Hinzufügen der vom aufrufenden Code bereitgestellten HTTP-Header;
- Zeilen 30–31: Senden der Antwort an den Client;
Schauen wir uns zum Schluss den Code für den Hauptcontroller an, der die Übermittlung der Antwort an den Client anfordert:
// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
- Zeile 4: Wir legen den Namen der zu instanziierenden [Response]-Klasse fest;
- Zeile 5: Wir instanziieren sie und senden die Antwort mithilfe der Methode [send($request, $session, $config, $statusCode, $content, $headers, $logger)] an den Client. Da sie alle dieselbe Schnittstelle [InterfaceResponse] implementieren, haben die [send]-Methoden der verschiedenen Antworttypen alle dieselbe Signatur;
23.12.2. Die Klasse [JsonResponse]
Sie wurde bereits im verlinkten Abschnitt vorgestellt. Wir geben ihren Code hier jedoch noch einmal wieder, um die Konsistenz der drei Antwortklassen besser hervorzuheben:
Die Klasse [JsonResponse] implementiert die Schnittstelle [InterfaceResponse] wie folgt:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
class JsonResponse extends ParentResponse implements InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session: the web application session
// array $config: application configuration
// int statusCode: HTTP response status code
// array $content: server response
// array $headers: HTTP headers to be added to the response
// Logger $logger: the logger for writing logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// symfony serializer preparation
$serializer = new Serializer(
[
// required for object serialization
new ObjectNormalizer()],
// encoder jSON
// for options, make OU between the different options
[new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
);
// serialization jSON
$json = $serializer->serialize($content, 'json');
// headers
$headers = array_merge($headers, ["content-type" => "application/json"]);
// sending reply
parent::sendResponse($statusCode, $json, $headers);
// log
if ($logger !== NULL) {
$logger->write("réponse=$json\n");
}
}
}
Kommentare
- Zeile 13: Die Klasse implementiert die Schnittstelle [InterfaceResponse];
- Zeile 13: Die Klasse erweitert die Klasse [ParentResponse]. Alle [Response]-Typen erweitern diese Klasse. Es ist diese übergeordnete Klasse, die die Antwort an den Client sendet (Zeile 46). Da dieser Code allen [Response]-Typen gemeinsam war, wurde er in eine übergeordnete Klasse ausgelagert;
- Zeilen 33–40: Instanziierung des [Symfony]-Serialisierers, der die Serverantwort [$content] in eine JSON-Zeichenkette umwandelt (Zeile 42);
- Zeilen 34–36: Der erste Parameter des [Serializer]-Konstruktors ist ein Array. Darin platzieren wir eine Instanz der Klasse [ObjectNormalizer], die für die Objektserialisierung benötigt wird. In dieser Anwendung geschieht dies mit einer Liste von Simulationen, wobei jede Simulation eine Instanz der Klasse [Simulation] ist;
- Zeile 39: Der zweite Parameter des [Serializer]-Konstruktors ist ebenfalls ein Array: Wir legen alle bei der Serialisierung verwendeten Encoder (XML, JSON, CSV usw.) darin ab;
- Zeile 39: Hier wird es nur einen Encoder geben, vom Typ [JsonEncoder]. Der parameterlose Konstruktor hätte möglicherweise ausgereicht. Hier haben wir einen [JsonEncode]-Parameter an den Konstruktor übergeben, ausschließlich um JSON-Kodierungsoptionen zu übergeben;
- Zeile 39: Der Konstruktorparameter [JsonEncode] ist ein Array von Optionen. Hier verwenden wir die Option [JSON_UNESCAPED_UNICODE], um festzulegen, dass die UTF-8-Zeichen in der JSON-Zeichenkette nativ dargestellt und nicht „escaped“ werden sollen;
- Zeile 42: Der Hauptteil der HTTP-Antwort wird mit dem zuvor genannten Serializer in JSON serialisiert;
- Zeile 44: Wir fügen den HTTP-Header hinzu, der dem Client mitteilt, dass wir JSON senden;
- Zeile 46: Die übergeordnete Klasse wird aufgefordert, die Antwort an den Client zu senden;
- Zeilen 48–50: Wir protokollieren die JSON-Antwort;
23.12.3. Die Klasse [XmlResponse]
Die Klasse [XmlResponse] implementiert die Schnittstelle [InterfaceResponse] wie folgt:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class XmlResponse extends ParentResponse implements InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session: the web application session
// array $config: application configuration
// int statusCode: HTTP response status code
// array $content: server response
// array $headers: HTTP headers to be added to the response
// Logger $logger: the logger for writing logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// symfony serializer preparation
$serializer = new Serializer(
// required for object serialization
[new ObjectNormalizer()],
[
// serialization XML
new XmlEncoder(
[
XmlEncoder::ROOT_NODE_NAME => 'root',
XmlEncoder::ENCODING => 'utf-8'
]
),
// serialization jSON
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// serialization XML
$xml = $serializer->serialize($content, 'xml');
// headers
$headers = array_merge($headers, ["content-type" => "application/xml"]);
// sending reply
parent::sendResponse($statusCode, $xml, $headers);
// log
if ($logger !== NULL) {
// log in jSON
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
Kommentare
- Zeilen 34–48: Instanziierung eines Symfony-Serialisierers. Der Konstruktor akzeptiert zwei Parameter vom Typ Array;
- Zeile 36: Das erste Array enthält eine Instanz vom Typ [ObjectNormalizer], die bei der Objektserialisierung verwendet wird;
- Zeilen 37–47: Das zweite Array enthält die für die Serialisierung verwendeten Encoder. Mit demselben Serializer lassen sich verschiedene Arten der Serialisierung konfigurieren;
- Zeilen 38–44: der XML-Encoder;
- Zeile 41: Die Wurzel des generierten XML-Codes wird festgelegt. Sie hat die Form <root>[andere XML-Tags]</root>;
- Zeile 42: Die Kodierung verwendet UTF-8-Zeichen;
- Zeile 46: der JSON-Encoder. Dieser wird verwendet, um die Antwort in der Datei [logs.txt] zu protokollieren, die im JSON-Format vorliegt;
- Zeile 50: Der an den Client gesendete Antworttext wird in XML serialisiert;
- Zeile 52: Wir fügen den als Parameter empfangenen Headern (Zeile 30) den HTTP-Header hinzu, der dem Client mitteilt, dass wir ein XML-Dokument senden;
- Zeile 54: Die übergeordnete Klasse sendet die Antwort tatsächlich an den Client;
- Zeilen 56–60: JSON-Protokoll der Antwort;
23.12.4. Tests [Postman]
Wir haben bereits alle möglichen Fehlertests in JSON durchgeführt. In XML gibt es nichts weiter zu tun. Wir zeigen zwei Beispiele für XML-Antworten:

Oben:
- in [1-3] die XML-Anfrage zum Start der Sitzung;
- in [4–7] die XML-Antwort des Servers;
Von nun an erfolgen alle Serverantworten im XML-Format. Wir können alle bereits in [Postman] verwendeten Anfragen unverändert wiederverwenden und erhalten für jede davon eine XML-Antwort. Führen wir zum Beispiel eine erfolgreiche Authentifizierung durch:

Oben:
- in [1-3] eine gültige Authentifizierungsanfrage;
- in [4–7] die XML-Antwort des Servers;
23.12.5. Die [HtmlResponse]
Wenn der Sitzungstyp [html] ist, wird ein Objekt vom Typ [HtmlResponse] instanziiert, um die Antwort an den Client zu senden. Dadurch wird dem Client ein HTML-Stream gesendet, der vom Statuscode abhängt, der vom sekundären -Controller zurückgegeben wird, der die Aktion verarbeitet hat. Diese [status=>view]-Zuordnung ist in der Konfigurationsdatei [config.json] wie folgt definiert:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Diese Konfiguration lautet wie folgt: [‘Ansichtsname’ => ‘mit dieser Ansicht verknüpfte Zustände’]
- Zeile 2: Wenn der sekundäre Controller einen Status aus dem Array [700, 221, 400] zurückgibt, muss die Ansicht [vue-authentification.php] angezeigt werden;
- Zeile 3: Wenn der sekundäre Controller ein Array [200, 300, 341, 350, 800] zurückgibt, dann die Ansicht [tax-calculation-view.php] anzeigen;
- Zeile 4: Wenn der sekundäre Controller ein Array [500, 600] zurückgibt, dann die Ansicht [view-simulation-list.php] anzeigen;
- Zeile 6: Wenn der sekundäre Controller einen Wert zurückgibt, der in keinem der vorherigen Arrays enthalten ist, dann die Ansicht [vue-erreurs.php] anzeigen;
Die Ansichten befinden sich im Ordner [Views] des Projekts:

Der Code für die Klasse [HtmlResponse] lautet wie folgt:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class HtmlResponse extends ParentResponse implements InterfaceResponse {
// Request $request : requête en cours de traitement
// Session $session: the web application session
// array $config: application configuration
// int statusCode: HTTP response status code
// array $content: server response
// array $headers: HTTP headers to be added to the response
// Logger $logger: the logger for writing logs
public function send(
Request $request = NULL,
Session $session = NULL,
array $config,
int $statusCode,
array $content,
array $headers,
Logger $logger = NULL): void {
// symfony serializer preparation
$serializer = new Serializer(
[
// for object serialization
new ObjectNormalizer()],
[
// for jSON serialization of the response log
new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
]
);
// the HTML response depends on the status code returned by the controller
$état = $content["état"];
// a view corresponds to a state - look for it in the application configuration
// view list
$vues = array_keys($config["vues"]);
$trouvé = false;
$i = 0;
// browse the list of views
while (!$trouvé && $i < count($vues)) {
// states associated with view n° i
$états = $config["vues"][$vues[$i]];
// is the state you're looking for in the states associated with view n° I?
if (in_array($état, $états)) {
// the view displayed will be view n° i
$vueRéponse = $vues[$i];
$trouvé = true;
}
// next view
$i++;
}
// found?
if (!$trouvé) {
// if no view exists for the current state of the application
// render error view
$vueRéponse = $config["vue-erreurs"];
}
// retrieve the HTML view to be displayed in a character string
ob_start();
require __DIR__ . "/../Views/$vueRéponse";
$html = ob_get_clean();
// we indicate in the headers that we're going to send HTML
$headers = array_merge($headers, ["content-type" => "text/html"]);
// the parent class handles the actual sending of the response
parent::sendResponse($statusCode, $html, $headers);
// log in jSON of the response without the HTML
if ($logger !== NULL) {
// log in jSON of the response from the secondary controller that processed the action
$log = $serializer->serialize($content, 'json');
$logger->write("réponse=$log\n");
}
}
}
Kommentare
- Zeilen 32–41: Wir instanziieren einen Symfony-Serializer. Dies ist für das JSON-Protokoll der Antwort des Controllers erforderlich, der die Aktion verarbeitet hat (Zeilen 72–82);
- Zeilen 42–57: Wir durchsuchen die Anwendungskonfiguration nach der Ansicht, die angezeigt werden soll. Dies hängt vom Statuscode ab, der vom Controller zurückgegeben wurde, der die Aktion bearbeitet hat. Dieser Code befindet sich in [$content[‘status’]] (Zeile 43);
- Zeilen 42–61: Es wird nach der diesem Status entsprechenden Ansicht gesucht;
- Zeilen 62–67: Wird keine Ansicht gefunden, befindet sich die HTML-Anwendung in einem abnormalen Zustand. Wir werden dieses Konzept der abnormalen Zustände später näher erläutern. In diesem Fall wird eine Fehleransicht angezeigt;
- Zeilen 68–70: Der PHP-Code der ausgewählten Ansicht wird interpretiert und das Ergebnis in der Variablen [$html] gespeichert (Zeile 71);
- Dieser Code bedarf einer Erläuterung. Nehmen wir an, die ausgewählte Ansicht ist [vue-authentification.php], die ein Web-Authentifizierungsformular anzeigt:
- Zeile 69: Die Funktion [ob_start] initiiert das, was in der Dokumentation als Ausgabepuffer bezeichnet wird. Alles, was durch print, require und ähnliche Operationen geschrieben wird – und normalerweise sofort an den Client gesendet würde –, wird in einen Ausgabepuffer (ob = output buffer) gestellt, ohne an den Client gesendet zu werden;
- Zeile 70: Die Ansicht [authentication-view.php] wird geladen; dies ist eine dynamische HTML-Ansicht, die PHP-Code enthält. Dann geschehen zwei Dinge:
- Der PHP-Code in der Ansicht [vue-authentification.php] wird geladen und interpretiert. Das Ergebnis ist eine Ansicht, die wir [vue-authentification.html] nennen, die nur HTML-Code – und möglicherweise CSS und JavaScript – enthält, aber kein PHP mehr;
- Dieser HTML-Code wird normalerweise an den Client gesendet. Dies ist eigentlich bei jedem Text der Fall, auf den der PHP-Interpreter stößt und der kein PHP-Code ist. Aufgrund der Ausgabepufferung wird dieser HTML-Code in den Ausgabepuffer gestellt, ohne an den Client gesendet zu werden;
- Zeile 71: Die Funktion [ob_get_clean] erfüllt zwei Aufgaben:
- Sie legt den Inhalt des Ausgabepuffers in die Variable [$html] ab, d. h. die Seite [vue-authentification.html], die dort abgelegt wurde;
- sie leert den Ausgabepuffer. Was den Puffer betrifft, ist es, als wäre nichts geschehen. Außerdem hat der Client noch immer nichts erhalten;
- Zeile 70: Wir führen derzeit die Klasse [HtmlResponse] aus, die sich im Ordner [Responses] befindet. Um die Ansicht zu finden, müssen wir daher eine Ebene nach oben gehen [..] und dann zum Ordner [Views] navigieren. [__DIR__] ist der absolute Pfad des Ordners, der das aktuell ausgeführte Skript enthält; in unserem Beispiel ist dies der Ordner [C:/myprograms/laragon-lite/www/php7/scripts-web/impots/13/Responses];
- Zeile 73: Wir fügen den als Parameter empfangenen HTTP-Headern (Zeile 29) den Header hinzu, der dem Client mitteilt, dass wir ihm HTML senden werden;
- Zeile 75: Wir weisen die übergeordnete Klasse an, die Antwort tatsächlich an den Client zu senden;
- Zeilen 77–81: Protokollieren Sie die Antwort [$content], die vom sekundären Controller bereitgestellt wurde, der die aktuelle Aktion in JSON verarbeitet hat;
23.12.6. Tests [Postman]
Um den HTML-Modus der Sitzung wirklich zu testen, müssten wir alle Ansichten überprüfen. Das werden wir später tun. Wir führen den folgenden Test durch:
Sehen wir uns die Liste der Ansichten in der Konfigurationsdatei an:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Wir können den Kontext identifizieren, der einige der oben genannten Statuscodes erzeugt, indem wir die durchgeführten [Postman]-Tests untersuchen:

Wir sehen, dass der Statuscode [700] einer erfolgreichen [init-session]-Aktion [2] entspricht. Oben haben wir eine JSON-Antwort, es könnte sich aber auch um XML oder HTML handeln. Es ist der letztere Fall, der getestet wird. Laut Konfigurationsdatei bildet die Ansicht [vue-authentification.php] die HTML-Antwort. Lassen Sie uns das überprüfen.

Oben:
- In [1-3] initialisieren wir eine HTML-Sitzung. Wir erwarten daher eine HTML-Antwort;
- in [4–8] die HTML-Antwort vom Server;
- Registerkarte [8] bietet eine Vorschau des empfangenen HTML-Codes;

- in [8-9] eine Vorschau der HTML-Ansicht;
23.13. Die HTML-Webanwendung
23.13.1. Übersicht über die Ansichten
Die HTML-Webanwendung verwendet vier Ansichten:
Die Authentifizierungsansicht:

Die Ansicht zur Steuerberechnung:

Die Simulationslistenansicht:

Die Ansicht „Unerwartete Fehler“:

Wir werden diese Ansichten nacheinander beschreiben.
23.13.2. Die Authentifizierungsansicht
23.13.2.1. Übersicht über die Ansicht
Die Authentifizierungsansicht sieht wie folgt aus:

Die Ansicht besteht aus zwei Elementen, die wir als Fragmente bezeichnen:
- Fragment [1] wird durch ein Skript [v-banner.php] generiert;
- Fragment [2] wird durch ein Skript [v-authentication.php] generiert;
Die Authentifizierungsansicht wird von der folgenden Seite [vue-authentification.php] generiert:
<?php
// page test data
// encapsulate paged data in $page
…
?>
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- bandeau sur 1 ligne et 12 colonnes -->
<?php require "v-bandeau.php"; ?>
<!-- formulaire d'authentification sur 9 colonnes -->
<div class="row">
<div class="col-md-9">
<?php require "v-authentification.php" ?>
</div>
</div>
<?php
// if error - displays an error alert
if ($modèle->error) {
print <<<EOT
<div class="row">
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
Les erreurs suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT;
}
?>
</div>
</body>
</html>
Kommentare
- Zeile 7: Ein HTML-Dokument beginnt mit dieser Zeile;
- Zeilen 8–44: Die HTML-Seite wird von den Tags <html> und </html> umschlossen;
- Zeilen 9–16: der Kopf (head) des HTML-Dokuments;
- Zeile 11: Der Tag <meta charset> gibt an, dass das Dokument in UTF-8 kodiert ist;
- Zeile 12: Der Tag <meta name='viewport'> legt die anfängliche Darstellung des Viewports fest: über die gesamte Breite des Bildschirms, auf dem er angezeigt wird (width), in seiner ursprünglichen Größe (initial-scale), ohne dass er an einen kleineren Bildschirm angepasst wird (shrink-to-fit);
- Zeile 14: Der Tag <link rel='stylesheet'> gibt die CSS-Datei an, die das Erscheinungsbild des Viewports bestimmt. Hier verwenden wir das CSS-Framework Bootstrap 4.1.3 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
- Zeile 15: Das title-Tag legt den Seitentitel fest:

- Zeilen 17–43: Der Hauptteil der Webseite wird von den Tags `body` und `/body` umschlossen;
- Zeilen 18–42: Das Tag <div> begrenzt einen Abschnitt der angezeigten Seite. Die in der Ansicht verwendeten [class]-Attribute beziehen sich alle auf das Bootstrap-CSS-Framework. Das Tag <div class=’container’> begrenzt einen Bootstrap-Container;
- Zeile 20: Wir binden das Skript [v-banner.php] ein. Dieses Skript generiert das Banner der Seite [1]. Wir werden es in Kürze beschreiben;
- Zeilen 22–26: Das <div class=’row’>-Tag definiert eine Bootstrap-Zeile. Diese Zeilen bestehen aus 12 Spalten;
- Zeile 23: Das Tag <div class=’col-md-9’> definiert einen Abschnitt mit 9 Spalten;
- Zeile 24: Wir binden das Skript [v-authentification.php] ein, das das Authentifizierungsformular der Seite [2] anzeigt. Wir werden es in Kürze beschreiben;
- Zeile 27: Das <?php-Tag fügt PHP-Code in die HTML-Seite ein. Dieser Code wird vor dem Rendern der HTML-Seite ausgeführt und kann diese verändern;
- Zeile 29: Alle dynamischen Daten in der angezeigten Ansicht werden in einem [$model]-Objekt vom Typ [stdClass] gekapselt. Dies ist eine willkürliche Wahl. Wir hätten stattdessen auch ein assoziatives Array wählen können, um das gleiche Ergebnis zu erzielen;
- Zeile 29: Die Authentifizierung schlägt fehl, wenn der Benutzer falsche Anmeldedaten eingibt. In diesem Fall wird die Authentifizierungsansicht mit einer Fehlermeldung erneut angezeigt. Das Attribut [$model→error] gibt an, ob diese Fehlermeldung angezeigt werden soll;
- Zeilen 30–39: Diese Syntax gibt den gesamten Text aus, der zwischen den PHP-Symbolen <<<EOT (Zeile 30 – anstelle von EOT=End Of Text kann ein beliebiger Text verwendet werden) und dem EOT-Symbol in Zeile 39 steht (muss mit dem in Zeile 30 verwendeten Symbol identisch sein). Das Symbol muss in der ersten Spalte von Zeile 39 stehen. PHP-Variablen, die sich im Text zwischen den beiden EOT-Symbolen befinden, werden interpretiert;
- Zeilen 33–36: definieren einen Bereich mit rosa Hintergrund (class="alert alert-danger") (Zeile 33);

- Zeile 34: Text;
- Zeile 35: Der HTML-Tag <ul> (ungeordnete Liste) zeigt eine Liste mit Aufzählungszeichen an. Jeder Listenpunkt muss die Syntax <li>Punkt</li> haben;
Beachten wir die dynamischen Elemente, die in diesem Code definiert werden müssen:
- [$model→error]: zur Anzeige einer Fehlermeldung;
- [$template→errors]: eine Liste (im HTML-Sinne) von Fehlermeldungen;
23.13.2.2. Das Fragment [v-bandeau.php]
Das Fragment [v-bandeau.php] zeigt das obere Banner aller Ansichten in der Webanwendung an:

Der Code für das Fragment [v-banner.php] lautet wie folgt:
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
Kommentare
- Zeilen 2–13: Das Banner ist in einen Bootstrap-Jumbotron-Abschnitt [<div class="jumbotron">] eingebettet. Diese Bootstrap-Klasse gestaltet den angezeigten Inhalt auf eine bestimmte Weise, um ihn hervorzuheben;
- Zeilen 3–12: eine Bootstrap-Zeile;
- Zeilen 4–6: Ein Bild [img] wird in den ersten vier Spalten der Zeile platziert;
- Zeile 5: Die Syntax [<?= $logo ?>] entspricht der Syntax [<?php print $logo ?>]. Mit anderen Worten: Der Wert des Attributs [src] ist der Wert der PHP-Variablen [$logo];
- Zeilen 7–11: Die verbleibenden 8 Spalten der Zeile (denken Sie daran, dass es insgesamt 12 sind) werden verwendet, um Text (Zeile 9) in großer Schriftgröße (<h1>, Zeilen 8–10) anzuzeigen;
Dynamische Elemente:
- [$logo]: URL des im Banner angezeigten Bildes;
23.13.2.3. Das Fragment [v-authentification.php]
Das Fragment [v-authentication.php] zeigt das Authentifizierungsformular der Webanwendung an:

Der Code für das Fragment [v-authentication.php] lautet wie folgt:
<!-- form HTML - post its values with the [authenticate-user] action -->
<form method="post" action="main.php?action=authentifier-utilisateur">
<!-- title -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
<!-- bootstrap form -->
<fieldset class="form-group">
<!-- 1st line -->
<div class="form-group row">
<!-- wording -->
<label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
<div class="col-md-4">
<!-- text input field -->
<input type="text" class="form-control" id="user" name="user"
placeholder="Nom d'utilisateur" value="<?= $modèle->login ?>">
</div>
</div>
<!-- 2nd line -->
<div class="form-group row">
<!-- wording -->
<label for="password" class="col-md-3 col-form-label">Mot de passe</label>
<!-- text input field -->
<div class="col-md-4">
<input type="password" class="form-control" id="password" name="password"
placeholder="Mot de passe">
</div>
</div>
<!-- submit] button on a 3rd line-->
<div class="form-group row">
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
Kommentare
- Zeilen 2–39: Das <form>-Tag definiert ein HTML-Formular. Dieses Formular weist im Allgemeinen folgende Merkmale auf:
- Es definiert Eingabefelder (Tags <input> in den Zeilen 17 und 27);
- es verfügt über eine [submit]-Schaltfläche (Zeile 34), die die eingegebenen Werte an die im [action]-Attribut des [form]-Tags (Zeile 2) angegebene URL sendet. Die HTTP-Methode, die für die Anfrage an diese URL verwendet wird, ist im [method]-Attribut des [form]-Tags (Zeile 2) festgelegt;
- Wenn der Benutzer hier auf die Schaltfläche [Submit] klickt (Zeile 34), sendet der Browser die im Formular eingegebenen Werte per POST (Zeile 2) an die URL [main.php?action=authentifier-utilisateur] (Zeile 2);
- Die gesendeten Werte sind die Werte, die der Benutzer in die Eingabefelder in den Zeilen 17 und 27 eingegeben hat. Sie werden im Format [user=xx&password=yy] gesendet. Die Parameternamen [user, password] entsprechen den [name]-Attributen der Eingabefelder in den Zeilen 17 und 27;
- Zeilen 5–7: Ein Bootstrap-Abschnitt zur Anzeige eines Titels auf blauem Hintergrund:

- Zeilen 10–37: ein Bootstrap-Formular. Alle Formularelemente werden dann auf eine bestimmte Weise gestaltet;
- Zeilen 12–20: Definieren die erste Zeile des Formulars:
![]()
- Zeile 14 definiert das Label [1] über drei Spalten. Das [for]-Attribut des [label]-Tags verknüpft das Label mit dem [id]-Attribut des Eingabefelds in Zeile 17;
- Zeilen 15–19: Platzieren das Eingabefeld in einem vier Spalten umfassenden Layout;
- Zeile 17: Das HTML-Tag [input] definiert ein Eingabefeld. Es hat mehrere Attribute:
- [type='text']: Dies ist ein Texteingabefeld. Sie können beliebige Zeichen eingeben;
- [class='form-control']: Bootstrap-Stil für das Eingabefeld;
- [id='user']: Kennung für das Eingabefeld. Diese Kennung wird in der Regel von CSS- und JavaScript-Code verwendet;
- [name='user']: Der Name des Eingabefelds. Der vom Benutzer eingegebene Wert wird vom Browser unter diesem Namen [user=xx] übermittelt;
- [placeholder='prompt']: der Text, der im Eingabefeld angezeigt wird, wenn der Benutzer noch nichts eingegeben hat;
![]()
- [value='value']: Der Text 'value' wird im Eingabefeld angezeigt, sobald es erscheint, noch bevor der Benutzer etwas eingibt. Dieser Mechanismus wird im Fehlerfall verwendet, um die Eingabe anzuzeigen, die den Fehler verursacht hat. Hier ist dieser Wert der Wert der PHP-Variablen [$model->login];
- Zeilen 21–30: ähnlicher Code für das Passwort-Eingabefeld;
- Zeile 27: [type='password'] erstellt ein Texteingabefeld (Sie können alles eingeben), aber die eingegebenen Zeichen werden ausgeblendet:
![]()
- Zeilen 32–36: eine dritte Zeile für die Schaltfläche [Submit];
- Zeile 34: Da diese Schaltfläche das Attribut [type="submit"] enthält, veranlasst ein Klick darauf den Browser, die eingegebenen Werte an den Server zu senden, wie zuvor erläutert. Das CSS-Attribut [class="btn btn-primary"] zeigt eine blaue Schaltfläche an:

Es gibt noch eine letzte Sache zu erklären. Zeile 2: Das Attribut [action="main.php?action=authentifier-utilisateur"] definiert eine unvollständige URL (sie beginnt nicht mit http://machine:port/chemin). In unserem Beispiel haben alle URLs der Anwendung die Form [http://localhost/php7/scripts-web/impots/version-12/main.php?action=xx]. Der Zugriff auf die Authentifizierungsansicht erfolgt über verschiedene URLs:
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html];
- [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur]
Diese URLs verweisen auf ein Dokument [main.php], das sich unter [http://localhost/php7/scripts-web/impots/version-12] befindet. Dies gilt für alle URLs in dieser Anwendung. Dem Parameter [action="main.php?action=authentifier-utilisateur"] wird dieser Pfad vorangestellt, wenn die eingegebenen Werte übermittelt werden. Diese Werte werden daher an die URL [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur] gesendet.
23.13.2.4. Visuelle Tests
Wir können die Ansichten bereits vor ihrer Integration in die Anwendung testen. Das Ziel hierbei ist es, ihr visuelles Erscheinungsbild zu prüfen. Wir werden alle Testansichten im Ordner [Tests] des Projekts sammeln:

Um die Ansicht [vue-authentification.php] zu testen, müssen wir das Datenmodell erstellen, das darin angezeigt werden soll:
<?php
// page test data
//
// calculate the view model
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
// user code
$modèle->login = "albert";
// error list
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// build a HTML list of errors
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// banner image
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// we render the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
…
</head>
<body>
….
</body>
</html>
Kommentare
- Zeilen 1–5: Die Authentifizierungsansicht enthält dynamische Teile, die vom Objekt [$model] gesteuert werden. Dieses Objekt wird als View-Modell bezeichnet. Gemäß einer der beiden Definitionen für das Akronym MVC steht dies für das M in MVC;
- Zeile 5: Das View-Modell wird durch die Funktion [getModelForThisView] berechnet;
- Zeile 9: Das View-Modell wird in einen Typ [stdClass] gekapselt;
- Zeilen 10–22: Für die dynamischen Elemente der Authentifizierungsansicht werden Testwerte definiert;
Visuelle Tests können in NetBeans durchgeführt werden:

Wir führen diese visuellen Tests so lange fort, bis wir mit dem Ergebnis zufrieden sind.
23.13.2.5. Berechnung des Ansichtsmodells
Sobald das visuelle Erscheinungsbild der Ansicht festgelegt ist, können wir mit der Berechnung des Ansichtsmodells unter realen Bedingungen fortfahren. Sehen wir uns die Zustandscodes an, die zu dieser Ansicht führen. Sie sind in der Konfigurationsdatei zu finden:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Die Statuscodes [700, 221, 400] lösen also die Anzeige der Authentifizierungsansicht aus. Um die Bedeutung dieser Codes zu verstehen, können wir uns die [Postman]-Tests ansehen, die an der JSON-Anwendung durchgeführt wurden:
- [init-session-json-700]: 700 ist der Statuscode nach einer erfolgreichen [init-session]-Aktion: Das Authentifizierungsformular wird dann leer angezeigt;
- [authenticate-user-221]: 221 ist der Statuscode nach einer fehlgeschlagenen [authenticate-user]-Aktion (unbekannte Anmeldedaten): Das Anmeldeformular wird daraufhin angezeigt, damit die Anmeldedaten korrigiert werden können;
- [end-session-400]: 400 ist der Statuscode nach einer erfolgreichen [end-session]-Aktion: Anschließend wird das leere Authentifizierungsformular angezeigt;
Da wir nun wissen, wann das Authentifizierungsformular angezeigt werden soll, können wir dessen Vorlage in [authentication-view.php] berechnen:

Der Code zur Berechnung der Ansichtsvorlage [vue-authentification.php] lautet wie folgt:
<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: controller response
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulate paged data in $modèle
$modèle = new stdClass();
// application status
$état = $content["état"];
// the model depends on the state
switch ($état) {
case 700:
case 400:
// case of empty form display
$modèle->login = "";
// no error to display
$modèle->error = FALSE;
break;
case 221:
// false authentication
// the user initially entered is redisplayed
$modèle->login = $request->request->get("user");
// there is an error to display
$modèle->error = TRUE;
// list HTML of error msg - here only one
$modèle->erreurs = "<li>Echec de l'authentification</li>";
}
// result
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Kommentare
- Zeilen 3–6: Die von der Klasse [HtmlResponse] geerbten Variablen werden deklariert; diese Klasse verwendet ein [require], um die Ansicht [vue-authentification.php] anzuzeigen;
- Zeilen 9–10: Die im View-Code verwendeten Symfony-Klassen;
- Zeilen 15–40: Die Funktion [getModelForThisView] ist für die Berechnung des View-Modells zuständig;
- Zeile 19: Der vom Controller, der die aktuelle Aktion verarbeitet hat, zurückgegebene Statuscode wird abgerufen;
- Zeilen 21–37: Das Modell hängt von diesem Statuscode ab;
- Zeilen 22–28: Fall, in dem ein leeres Authentifizierungsformular angezeigt werden muss;
- Zeilen 29–37: Fall einer fehlgeschlagenen Authentifizierung: Der vom Benutzer eingegebene Benutzername wird zusammen mit einer Fehlermeldung angezeigt. Der Benutzer kann dann einen weiteren Authentifizierungsversuch unternehmen;
Für das Banner wurde eine spezielle Vorlage geschrieben [v-bandeau.php]:
<?php
// logo
$scheme = $request->server->get('REQUEST_SCHEME'); // http
$host = $request->server->get('SERVER_NAME'); // localhost
$port = $request->server->get('SERVER_PORT'); // 80
$uri = $request->server->get('REQUEST_URI'); // /php7/scripts-web/impots/version-12/main.php?action=xxx
$champs = [];
preg_match("/(.+)\/.+?$/", $uri, $champs);
$root = $champs[1]; // /php7/scripts-web/impots/version-12
$modèle->logo = "$scheme://$host:$port$root/Views/logo.jpg"; // http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg
?>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-4">
<img src="<?= $modèle->logo ?>" alt="Cerisier en fleurs" />
</div>
<div class="col-md-8">
<h1>
Calculez votre impôt
</h1>
</div>
</div>
</div>
Kommentare
- In Zeile 16 wird die Variable [$template→logo] verwendet, die die URL des Bannerlogos angibt. Anstatt diese Variable viermal für die vier Ansichten der Anwendung zu berechnen, wird diese Berechnung in das Fragment [v-banner.php] ausgelagert;
- Die Zeilen 1–11 zeigen, wie die URL [http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg] unter Verwendung von Informationen aus der Serverumgebung [$request→server] aufgebaut wird;
23.13.2.6. Tests [Postman]
Wir haben bereits Anfragen erstellt, die die Statuscodes [700, 221, 400] zurückgeben, wodurch die Authentifizierungsansicht angezeigt wird. Sehen wir uns diese noch einmal an:
- [init-session-html-700]: 700 ist der Statuscode nach einer erfolgreichen [init-session]-Aktion: Anschließend wird das leere Anmeldeformular angezeigt;
- [authenticate-user-221]: 221 ist der Statuscode nach einer fehlgeschlagenen [authenticate-user]-Aktion (unbekannte Anmeldedaten): Das Anmeldeformular wird dann angezeigt, damit die Anmeldedaten korrigiert werden können;
- [end-session-400]: 400 ist der Statuscode nach einer erfolgreichen [end-session]-Aktion: Anschließend wird das leere Authentifizierungsformular angezeigt;
Verwenden Sie diese einfach wieder und prüfen Sie, ob die Authentifizierungsansicht korrekt angezeigt wird. Wir zeigen hier nur zwei Tests:
- [init-session-html-700]: Start einer HTML-Sitzung;

- [authenticate-user-221]: Authentifizierung des Benutzers [x, x];

Oben:
- Die Anfrage sendete die Zeichenfolge [user=x&password=x];
- in [4] wird eine Fehlermeldung angezeigt;
- in [3] wurde erneut der falsche Benutzer angezeigt;
23.13.2.7. Fazit
Wir konnten die Ansicht [vue-authentification.php] testen, ohne die anderen Ansichten geschrieben zu haben. Dies war möglich, weil:
- alle Controller geschrieben sind;
- [Postman] es uns ermöglicht, Anfragen an den Server zu senden, ohne die Ansichten zu benötigen. Beim Schreiben von Controllern müssen Sie sich bewusst sein, dass dies jeder tun kann. Sie müssen daher darauf vorbereitet sein, Anfragen zu bearbeiten, die keine Ansicht zulassen würde. Diese werden manuell in [Postman] erstellt. Sie sollten niemals von vornherein davon ausgehen, dass „diese Anfrage unmöglich ist“. Sie müssen dies überprüfen;
23.13.3. Die Ansicht zur Steuerberechnung
23.13.3.1. Übersicht über die Ansicht
Die Ansicht zur Steuerberechnung sieht wie folgt aus:

Die Ansicht besteht aus drei Teilen:
- 1: Das obere Banner wird durch das bereits vorgestellte Fragment [v-bandeau.php] generiert;
- 2: das Formular zur Steuerberechnung, das vom Fragment [v-calcul-impot.php] generiert wird;
- 3: ein Menü mit zwei Links, das vom Fragment [v-menu.php] generiert wird;
Die Ansicht zur Steuerberechnung wird durch das folgende Skript [vue-calcul-impot.php] generiert:

<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: the response of the controller that processed the action
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
…
// we render the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- bandeau -->
<?php require "v-bandeau.php"; ?>
<!-- ligne à deux colonnes -->
<div class="row">
<!-- le menu -->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- le formulaire de calcul -->
<div class="col-md-9">
<?php require "v-calcul-impot.php" ?>
</div>
</div>
<!-- cas du succès -->
<?php
if ($modèle->success) {
// a success alert is displayed
print <<<EOT1
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-success" role="alert">
$modèle->impôt</br>
$modèle->décôte</br>\n
$modèle->réduction</br>\n
$modèle->surcôte</br>\n
$modèle->taux</br>\n
</div>
</div>
</div>
EOT1;
}
?>
<?php
if ($modèle->error) {
// 9-column error list
print <<<EOT2
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<div class="alert alert-danger" role="alert">
L'erreur suivante s'est produite :
<ul>$modèle->erreurs</ul>
</div>
</div>
</div>
EOT2;
}
?>
</div>
</body>
</html>
Kommentare
- Wir kommentieren nur neue Funktionen, die bisher noch nicht aufgetreten sind;
- Zeile 37: Einbindung des oberen Banners der Ansicht in die erste Bootstrap-Zeile der Ansicht;
- Zeilen 41–43: Einbindung des Menüs, das drei Spalten der zweiten Bootstrap-Zeile der Ansicht einnehmen wird;
- Zeilen 45–47: Einbindung des Formulars zur Steuerberechnung, das neun Spalten der zweiten Bootstrap-Zeile der Ansicht einnimmt;
- Zeilen 51–69: Wenn die Steuerberechnung erfolgreich ist [$model→success=TRUE], wird das Ergebnis der Steuerberechnung in einem grünen Feld angezeigt (Zeilen 59–65). Dieses Feld befindet sich in der dritten Bootstrap-Zeile der Ansicht (Zeile 54) und nimmt neun Spalten (Zeile 58) rechts von drei leeren Spalten (Zeilen 55–57) ein. Dieses Feld befindet sich somit unmittelbar unterhalb des Formulars zur Steuerberechnung;
- Zeilen 71–87: Wenn die Steuerberechnung fehlschlägt [$model→error=TRUE], wird eine Fehlermeldung in einem rosa Feld angezeigt (Zeilen 80–83). Dieser Rahmen befindet sich in der dritten Bootstrap-Zeile der Ansicht (Zeile 75) und nimmt neun Spalten (Zeile 79) rechts von drei leeren Spalten (Zeilen 76–78) ein. Dieser Rahmen befindet sich somit unmittelbar unterhalb des Formulars zur Steuerberechnung;
23.13.3.2. Das Fragment [v-calcul-impot.php]
Das Fragment [v-calcul-impot.php] zeigt das Anmeldeformular der Webanwendung an:

Der Code für das Fragment [v-calcul-impot.php] lautet wie folgt:
<!-- form HTML posted -->
<form method="post" action="main.php?action=calculer-impot">
<!-- 12-column message on blue background -->
<div class="col-md-12">
<div class="alert alert-primary" role="alert">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</div>
</div>
<!-- form elements -->
<fieldset class="form-group">
<!-- first row of 9 columns -->
<div class="row">
<!-- 4-column wording -->
<legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
<!-- 5-column radio buttons-->
<div class="col-md-5">
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios1" value="oui" <?= $modèle->checkedOui ?>>
<label class="form-check-label" for="gridRadios1">
Oui
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="marié" id="gridRadios2" value="non" <?= $modèle->checkedNon ?>>
<label class="form-check-label" for="gridRadios2">
Non
</label>
</div>
</div>
</div>
<!-- second row of 9 columns -->
<div class="form-group row">
<!-- 4-column wording -->
<label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
<!-- 5-column numerical entry field for number of children -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="<?= $modèle->enfants ?>">
</div>
</div>
<!-- third row of 9 columns -->
<div class="form-group row">
<!-- 4-column wording -->
<label for="salaire" class="col-md-4 col-form-label">Salaire annuel</label>
<!-- 5-column numeric input field for wages -->
<div class="col-md-5">
<input type="number" min="0" step="1" class="form-control" id="salaire" name="salaire" placeholder="Salaire annuel" aria-describedby="salaireHelp" value="<?= $modèle->salaire ?>">
<small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
</div>
</div>
<!-- fourth row, [submit] button on 5 columns -->
<div class="form-group row">
<div class="col-md-5">
<button type="submit" class="btn btn-primary">Valider</button>
</div>
</div>
</fieldset>
</form>
Kommentare
- Zeile 2: Das HTML-Formular wird (über das Attribut [method]) an die URL [main.php?action=calculer-impot] (das Attribut [action]) gesendet. Die übermittelten Werte sind die Werte der Eingabefelder:
- der Wert des ausgewählten Optionsfelds im Formular:
- [marié=oui], wenn das Optionsfeld [Oui] ausgewählt ist (Zeilen 16–22). [marié] ist der Wert des Attributs [name] in Zeile 18, [oui] ist der Wert des Attributs [value] in Zeile 18;
- [married=no], wenn das Optionsfeld [No] ausgewählt ist (Zeilen 23–28). [married] ist der Wert des Attributs [name] in Zeile 24, und [no] ist der Wert des Attributs [value] in Zeile 24;
- der Wert des numerischen Eingabefelds in Zeile 37 in der Form [children=xx], wobei [children] der Wert des Attributs [name] in Zeile 37 ist und [xx] der vom Benutzer über die Tastatur eingegebene Wert;
- der Wert des numerischen Eingabefelds in Zeile 46 in der Form [salary=xx], wobei [salary] der Wert des Attributs [name] in Zeile 46 ist und [xx] der vom Benutzer über die Tastatur eingegebene Wert;
- der Wert des ausgewählten Optionsfelds im Formular:
Schließlich hat der übermittelte Wert die Form [married=xx&children=yy&salary=zz].
- Die eingegebenen Werte werden übermittelt, sobald der Benutzer in Zeile 53 auf die Schaltfläche [Absenden] klickt;
- Zeilen 16–30: Die beiden Optionsfelder:
![]()
Die beiden Optionsfelder gehören zur selben Optionsfeldgruppe, da sie dasselbe [name]-Attribut haben (Zeilen 18, 24). Der Browser stellt sicher, dass innerhalb einer Optionsfeldgruppe zu jedem Zeitpunkt nur eines ausgewählt ist. Daher wird durch das Klicken auf eines das zuvor ausgewählte deaktiviert;
- Es handelt sich aufgrund des Attributs [type="radio"] um Optionsfelder (Zeilen 18, 24);
- Wenn das Formular angezeigt wird (vor der Eingabe), muss eines der Optionsfelder markiert sein: Fügen Sie dazu einfach das Attribut [checked=’checked’] zum entsprechenden <input type="radio">-Tag hinzu. Dies wird mithilfe dynamischer Variablen erreicht:
- [<?= $model->checkedYes ?>] in Zeile 18;
- [<?= $model->checkedNo ?>] in Zeile 24;
Diese Variablen werden Teil der View-Vorlage sein.
- Zeile 37: ein numerisches Eingabefeld [type="number"] mit einem Mindestwert von 0 [min="0"]. In modernen Browsern bedeutet dies, dass der Benutzer nur eine Zahl >=0 eingeben kann. In denselben modernen Browsern kann die Eingabe über einen Schieberegler erfolgen, der nach oben oder unten geklickt werden kann. Das Attribut [step="1"] in Zeile 37 gibt an, dass der Schieberegler in Schritten von 1 arbeitet. Folglich akzeptiert der Schieberegler nur ganzzahlige Werte im Bereich von 0 bis n in Schritten von 1. Bei manueller Eingabe bedeutet dies, dass Zahlen mit Dezimalstellen nicht akzeptiert werden;
![]()
- Zeile 37: Auf bestimmten Bildschirmen muss das Eingabefeld für Kinder mit dem zuletzt in diesem Feld vorgenommenen Eintrag vorbelegt sein. Dazu verwenden wir das Attribut [value], das den im Eingabefeld anzuzeigenden Wert festlegt. Dieser Wert ist dynamisch und wird durch die Variable [$model→children] generiert;
- Zeile 46: Für die Eingabe des Gehalts gelten dieselben Erläuterungen wie für die Kinder;
- Zeile 53: Die Schaltfläche [submit], die den POST der eingegebenen Werte an die URL [main.php?action=calculer-impot] auslöst;

23.13.3.3. Das Fragment [v-menu.php]
Dieses Fragment zeigt links neben dem Formular zur Steuerberechnung ein Menü an:

Der Code für dieses Fragment lautet wie folgt:
<!-- bootstrap menu -->
<nav class="nav flex-column">
<?php
// affichage d'une liste de liens HTML
foreach($modèle->optionsMenu as $texte=>$url){
print <<<EOT3
<a class="nav-link" href="$url">$texte</a>
EOT3;
}
?>
</nav>
Kommentare
- Zeilen 2–11: Der HTML-Tag [nav] umschließt einen Abschnitt des HTML-Dokuments, der Navigationslinks zu anderen Dokumenten enthält;
- Zeile 7: Der HTML-Tag [a] leitet einen Navigationslink ein:
- [$url]: ist die URL, zu der der Benutzer weitergeleitet wird, wenn er auf den Link [$text] klickt. Der Browser führt dann eine [GET $url]-Operation durch. Wenn [$url] eine relative URL ist, wird ihr die Root der URL vorangestellt, die aktuell in der Adressleiste des Browsers angezeigt wird. Um also den Link [1] zu erstellen, wenn die aktuelle URL des Browsers die Form [http://chemin/main.php?paramètres] hat, erstellen wir den Link:
- Zeile 5: Die Vorlage des Fragments [$modèle→optionsMenu] ist ein Array der Form:
[‘ Liste des simulations’=>’main.php?action=liste-simulations’,
‘ Fin de session’=>’main.php?action=fin-session’]
- Zeilen 2, 7: Die CSS-Klassen [nav, flex-column, nav-link] sind Bootstrap-Klassen, die das Erscheinungsbild des Menüs definieren;
23.13.3.4. Visueller Test
Wir sammeln diese verschiedenen Elemente im Ordner [Tests] und erstellen eine Testvorlage für die Ansicht [view-tax-calculation.php]:

Das Datenmodell für die Ansicht [view-tax-calculation] sieht wie folgt aus:
<?php
// page test data
//
// calculate the view model
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
// form
$modèle->checkedOui = "";
$modèle->checkedNon = 'checked="checked"';
$modèle->enfants = 2;
$modèle->salaire = 300000;
// message of success
$modèle->success = TRUE;
$modèle->impôt = "Montant de l'impôt : 1000 euros";
$modèle->décôte = "Décôte : 15 euros";
$modèle->réduction = "Réduction : 20 euros";
$modèle->surcôte = "Surcôte : 0 euros";
$modèle->taux = "Taux d'imposition : 14 %";
// error message
$modèle->error = TRUE;
$erreurs = ["erreur1", "erreur2"];
// build a HTML list of errors
$content = "";
foreach ($erreurs as $erreur) {
$content .= "<li>$erreur</li>";
}
$modèle->erreurs = $content;
// menu
$modèle->optionsMenu = [
'Liste des simulations' => 'main.php?action=liste-simulations',
'Fin de session' => 'main.php?action=fin-session'];
// banner image
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// we render the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Kommentare
- Zeilen 7–39: Wir initialisieren alle dynamischen Teile der Ansicht [vue-calcul-impot.php] sowie die Komponenten [v-calcul-impot.php] und [v-menu.php];
Wir testen die Ansicht [vue-calcul-impot.php]:

Wir erhalten das folgende Ergebnis:

Wir arbeiten an dieser Ansicht, bis wir mit dem visuellen Ergebnis zufrieden sind. Anschließend können wir die Ansicht in die derzeit in Entwicklung befindliche Webanwendung integrieren.
23.13.3.5. Berechnung des View-Modells

Sobald das visuelle Erscheinungsbild der Ansicht festgelegt ist, können wir damit fortfahren, das Ansichtsmodell unter realen Bedingungen zu berechnen. Sehen wir uns die Statuscodes an, die zu dieser Ansicht führen. Sie sind in der Konfigurationsdatei zu finden:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Diese Statuscodes [200, 300, 341, 350, 800] lösen die Anzeige der Authentifizierungsansicht aus. Um die Bedeutung dieser Codes zu verstehen, können wir uns auf die [Postman]-Tests beziehen, die an der JSON-Anwendung durchgeführt wurden:
- [authenticate-user-200]: 200 ist der Statuscode nach einer erfolgreichen [authenticate-user]-Aktion; anschließend wird das leere Formular zur Steuerberechnung angezeigt;
- [calculate-tax-300]: 300 ist der Statuscode nach einer erfolgreichen [calculate-tax]-Aktion. Das Berechnungsformular wird dann mit den eingegebenen Daten und dem Steuerbetrag angezeigt. Der Benutzer kann dann eine weitere Berechnung durchführen;
- [end-session-400]: 400 ist der Statuscode nach einer erfolgreichen [end-session]-Aktion: Anschließend wird das leere Authentifizierungsformular angezeigt;
- Der Statuscode [341] wird für eine gültige Steuerberechnung zurückgegeben, aber das Fehlen einer Verbindung zum DBMS verursacht einen Fehler;
- Der Statuscode [350] wird bei einer gültigen Steuerberechnung zurückgegeben, doch die fehlende Verbindung zum [Redis]-Server führt zu einem Fehler;
- Der Statuscode [800] wird später vorgestellt. Wir sind ihm bisher noch nicht begegnet;
- Wir sind hier davon ausgegangen, dass der Benutzer einen modernen Browser verwendet. Daher ist es bei dem betrachteten Formular nicht möglich, negative Zahlen, nicht-numerische Zeichenfolgen oder Dezimalzahlen in die Eingabefelder [children, salary] einzugeben. Bei älteren Browsern wäre dies möglich. Wir behandeln diese Fehler als unerwartete Fehler und zeigen die Ansicht [vue-erreurs] an;
Da wir nun wissen, wann das Formular zur Steuerberechnung angezeigt werden soll, können wir dessen Vorlage in [tax-calculation-view.php] berechnen:
<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: the response of the controller that processed the action
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
// application status
$état = $content["état"];
// the model depends on the state
switch ($état) {
case 200 :
case 800:
// initial display of an empty form
$modèle->success = FALSE; $modèle->errror = FALSE;
$modèle->checkedNon = 'checked="checked"';
$modèle->checkedOui = "";
$modèle->enfants = "";
$modèle->salaire = "";
break;
case 300:
// successful calculation - result display
$modèle->success = TRUE;
$modèle->error = FALSE;
$modèle->impôt = "Montant de l'impôt : {$content["réponse"]["impôt"]} euros";
$modèle->décôte = "Décôte : {$content["réponse"]["décôte"]} euros";
$modèle->réduction = "Réduction : {$content["réponse"]["réduction"]} euros";
$modèle->surcôte = "Surcôte : {$content["réponse"]["surcôte"]} euros";
$modèle->taux = "Taux d'imposition : " . ($content["réponse"]["taux"] * 100) . " %";
// form restored with values entered
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
break;
case 341:
// database HS
case 350:
// redis server HS
// form restored with values entered
$modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
$modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
$modèle->enfants = $request->request->get("enfants");
$modèle->salaire = $request->request->get("salaire");
// error
$modèle->success = FALSE;
$modèle->error = TRUE;
$modèle->erreurs = "<li>{$content["réponse"]}</li>";
break;
}
//menu
$modèle->optionsMenu = [
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session"];
// we render the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
<title>Application impots</title>
</head>
<body>
…
</body>
</html>
Kommentare
- Zeilen 22–30: Anzeige eines leeren Formulars;
- Zeilen 31–45: erfolgreiche Steuerberechnung. Die eingegebenen Werte und der Steuerbetrag werden erneut angezeigt;
- Zeilen 46–59: Fall, in dem die Steuerberechnung aufgrund der Nichtverfügbarkeit eines der Server [Redis] oder [MySQL] fehlschlägt;
- Zeilen 62–64: Berechnung der beiden Menüoptionen;
23.13.3.6. [Postman]-Tests
Der Test [calculate-tax-300] gibt den Statuscode 300 zurück, was auf eine erfolgreiche Steuerberechnung hinweist:

- in [3] die Werte, die zu dem Ergebnis [2] geführt haben;
Versuchen wir einen Fehlerfall: Fehler [350], da der [Redis]-Server nicht verfügbar ist:

23.13.4. Die Simulationslistenansicht
23.13.4.1. Übersicht über die Ansicht
Die Ansicht, in der die Liste der Simulationen angezeigt wird, sieht wie folgt aus:

Die vom Skript [vue-liste-simulations] generierte Ansicht besteht aus drei Teilen:
- 1: Das obere Banner wird durch das bereits vorgestellte Fragment [v-bandeau.php] generiert;
- 2: die Tabelle der Simulationen, die vom Fragment [v-simulation-list.php] generiert wird;
- 3: ein Menü mit zwei Links, das vom Fragment [v-menu.php] generiert wird;
Die Simulationsansicht wird durch das folgende Skript [simulation-list-view.php] generiert:

<?php
// calculate the view model
$modèle = getModelForThisView();
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
…
// we render the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- bandeau -->
<?php require "v-bandeau.php"; ?>
<!-- ligne à deux colonnes -->
<div class="row">
<!-- menu sur trois colonnes-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- liste des simulations sur 9 colonnes-->
<div class="col-md-9">
<?php require "v-liste-simulations.php" ?>
</div>
</div>
</div>
</body>
</html>
Kommentare
- Zeile 28: Einfügen des Anwendungsbanners [1];
- Zeile 33: Einfügen des Menüs [2]. Es wird in drei Spalten unterhalb des Banners angezeigt;
- Zeile 37: Einfügen der Simulationstabelle [3]. Sie wird in neun Spalten unterhalb des Banners und rechts neben dem Menü angezeigt;
Wir haben bereits zwei der drei Fragmente dieser Ansicht kommentiert:
Das Fragment [v-liste-simulations.php] lautet wie folgt:
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- simulation table -->
<table class="table table-sm table-hover table-striped">
<!-- headers of the six table columns -->
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Marié</th>
<th scope="col">Nombre d'enfants</th>
<th scope="col">Salaire annuel</th>
<th scope="col">Montant impôt</th>
<th scope="col">Surcôte</th>
<th scope="col">Décôte</th>
<th scope="col">Réduction</th>
<th scope="col">Taux</th>
<th scope="col"></th>
</tr>
</thead>
<!-- table body (data displayed) -->
<tbody>
<?php
$i = 0;
// on affiche chaque simulation en parcourant le tableau des simulations
foreach ($modèle->simulations as $simulation) {
// affichage d'une ligne du tableau avec 6 colonnes - balise <tr>
// colonne 1 : entête ligne (n° simulation) - balise <th scope='row'>
// colonne 2 : valeur paramètre [marié] - balise <td>
// colonne 3 : valeur paramètre [enfants] - balise <td>
// colonne 4 : valeur paramètre [salaire] - balise <td>
// colonne 5 : valeur paramètre [impôt] (de l'impôt) - balise <td>
// colonne 6 : valeur paramètre [surcôte] - balise <td>
// colonne 7 : valeur paramètre [décôte] - balise <td>
// colonne 8 : valeur paramètre [réduction] - balise <td>
// colonne 9 : valeur paramètre [taux] (de l'impôt) - balise <td>
// colonne 10 : lien de suppression de la simulation - balise <td>
print <<<EOT
<tr>
<th scope="row">$i</th>
<td>{$simulation["marié"]}</td>
<td>{$simulation["enfants"]}</td>
<td>{$simulation["salaire"]}</td>
<td>{$simulation["impôt"]}</td>
<td>{$simulation["surcôte"]}</td>
<td>{$simulation["décôte"]}</td>
<td>{$simulation["réduction"]}</td>
<td>{$simulation["taux"]}</td>
<td><a href="main.php?action=supprimer-simulation&numéro=$i">Supprimer</a></td>
</tr>
EOT;
$i++;
}
?>
</tr>
</tbody>
</table>
Kommentare
- Eine HTML-Tabelle wird mit dem Tag <table> erstellt (Zeilen 6 und 58);
- Die Spaltenüberschriften der Tabelle werden durch das <thead>-Tag (Tabellenkopf, Zeilen 8, 21) umschlossen. Das <tr>-Tag (Tabellenzeile, Zeilen 9 und 20) definiert eine Zeile. Zeilen 10–15: Das <th>-Tag (Tabellenkopf) definiert eine Spaltenüberschrift. Es gibt daher zehn davon. [scope="col"] gibt an, dass die Überschrift für die Spalte gilt. [scope="row"] gibt an, dass die Überschrift für die Zeile gilt;
- Zeilen 23–57: Das <tbody>-Tag umschließt die von der Tabelle angezeigten Daten;
- Zeilen 40–51: Das <tr>-Tag umschließt eine Zeile der Tabelle;
- Zeile 41: Das <th scope=’row’>-Tag definiert die Zeilenüberschrift;
- Zeilen 42–50: Jedes td-Tag definiert eine Spalte der Zeile;
- Zeile 27: Die Liste der Simulationen befindet sich im Modell [$model→simulations], einem assoziativen Array;
- Zeile 50: Ein Link zum Löschen der Simulation. Die URL verwendet die Nummer, die in der ersten Spalte der Tabelle (Zeile 41) angezeigt wird;
23.13.4.2. Visueller Test
Wir sammeln diese verschiedenen Elemente im Ordner [Tests] und erstellen eine Testvorlage für die Ansicht [view-simulation-list.php]:

Das Datenmodell für die Ansicht [simulation-list-view] sieht wie folgt aus:
<?php
// calculate the view model
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
// put the simulations in the format expected by the page
$modèle->simulations = [
[
"marié" => "oui",
"enfants" => 2,
"salaire" => 60000,
"impôt" => 448,
"décôte" => 100,
"réduction" => 20,
"surcôte" => 0,
"taux" => 0.14
],
[
"marié" => "non",
"enfants" => 2,
"salaire" => 200000,
"impôt" => 25600,
"décôte" => 0,
"réduction" => 0,
"surcôte" => 8400,
"taux" => 0.45
]
];
// menu options
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// banner image
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// we render the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Kommentare
- Zeilen 9–30: die Tabelle der Simulationen, die durch die HTML-Tabelle angezeigt wird;
- Zeilen 32–34: die Tabelle mit den Menüoptionen;
Zeigen wir diese Ansicht an:

Wir erhalten folgendes Ergebnis:

Wir arbeiten an dieser Ansicht, bis wir mit dem visuellen Ergebnis zufrieden sind. Anschließend können wir die Ansicht in die derzeit in Entwicklung befindliche Webanwendung integrieren.
23.13.4.3. Berechnung des View-Modells

Sobald das visuelle Erscheinungsbild der Ansicht festgelegt ist, können wir damit fortfahren, das Ansichtsmodell unter realen Bedingungen zu berechnen. Sehen wir uns die Statuscodes an, die zu dieser Ansicht führen. Sie sind in der Konfigurationsdatei zu finden:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Es sind also die Statuscodes [500, 600], die die Simulationsansicht anzeigen. Um die Bedeutung dieser Codes zu ermitteln, können wir auf die mit [Postman] durchgeführten Tests der JSON-Anwendung zurückgreifen:
- [list-simulations-500]: 500 ist der Statuscode nach einer erfolgreichen [list-simulations]-Aktion: Die Liste der vom Benutzer durchgeführten Simulationen wird dann angezeigt;
- [delete-simulation-600]: 600 ist der Statuscode nach einer erfolgreichen [delete-simulation]-Aktion. Die nach dieser Löschung erhaltene neue Liste der Simulationen wird dann angezeigt;
Da wir nun wissen, wann die Liste der Simulationen angezeigt werden soll, können wir ihre Vorlage in [view-simulation-list.php] berechnen:
<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: controller response
// no errors possible
// array $content: controller response
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
// put the simulations in the format expected by the page
// they are found in the response of the controller that executed the action
// as an array of objects of type [Simulation]
$objetsSimulation = $content["réponse"];
// each [Simulation] object will be transformed into an associative array
$modèle->simulations = [];
foreach ($objetsSimulation as $objetSimulation) {
$modèle->simulations[] = [
"marié" => $objetSimulation->getMarié(),
"enfants" => $objetSimulation->getEnfants(),
"salaire" => $objetSimulation->getSalaire(),
"impôt" => $objetSimulation->getImpôt(),
"surcôte" => $objetSimulation->getSurcôte(),
"décôte" => $objetSimulation->getdécôte(),
"réduction" => $objetSimulation->getRéduction(),
"taux" => $objetSimulation->getTaux()
];
}
// menu options
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Fin de session" => "main.php?action=fin-session"];
// we render the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Kommentare
- Zeilen 26–36: Berechnung der Vorlage [$template→simulations], die vom Fragment [v-list-simulations.php] verwendet wird;
- Zeilen 39–41: Berechnung der Vorlage [$template→optionsMenu], die vom Fragment [v-menu.php] verwendet wird;
23.13.4.4. [Postman] Tests
Der Test [list-simulations-500] gibt den Statuscode 500 zurück. Er entspricht einer Anfrage zur Anzeige der Simulationen:

Der Test [delete-simulation-600] gibt den Statuscode 600 zurück. Er entspricht dem erfolgreichen Löschen der Simulation Nr. 0. Das zurückgegebene Ergebnis ist eine Liste von Simulationen, in der eine Simulation fehlt:

23.13.5. Anzeigen unerwarteter Fehler
Hier bezeichnen wir einen unerwarteten Fehler als einen Fehler, der bei normaler Nutzung der Webanwendung nicht hätte auftreten dürfen.
Nehmen wir als Beispiel den [Postman]-Test [calculate-tax-3xx], der wie folgt definiert ist:

- in [1-3] eine POST-Anfrage mit der Aktion [calculer-impot];
- in [4-6]: Hier können wir für die drei POST-Parameter beliebige Werte festlegen:
- [4]: Der Parameter [marié] fehlt;
- [5-6]: Die Parameter [children, salary] sind vorhanden, aber ungültig;
- in [9] werden diese drei Fehler mit dem Statuscode 338 gemeldet;
In dem HTML-Formular der Webanwendung kann dieses Szenario jedoch nicht auftreten:
- Alle Parameter sind vorhanden;
- der Parameter [married], der seinen Wert aus den [value]-Attributen zweier Radiobuttons bezieht, muss einen der Werte [yes] oder [no] annehmen;
- bei einem modernen Browser stellen die Attribute <input type='number' min='0' step='1' …> sicher, dass die Eingaben für children und salary zwingend ganze Zahlen >=0 sind;
Nichts hindert einen Benutzer jedoch daran, [Postman] zu verwenden, um den obigen Test [calcul-impot-3xx] an unseren Server zu senden. Wir haben gesehen, dass unsere Webanwendung weiß, wie sie korrekt auf diese Anfrage reagieren muss. Wir bezeichnen einen „unerwarteten Fehler“ als einen Fehler, der im Kontext der HTML-Anwendung nicht auftreten sollte. Tritt er dennoch auf, ist es wahrscheinlich, dass jemand versucht, die Anwendung zu „hacken“. Zu Lehrzwecken haben wir uns entschieden, in diesen Fällen eine Fehlerseite anzuzeigen. In der Praxis könnten wir einfach die zuletzt an den Client gesendete Seite erneut anzeigen. Dazu speichern wir einfach die zuletzt gesendete HTML-Antwort in der Sitzung. Im Falle eines unerwarteten Fehlers geben wir diese Antwort zurück. Auf diese Weise hat der Benutzer den Eindruck, dass der Server nicht auf seine Fehler reagiert, da sich die angezeigte Seite nicht ändert.
23.13.5.1. Präsentation anzeigen
Die Ansicht, die unerwartete Fehler anzeigt, sieht wie folgt aus:

Die vom Skript [vue-erreurs.php] generierte Seite besteht aus drei Teilen:
- 1: Das obere Banner wird durch das bereits vorgestellte Fragment [v-banner.php] generiert;
- 2: die unerwarteten Fehler;
- 3: ein Menü mit drei Links, das vom Fragment [v-menu.php] generiert wird;
Die Ansicht für unerwartete Fehler wird durch das folgende Skript [vue-erreurs.php] generiert:

<?php
// calculate the view model
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
…
// we return the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Application impots</title>
</head>
<body>
<div class="container">
<!-- bandeau sur 12 colonnes -->
<?php require "v-bandeau.php"; ?>
<!-- ligne à deux colonnes -->
<div class="row">
<!-- menu sur 3 colonnes-->
<div class="col-md-3">
<?php require "v-menu.php" ?>
</div>
<!-- liste des erreurs -->
<div class="col-md-9">
<?php
print <<<EOT
<div class="alert alert-danger" role="alert">
Les erreurs inattendues suivantes se sont produites :
<ul>$modèle->erreurs</ul>
</div>
EOT;
?>
</div>
</div>
</div>
</body>
</html>
Kommentare
- Zeile 27: Einfügen des Anwendungsbanners [1];
- Zeile 32: Einfügen des Menüs [2]. Es wird in drei Spalten unterhalb des Banners angezeigt;
- Zeilen 34–44: Anzeige des Fehlerbereichs über neun Spalten;
- Zeilen 37–44: die [print]-Operation, die unerwartete Fehler anzeigt;
- Zeile 38: Diese Anzeige erscheint in einem Bootstrap-Container mit rosa Hintergrund;
- Zeile 39: Einleitungstext;
- Zeile 40: Das Tag <ul> umschließt eine Aufzählungsliste. Diese Aufzählungsliste wird vom Modell [$model->errors] bereitgestellt;
Wir haben bereits die beiden Fragmente dieser Ansicht kommentiert:
- [v-bandeau.php]: im Link-Abschnitt;
- [v-menu.php]: im Link-Abschnitt;
23.13.5.2. Visuelle Tests
Wir sammeln diese verschiedenen Elemente im Ordner [Tests] und erstellen eine Testvorlage für die Ansicht [vue-erreurs.php]:

Das Datenmodell für die Ansicht [vue-erreurs.php] sieht wie folgt aus:
<?php
// calculate the view model
$modèle = getModelForThisView();
function getModelForThisView(): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
// the table of unexpected errors
$erreurs = ["erreur1", "erreur2"];
// build the HTML list of errors
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// menu options
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// banner image
$modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
// we return the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Kommentare
- Zeilen 9–15: Erstellen der HTML-Fehlerliste;
- Zeilen 17–20: das Array mit den Menüoptionen;
Zeigen wir diese Ansicht an:

Wir erhalten folgendes Ergebnis:

Wir arbeiten an dieser Ansicht, bis wir mit dem visuellen Ergebnis zufrieden sind. Anschließend können wir die Ansicht in die derzeit in Entwicklung befindliche Webanwendung integrieren.
23.13.5.3. Berechnung des Ansichtsmodells

Sobald das visuelle Erscheinungsbild der Ansicht festgelegt ist, können wir mit der Berechnung des Ansichtsmodells unter realen Bedingungen fortfahren. Sehen wir uns die Zustandscodes an, die zu dieser Ansicht führen. Sie sind in der Konfigurationsdatei zu finden:
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
Daher sind es die Statuscodes, die nicht in den Zeilen [2–4] aufgeführt sind, die die Anzeige der Ansicht für unerwartete Fehler auslösen.
Der Code zur Berechnung der Ansichtsvorlage [vue-erreurs.php] lautet wie folgt:
<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: controller response
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
// encapsulate paged data in $modèle
$modèle = new \stdClass();
// recover errors from the controller response
$réponse = $content["réponse"];
if (!is_array($réponse)) {
// a single error message
$erreurs = [$réponse];
} else {
// several error messages
$erreurs = $réponse;
}
// build the HTML list of errors
$modèle->erreurs = "";
foreach ($erreurs as $erreur) {
$modèle->erreurs .= "<li>$erreur</li>";
}
// menu options
$modèle->optionsMenu = [
"Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
"Liste des simulations" => "main.php?action=lister-simulations",
"Fin de session" => "main.php?action=fin-session",];
// we return the model
return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
…
</head>
<body>
…
</body>
</html>
Kommentare
- Zeilen 19–32: Berechnung der Vorlage [$template→errors], die von der Ansicht [view-errors.php] verwendet wird;
- Zeilen 34–37: Berechnung der Vorlage [$template→optionsMenu], die vom Fragment [v-menu.php] verwendet wird;
23.13.5.4. Tests [Postman]
Der Test [calculate-tax-3xx] gibt den Statuscode 338 zurück, was kein erwarteter Statuscode ist. Die HTML-Antwort lautet wie folgt:

23.13.6. Implementierung der Menüaktionen der Anwendung
Hier werden wir die Implementierung der Menüaktionen besprechen. Schauen wir uns die Bedeutung der Links an, auf die wir gestoßen sind
Anzeigen | Link | Ziel | Rolle |
Steuerberechnung | [Liste der Simulationen] | [main.php?action=list-simulations] | Liste der Simulationen anfordern |
[Sitzung beenden] | |||
Liste der Simulationen | [Steuerberechnung] | [main.php?action=display-tax-calculation] | Ansicht der Steuerberechnung anzeigen |
[Abmelden] | |||
Unerwartete Fehler | [Steuerberechnung] | [main.php?action=display-tax-calculation] | Ansicht der Steuerberechnung anzeigen |
[Liste der Simulationen] | |||
[Sitzung beenden] |
Es ist wichtig zu beachten, dass das Klicken auf einen Link eine GET-Anfrage an das Ziel des Links auslöst. Die Aktionen [lister-simulations, fin-session] wurden mithilfe einer GET-Operation implementiert, wodurch wir sie als Linkziele verwenden können. Wenn die Aktion über eine POST-Anfrage ausgeführt wird, ist die Verwendung eines Links nicht mehr möglich, es sei denn, sie wird mit JavaScript kombiniert.
Aus den oben genannten Aktionen geht hervor, dass die Aktion [display-tax-calculation] noch nicht implementiert wurde. Hierbei handelt es sich um einen Navigationsvorgang zwischen zwei Ansichten: Der JSON- oder XML-Server hat keinen Grund, dies zu implementieren, da dort das Konzept einer Ansicht nicht existiert. Dieses Konzept wird erst durch den HTML-Server eingeführt.
Wir müssen daher die Aktion [display-tax-calculation] implementieren. Dies ermöglicht es uns, den Ablauf der Implementierung einer Aktion innerhalb des Servers zu überprüfen.
Zunächst müssen wir einen neuen sekundären Controller hinzufügen. Wir nennen ihn [AfficherCalculImpotController]:

Dieser Controller muss zur Konfigurationsdatei [config.json] hinzugefügt werden:
{
"databaseFilename": "database.json",
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
"relativeDependencies": [
…
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
…
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController"
},
…
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
- Zeile 15: der neue Controller;
- Zeile 30: die neue Aktion und ihr Controller;
- Zeile 35: Der neue Controller gibt den Statuscode 800 zurück. Beim Wechseln der Ansichten darf kein Fehler auftreten;
Der Controller [AfficherCalculImpotController.php] sieht wie folgt aus:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Response;
class AfficherCalculImpotController implements InterfaceController {
// $config is the application configuration
// traitement d'une requête Request
// session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// view change - just a status code to set
return [Response::HTTP_OK, 800, ["réponse" => ""], []];
}
}
Kommentare
- Zeile 10: Wie die anderen sekundären Controller implementiert auch der neue Controller die Schnittstelle [InterfaceController];
- Änderungen an der Ansicht lassen sich leicht umsetzen: Geben Sie einfach den mit der Zielansicht verbundenen Statuscode zurück, in diesem Fall den oben gezeigten Code 800;
23.13.7. Praxistests
Der Code wurde geschrieben und jede Aktion mit [Postman] getestet. Wir müssen den View-Ablauf noch in einem realen Szenario testen. Wir benötigen eine Möglichkeit, die HTML-Sitzung zu initialisieren. Wir wissen, dass wir die Parameter [action=init-session&type=html] an den Server senden müssen. Um zu vermeiden, dass wir diese in die Adressleiste des Browsers eingeben müssen, fügen wir das Skript [index.php] zu unserer Anwendung hinzu:

Das Skript [index.php] sieht wie folgt aus:
<?php
// redirect to [main.php] in [html] mode
header('Location: main.php?action=init-session&type=html');
- Zeile 4: [header] ist eine PHP-Funktion, die der Antwort einen HTTP-Header hinzufügt. Der HTTP-Header [Location: main.php?action=init-session&type=html] weist den Client-Browser an, zur in [Location] angegebenen Ziel-URL umzuleiten. Das Skript [index.php] wird mit der URL [http://localhost/php7/scripts-web/impots/version-12/index.php] angefordert. Wenn der Client-Browser die Weiterleitung zur relativen URL [main.php?action=init-session&type=html] erhält, fordert er die absolute URL [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html] an und die HTML-Sitzung wird gestartet;
Die Start-URL kann auf [http://localhost/php7/scripts-web/impots/version-12/] vereinfacht werden. Wenn in der URL keine Seite angegeben ist, werden standardmäßig die Seiten [index.html, index.php] verwendet. In diesem Fall wird daher das Skript [index.php] verwendet;
Los geht’s: Wir stellen nun einige Ansichtssequenzen vor.
In unserem Browser aktivieren wir die Entwicklertools (F12 in Firefox) und rufen die Start-URL [https://localhost/php7/scripts-web/impots/version-12/] auf:

- Bei [4] ist die erste Antwort des Servers eine 302-Weiterleitung:
- Unter [5] wird eine neue Anfrage an die URL [http://localhost/php7/scripts-web/impots/13/main.php?action=init-session&type=html] gesendet;
Schauen wir uns die 302-Weiterleitung einmal genauer an:

- In [8] ist der HTTP-Code [302] ein Weiterleitungscode: Dem Browser des Clients wird mitgeteilt, dass die angeforderte URL verschoben wurde. Die neue URL ist in [9] angegeben. Der Browser folgt dieser Weiterleitung mit einer neuen GET-Anfrage:

- in [12-13], die neue Anfrage des Browsers;
Füllen wir das Formular aus, das wir erhalten haben;

Führen wir dann ein paar Simulationen durch:


Fordern wir die Liste der Simulationen an:

Löschen Sie die erste Simulation:

Beenden Sie die Sitzung:

Der Leser ist eingeladen, weitere Tests durchzuführen.
23.14. jSON-Webservice-Client
23.14.1. Client-Server-Architektur

Wir konzentrieren uns nun auf den JSON-Client [A] des Webdienstes [B]. Der Client [A] weist ebenso wie der Webdienst [B] eine mehrschichtige Struktur auf:

Diese Architektur spiegelt sich in der folgenden Code-Organisation wider:

Die meisten Klassen wurden bereits vorgestellt und erläutert:
Link-Absatz. | |
Link-Absatz. | |
Link-Absatz. | |
Link zum Absatz. | |
Absatz-Link. | |
Link zum Absatz. |
23.14.2. Die [dao]-Ebene

23.14.2.1. Schnittstelle
Die Schnittstelle für die [dao]-Schicht sieht wie folgt aus [InterfaceClientDao.php]:
<?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): Simulation;
// recording results
public function saveResults(string $resultsFilename, array $simulations): void;
// authentication
public function authentifierUtilisateur(String $user, string $password): void;
// list of simulations
public function listerSimulations(): array;
// delete a simulation
public function supprimerSimulation(int $numéro): array;
// start of session
public function initSession(string $type = 'json'): void;
// end of session
public function finSession(): void;
}
Kommentare
- Zeile 9: Die Methode [getTaxPayersData] ermöglicht es Ihnen, die JSON-Datei mit den Steuerzahlerdaten zu verwenden. Diese Methode wird durch das bereits besprochene Trait [TraitDao] implementiert (siehe den verlinkten Absatz);
- Zeile 15: Die Methode [saveResults] speichert die Ergebnisse mehrerer Steuerberechnungen in einer JSON-Datei. Auch hier wird diese Methode durch das bereits besprochene Trait [TraitDao] implementiert (siehe den verlinkten Absatz);
- Zeilen 12, 18, 21, 27, 30: Für jede vom Webdienst akzeptierte Aktion wurde eine Methode erstellt;
23.14.2.2. Implementierung
Die Schnittstelle [InterfaceClientDao] wird durch die folgende Klasse [ClientDao] implementiert:
<?php
namespace Application;
// dependencies
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\CurlResponse;
class ClientDao implements InterfaceClientDao {
// using a Trait
use TraitDao;
// attributes
private $urlServer;
private $sessionCookie;
private $verbose;
// manufacturer
public function __construct(string $urlServer, bool $verbose = TRUE) {
$this->urlServer = $urlServer;
$this->verbose = $verbose;
}
…
}
Kommentare
- Zeilen 18–21: Der Konstruktor erhält zwei Parameter:
- die URL [$urlServer] des JSON-Webdienstes;
- einen booleschen Wert [$verbose], der, wenn er auf TRUE gesetzt ist, angibt, dass die Klasse die Antworten des Servers auf der Konsole anzeigen soll;
- Zeile 14: das Session-Cookie. Seine Funktion wurde in Version 09 des Clients beschrieben (Link im Absatz);
- Zeile 11: Die Klasse verwendet das Trait [TraitDao], das zwei Methoden der Schnittstelle implementiert:
- [getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array];
- [function calculateTax(string $married, int $children, int $salary): Simulation];
23.14.2.2.1. Methode [initSession]
Die Methode [initSession] ist wie folgt implementiert:
public function initSession(string $type = 'json'): void {
// create a HTTP customer
$httpClient = HttpClient::create();
// make the request to the server without authentication
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "init-session",
"type" => $type
],
"verify_peer" => false
]);
// the answer is retrieved
$this->getResponse($response);
// retrieve the session cookie
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// session cookie ?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
Da die Aktion [init-session] die erste vom Webdienst angeforderte Aktion sein muss, wird die Methode [initSession] als erste Methode in der [dao]-Schicht aufgerufen.
Kommentare
- Zeile 1: Der gewünschte Sitzungstyp wird als Parameter übergeben. Wird kein Parameter angegeben, wird eine JSON-Sitzung gestartet;
- Zeilen 5–11: Es wird eine GET-Anfrage an den Webdienst gesendet;
- Zeilen 7–8: Die beiden GET-Parameter;
- Zeile 10: Bei sicherer Kommunikation (HTTPS) wird das vom Webdienst gesendete Sicherheitszertifikat nicht überprüft;
- Zeile 13: Die Methode [getResponse] ruft die Antwort des Servers ab. Sie gibt diese als Array zurück. Hier wird das Ergebnis der Methode nicht verwendet. Die Methode [getResponse] löst eine Ausnahme aus, wenn der HTTP-Statuscode der Antwort des Webdienstes nicht 200 OK lautet;
- Zeilen 14–25: Da die Methode [initSession] die erste Methode in der [dao]-Schicht ist, die ausgeführt wird, rufen wir das Session-Cookie ab, damit nachfolgende Methoden es an den Webdienst zurücksenden können. Dieser Code wurde bereits in Version 09 kommentiert;
23.14.2.2.2. Die Methode [getResponse]
Die Methode [getResponse] ist für die Verarbeitung der Antwort des Webdienstes zuständig:
private function getResponse(CurlResponse $response) {
// the answer is retrieved
$json = $response->getContent(false);
// logs
if ($this->verbose) {
print "$json\n";
}
// retrieve response status
$statusCode = $response->getStatusCode();
// mistake?
if ($statusCode !== 200) {
// we have an error
throw new ExceptionImpots($json);
}
// we give our answer
$array = json_decode($json, true);
return $array["réponse"];
}
Kommentare
- Zeile 1: Die Methode ist privat;
- Zeile 1: Der Parameter der Methode ist die Webservice-Antwort vom Typ [Symfony\Component\HttpClient\Response\CurlResponse], dem Symfony-Antworttyp, wenn [HttpClient] durch [CurlClient] implementiert wird, d. h. durch die [curl]-Bibliothek;
- Zeile 3: Wir rufen die JSON-Antwort vom Server ab. Beachten Sie, dass der Parameter [false] dazu dient, zu verhindern, dass Symfony eine Ausnahme auslöst, wenn der HTTP-Antwortstatus des Servers im Bereich [3xx, 4xx, 5xx] liegt;
- Zeilen 5–7: Befinden wir uns im [$verbose]-Modus, zeigen wir die Antwort des Servers auf der Konsole an;
- Zeilen 9–14: Wenn der HTTP-Antwortstatus des Servers nicht 200 ist, wird eine Ausnahme ausgelöst, wobei die JSON-Antwort des Servers als Fehlermeldung dient;
- Zeile 16: Die JSON-Zeichenkette wird in ein Array dekodiert;
- Zeile 17: Die nützlichen Informationen befinden sich in [$array["response"]];
23.14.2.2.3. Die Methode [authenticateUser]
Die Methode [authenticateUser] sieht wie folgt aus:
public function authentifierUtilisateur(string $user, string $password): void {
// create a HTTP customer
$httpClient = HttpClient::create();
// make a request to the server with authentication
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "authentifier-utilisateur"
],
"body" => [
"user" => $user,
"password" => $password
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// the answer is retrieved
$this->getResponse($response);
}
Kommentare
- Zeile 5: Die Anfrage des Clients ist ein POST;
- Zeilen 6–8: Parameter in der URL;
- Zeilen 9–12: POST-Parameter;
- Zeile 14: das Session-Cookie;
- Zeile 17: Wir lesen die Antwort. Wir wissen, dass die Methode [getResponse] selbst eine Ausnahme auslöst, wenn ein Fehler vorliegt (ein HTTP-Statuscode ungleich 200);
23.14.2.2.4. Die Methode [calculateTax]
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
// create a HTTP customer
$httpClient = HttpClient::create();
// make the request to the server without authentication but with the session cookie
$response = $httpClient->request('POST', $this->urlServer,
["query" => [
"action" => "calculer-impot"],
"body" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// the answer is retrieved
$array = $this->getResponse($response);
return (new Simulation())->setFromArrayOfAttributes($array);
}
Kommentare
- Zeilen 6–7: der einzelne URL-Parameter;
- Zeilen 8–12: die drei POST-Parameter (Zeile 5);
- Zeile 17: Die Antwort wird verarbeitet;
- Zeile 18: Wenn wir diesen Punkt erreichen, bedeutet dies, dass die Methode [getResponse] keine Ausnahme ausgelöst hat. Wir geben ein [Simulation]-Objekt zurück, das mit dem von [getResponse] zurückgegebenen Array initialisiert wurde;
23.14.2.2.5. Die Methode [listerSimulations]
public function listerSimulations(): array {
// create a HTTP customer
$httpClient = HttpClient::create();
// make the request to the server without authentication but with the session cookie
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "lister-simulations"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// the answer is retrieved
return $this->getSimulations($response);
}
Kommentare
- Zeile 5: GET-Methode;
- Zeilen 6–8: der einzige GET-Parameter;
- Zeile 13: Das Abrufen der Simulationen wird von der privaten Methode [getSimulations] übernommen;
23.14.2.2.6. Die Methode [getSimulations]
private function getSimulations(CurlResponse $response): array {
// we retrieve the JSON response
$array = $this->getResponse($response);
// we have an array of associative objects
// we'll turn it into an array of Simulation objects
$simulations = [];
foreach ($array as $simulation) {
$simulations [] = (new Simulation())->setFromArrayOfAttributes($simulation);
}
// we render the Simulation object list
return $simulations;
}
Kommentare
- Zeile 3: Wir rufen das Array aus der Antwort ab. Es handelt sich um ein Array von Arrays, von denen jedes alle Attribute eines [Simulation]-Objekts enthält;
- Zeile 6: Wenn wir diesen Punkt erreichen, bedeutet dies, dass die Methode [getResponse] keine Ausnahme ausgelöst hat;
- Zeilen 6–9: Wir verwenden die Antwort, um ein Array von [Simulation]-Objekten zu erstellen;
- Zeile 11: Wir geben dieses Array zurück;
23.14.2.2.7. Die Methode [DeleteSimulation]
public function supprimerSimulation(int $numéro): array {
// create a HTTP customer
$httpClient = HttpClient::create();
// make the request to the server without authentication but with the session cookie
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "supprimer-simulation",
"numéro" => $numéro
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// the answer is retrieved
return $this->getSimulations($response);
}
Kommentare
- Zeile 5: Es wird eine GET-Anfrage gestellt;
- Zeilen 6–9: Die beiden URL-Parameter;
- Zeile 14: Nach einer Löschung gibt der Server das neue Array mit Simulationen zurück. Wir geben dieses Array zurück;
23.14.2.2.8. Die Methode [endSession]
Eine Sitzung mit dem Webdienst wird normalerweise durch Aufruf der Methode [finSession] beendet:
public function finSession(): void {
// create a HTTP customer
$httpClient = HttpClient::create();
// make the request to the server without authentication but with the session cookie
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"action" => "fin-session"
],
"verify_peer" => false,
"headers" => ["Cookie" => $this->sessionCookie]
]);
// the answer is retrieved
$this->getResponse($response);
}
Kommentare
- Zeile 5: Wir senden eine GET-Anfrage;
- Zeilen 6–8: der einzelne URL-Parameter;
- Zeile 13: Wir lesen die Antwort. Es wird eine Ausnahme ausgelöst, wenn der HTTP-Statuscode der Antwort nicht 200 ist;
23.14.3. Die [Business]-Schicht

23.14.3.1. Die Schnittstelle
Die Schnittstelle für die [Business]-Schicht sieht wie folgt aus [InterfaceClientMetier.php]:
<?php
// namespace
namespace Application;
interface InterfaceClientMetier {
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFilename, string $errorsFileName): void;
// authentication
public function authentifierUtilisateur(String $user, string $password): void;
// list of simulations
public function listerSimulations(): array;
// recording results
public function saveResults(string $resultsFilename, array $simulations): void;
// delete a simulation
public function supprimerSimulation(int $numéro): array;
// start of session
public function initSession(string $type = 'json'): void;
// end of session
public function finSession(): void;
}
Kommentare
- Nur die Methode [executeBatchImports] in Zeile 12 ist spezifisch für die [business]-Schicht. Alle anderen gehören zur [DAO]-Schicht, die sie implementiert;
23.14.3.2. Die Klasse [ClientMetier]
Die Klasse, die die [business]-Schicht implementiert, sieht wie folgt aus:
<?php
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// attribute
private $clientDao;
// manufacturer
public function __construct(InterfaceClientDao $clientDao) {
$this->clientDao = $clientDao;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
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
$simulations = [];
// we exploit them
foreach ($taxPayersData as $taxPayerData) {
// tax calculation
$simulations [] = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
}
// recording results
if ($resultsFileName !== NULL) {
$this->clientDao->saveResults($resultsFileName, $simulations);
}
}
public function authentifierUtilisateur(String $user, string $password): void {
$this->clientDao->authentifierUtilisateur($user, $password);
}
public function listerSimulations(): array {
return $this->clientDao->listerSimulations();
}
public function saveResults(string $resultsFilename, array $simulations): void {
$this->clientDao->saveResults($resultsFilename, $simulations);
}
public function supprimerSimulation(int $numéro): array {
return $this->clientDao->supprimerSimulation($numéro);
}
public function finSession(): void {
$this->clientDao->finSession();
}
public function initSession(string $type = 'json'): void {
$this->clientDao->initSession($type);
}
}
Kommentare
- Zeilen 10–12: Um erstellt zu werden, benötigt die [Business]-Schicht eine Referenz auf die [DAO]-Schicht;
- Zeilen 20–38: Nur die Methode [executeBatchImports] ist spezifisch für die [Business]-Schicht. Die Implementierung der anderen Methoden delegiert die Arbeit an gleichnamige Methoden in der [DAO]-Schicht;
- Zeile 23: Wir rufen die [DAO]-Schicht auf, um Steuerzahlerdaten in einem Array von [TaxPayerData]-Objekten abzurufen;
- Zeile 25: Die verschiedenen berechneten Simulationen werden im Array [$simulations] gesammelt;
- Zeilen 27–33: Wir berechnen die Steuer für jeden Steuerzahler im Array [$taxPayersData];
- Zeilen 35–37: Die im Array [$simulations] erhaltenen Ergebnisse werden in einer JSON-Datei gespeichert;
Hinweis: Die [Business]-Schicht hat so gut wie keine Funktion. Wir könnten sie entfernen und alles in der [DAO]-Schicht zusammenfassen.
23.14.4. Das Hauptskript

Das Hauptskript wird durch die folgende [config.json]-Datei konfiguriert:
{
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12",
"dependencies": [
"/Entities/BaseEntity.php",
"/Entities/TaxPayerData.php",
"/Entities/Simulation.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Utilitaires.php",
"/Model/InterfaceClientDao.php",
"/Model/TraitDao.php",
"/Model/ClientDao.php",
"/Model/InterfaceClientMetier.php",
"/Model/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-12/main.php"
}
Das Hauptskript [main.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.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"]);
// creation of the [business] layer
$clientMetier = new ClientMetier($clientDao);
// tax calculation in batch mode
try {
// session initialization
$clientMetier->initSession('json');
// authentication
$clientMetier->authentifierUtilisateur($config["user"]["login"], $config["user"]["passwd"]);
// tax calculation without saving results
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, NULL, ERRORS_FILENAME);
// list of simulations
$clientMetier->listerSimulations();
// deleting a simulation
$simulations = $clientMetier->supprimerSimulation(1);
// saving results
$clientMetier->saveResults(RESULTS_FILENAME, $simulations);
// end of session
$clientMetier->finSession();
// action without being authenticated - must crash
$clientMetier->listerSimulations();
} catch (ExceptionImpots $ex) {
// error is displayed
print "Une erreur s'est produite : " . $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit();
Kommentare
- Zeilen 12–16: Verarbeiten der Konfigurationsdatei [config.json];
- Zeilen 18–26: Laden aller Abhängigkeiten;
- Zeilen 28–34: Definieren von Konstanten und Aliasen;
- Zeilen 36–39: Erstellen der [dao]- und [business]-Schichten;
- Zeile 44: Initialisierung einer JSON-Sitzung;
- Zeile 46: Authentifizierung beim Server;
- Zeile 48: Berechnung der Steuer für eine Reihe von Steuerzahlern. Die Ergebnisse werden nicht gespeichert (2. Parameter auf NULL gesetzt);
- Zeile 50: Abrufen der Ergebnisse all dieser Berechnungen;
- Zeile 52: Löschen der Simulation Nr. 1 (die zweite in der Liste);
- Zeile 54: Speichern der verbleibenden Simulationen;
- Zeile 56: Die Sitzung wird beendet. Das bedeutet, dass das Sitzungs-Cookie gelöscht wird;
- Zeile 58: Wir fordern die Liste der Simulationen an. Da das Sitzungs-Cookie gelöscht wurde, muss die Authentifizierung erneut durchgeführt werden. Wir sollten daher eine Ausnahme erhalten, die besagt, dass wir nicht authentifiziert sind;
Die Datei [taxpayersdata.json] sieht wie folgt aus:
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
Es gibt 12 Steuerzahler, von denen einer falsch ist. Das ergibt insgesamt 11 Simulationen. Eine davon wird entfernt. Es sollten noch 10 übrig bleiben.
Nach Ausführung des Hauptskripts sieht die JSON-Datei [results.json] wie folgt aus:
[
{
"marié": "oui",
"enfants": "2",
"salaire": "55555",
"impôt": 2814,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "oui",
"enfants": "3",
"salaire": "50000",
"impôt": 0,
"surcôte": 0,
"décôte": 720,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "2",
"salaire": "100000",
"impôt": 19884,
"surcôte": 4480,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "non",
"enfants": "3",
"salaire": "100000",
"impôt": 16782,
"surcôte": 7176,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "3",
"salaire": "100000",
"impôt": 9200,
"surcôte": 2180,
"décôte": 0,
"réduction": 0,
"taux": 0.3
},
{
"marié": "oui",
"enfants": "5",
"salaire": "100000",
"impôt": 4230,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"marié": "non",
"enfants": "0",
"salaire": "100000",
"impôt": 22986,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.41
},
{
"marié": "oui",
"enfants": "2",
"salaire": "30000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
},
{
"marié": "non",
"enfants": "0",
"salaire": "200000",
"impôt": 64210,
"surcôte": 7498,
"décôte": 0,
"réduction": 0,
"taux": 0.45
},
{
"marié": "oui",
"enfants": "3",
"salaire": "20000",
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
}
]
Es gibt tatsächlich 10 Simulationen.
Die JSON-Datei [errors.json] hat folgenden Inhalt:
{
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
Die Konsolenausgabe sieht wie folgt aus (im ausführlichen Modus werden die JSON-Antworten des Servers auf der Konsole angezeigt):
{"action":"init-session","état":700,"réponse":"session démarrée avec type [json]"}
{"action":"authentifier-utilisateur","état":200,"réponse":"Authentification réussie [admin, admin]"}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"lister-simulations","état":500,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"supprimer-simulation","état":600,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"fin-session","état":400,"réponse":"session supprimée"}
{"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Une erreur s'est produite : {"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Terminé
23.14.5. Tests [Codeception]
Wie bei früheren Clients kann der Client der Version 12 mit [Codeception] getestet werden:

Der Code für die Testklasse der [Business]-Schicht des Clients ähnelt dem der Testklassen für frühere Clients:
<?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-12");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config.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";
}
// symfony dependencies
use Symfony\Component\HttpClient\HttpClient;
// test class
class ClientDaoTest extends \Codeception\Test\Unit {
// dao layer
private $clientDao;
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"]);
// creation of the [business] layer
$this->métier = new ClientMetier($clientDao);
// session initialization
$this->métier->initSession("json");
// authentication
$this->métier->authentifierUtilisateur("admin", "admin");
}
// tests
public function test1() {
$simulation = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $simulation->getImpôt(), 1);
$this->assertEqualsWithDelta(0, $simulation->getSurcôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getDécôte(), 1);
$this->assertEqualsWithDelta(0, $simulation->getRéduction(), 1);
$this->assertEquals(0.14, $simulation->getTaux());
}
public function test2() {
….
}
…
public function test11() {
…
}
}
Kommentare
- Zeilen 34–46: Beachten Sie, dass der Konstruktor der Testklasse vor jedem Test ausgeführt wird;
- Zeilen 38–41: Aufbau der [dao]- und [business]-Schichten;
- Zeilen 42–45: Die Testmethoden [test1…, test11] testen die Methode [calculateTax]. Um dies zu ermöglichen, muss zunächst eine JSON-Sitzung initialisiert und die Authentifizierung durchgeführt werden;
Die Testergebnisse lauten wie folgt:

Es sollten noch viele weitere Tests durchgeführt werden:
- Testen Sie die verschiedenen Methoden der [dao]-Schicht;
- Testen Sie die vom Webserver zurückgegebenen Status. Diese Status sind wichtig, da ihr Wert bestimmt, welche HTML-Seite angezeigt wird;