11. Anwendungsübung – Version 4
Die Anwendung zur Steuerberechnung wird die folgende Schichtstruktur implementieren:

Wir werden die Elemente aus Version 3 des Abschnitts „Link“ übernehmen und sie so anpassen, dass sie der neuen Architektur der Anwendung entsprechen. Dies wird manchmal als „Refactoring“ bezeichnet. Wir gehen hier davon aus, dass die für die Anwendung erforderlichen Daten in Textdateien gespeichert sind. Die Schicht [Dao] wird den Austausch mit diesen Dateien übernehmen.
11.1. Verzeichnisstruktur der Skripte

11.2. Zwischen den Schichten ausgetauschte Objekte
Wir werden bestimmte Objekte aus Version 3 beibehalten. Zur Erinnerung führen wir sie hier noch einmal auf.
Die Ausnahme [ExceptionImpots] ist die Ausnahme, die die Schicht [Dao] auslöst, wenn sie auf ein Problem entweder beim Zugriff auf die Daten oder bei der Art der Daten (falsche Daten) stößt.
<?php
// Namensraum
namespace Application;
class ExceptionImpots extends \RuntimeException {
public function __construct(string $message, int $code=0) {
parent::__construct($message, $code);
}
}
Die Klasse [Utilitaires] enthält nützliche Methoden zur Verwaltung von Textdateien (hier eine einzige Methode):
<?php
// Namensraum
namespace Application;
// Eine Klasse mit Hilfsfunktionen
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
// Die Zeilenende-Markierung von $ligne wird entfernt, falls vorhanden
$longueur = strlen($ligne); // Zeilenlänge
while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
$ligne = substr($ligne, 0, $longueur - 1);
$longueur--;
}
// Ende – die Zeile wird zurückgegeben
return($ligne);
}
}
Die Klasse [TaxAdminData] ist die Klasse, die die Daten der Steuerverwaltung kapselt:
<?php
namespace Application;
class TaxAdminData {
// Steuerklassen
private $limites;
private $coeffR;
private $coeffN;
// Konstanten für die Steuerberechnung
private $plafondQfDemiPart;
private $plafondRevenusCelibatairePourReduction;
private $plafondRevenusCouplePourReduction;
private $valeurReducDemiPart;
private $plafondDecoteCelibataire;
private $plafondDecoteCouple;
private $plafondImpotCouplePourDecote;
private $plafondImpotCelibatairePourDecote;
private $abattementDixPourcentMax;
private $abattementDixPourcentMin;
// Initialisierung
public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
// Inhalt der Steuerdatendatei abrufen
$fileContents = \file_get_contents($taxAdminDataFilename);
…
// das Objekt wird zurückgegeben
return $this;
}
private function check($value): \stdClass {
…
return $result;
}
// toString
public function __toString() {
// JSON-Zeichenkette des Objekts
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
// Getter und Setter
public function getLimites() {
return $this->limites;
}
…
public function setLimites($limites) {
$this->limites = $limites;
return $this;
}
…
}
Wir fügen eine neue Klasse [TaxPayerData] hinzu, die die in die Ergebnisdatei geschriebenen Daten kapselt:
<?php
// Namensraum
namespace Application;
// die Datenklasse
class TaxPayerData {
// für die Berechnung der Steuer des Steuerpflichtigen erforderliche Daten
private $marié;
private $enfants;
private $salaire;
// Ergebnisse der Steuerberechnung
private $montant;
private $surcôte;
private $décôte;
private $réduction;
private $taux;
// Setter
public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel) : TaxPayerData{
// für die Steuerberechnung erforderliche Daten des Steuerpflichtigen
$this->marié = $marié;
$this->enfants = $nbEnfants;
$this->salaire = $salaireAnnuel;
// Das Objekt wird initialisiert
return $this;
}
// Getter und Setter
public function getMarié() {
return $this->marié;
}
…
public function setMarié($marié) {
$this->marié = $marié;
return $this;
}
…
// toString
public function __toString() {
// JSON-Zeichenkette des Objekts
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
}
Hinweis: Verwenden Sie die automatische Codegenerierung, um den Konstruktor sowie die Getter und Setter zu generieren (siehe Abschnitt „Link“). Beachten Sie, dass die Setter „fluent“ sind.
11.3. Die Schicht [dao]
Wir befassen uns hier mit der Schicht [1] unserer Anwendung:

11.3.1. Die Schnittstelle [InterfaceDao]
Die Schnittstelle der Schicht [dao] lautet wie folgt: [InterfaceDao.php]:
<?php
// Namensraum
namespace Application;
interface InterfaceDao {
// Auslesen der Steuerzahlerdaten
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// Auslesen der Daten der Steuerbehörde (Steuerklassen)
public function getTaxAdminData(): TaxAdminData;
// Speichern der Ergebnisse
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Anmerkungen
- Die Anforderungen lauten wie folgt:
- Die Daten der Steuerzahler befinden sich in einer Textdatei;
- die Ergebnisse der Steuerberechnung werden in einer Textdatei gespeichert;
- Eventuelle Fehler werden in einer Textdatei protokolliert;
- es ist nicht bekannt, in welchem Format die Daten der Steuerbehörde vorliegen. Für jedes neue Format muss die Schnittstelle [InterfaceDao] durch eine neue Klasse implementiert werden;
- Die Methoden der Schnittstelle, bei denen beim Zugriff auf die Daten ein nicht behebbarer Fehler auftritt, müssen eine Ausnahme vom Typ [ExceptionImpots] auslösen;
- Zeile 9: Die Methode zum Abrufen der Steuerzahlerdaten [statut marital, nombre d’enfants, salaire annuel];
- Der erste Parameter ist der Name der Textdatei, in der sich diese Daten befinden;
- Der zweite Parameter ist der Name der Textdatei, in der eventuell aufgetretene Fehler gespeichert werden sollen;
- Zeile 12: Die Methode zum Abrufen der Daten der Steuerbehörde. Hier werden keine Parameter übergeben, da nicht bekannt ist, wie diese Daten gespeichert sind;
- Zeile 15: Die Methode, mit der die Ergebnisse der Steuerberechnung in einer Textdatei gespeichert werden, deren Name als Parameter übergeben wird;
Beim Schreiben der Schnittstelle [InterfaceDao] ist bekannt, dass es je nach Art der Speicherung der Daten der Steuerbehörde verschiedene Möglichkeiten geben wird, die Methode [getTaxAdminData] zu implementieren. Die Schnittstelle [InterfaceDao] wird daher durch verschiedene Klassen implementiert, von denen jede für eine bestimmte Art der Speicherung dieser Daten zuständig ist (Tabellen, Textdateien, Datenbank, Webservice). Diese abgeleiteten Klassen werden jedoch einen gemeinsamen Code aufweisen, nämlich den der Implementierung der Methoden [getTaxPayersData, saveResults]. Es ist bekannt, dass dieser Anwendungsfall auf zwei Arten implementiert werden kann (siehe Abschnitt „Link“):
- Man erstellt eine abstrakte Klasse C, die den gemeinsamen Code der abgeleiteten Klassen zusammenfasst. Die Klasse C implementiert die Schnittstelle I, aber bestimmte Methoden, die in den abgeleiteten Klassen deklariert werden müssen, sind in der Klasse C als abstrakt deklariert, weshalb die Klasse C selbst abstrakt ist. Anschließend erstellt man die von C abgeleiteten Klassen C1 und C2, die jeweils auf ihre eigene Weise die nicht definierten (abstrakten) Methoden ihrer übergeordneten Klasse C implementieren;
- man erstellt ein Merkmal T, das nahezu identisch mit der abstrakten Klasse C aus der vorherigen Lösung ist. Dieses Merkmal implementiert die Schnittstelle I nicht, da es dies syntaktisch nicht kann. Anschließend werden die Klassen C1 und C2 erstellt, die die Schnittstelle I implementieren und das Trait T verwenden. Diese Klassen müssen lediglich die Methoden der Schnittstelle I implementieren, die nicht durch das von ihnen importierte Trait T implementiert werden;
Für das Beispiel verwenden wir hier ein Trait [TraitDao].
11.3.2. Das Trait [TraitDao]
Der Code des Traits [TraitDao] lautet wie folgt: [TraitDao.php]:
<?php
// Namensraum
namespace Application;
trait TraitDao {
// Lesen der Steuerpflichtigen-Daten
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// Steuerpflichtigen-Tabelle
$taxPayersData = [];
// Fehlertabelle
$errors = [];
// Beim Verarbeiten von Dateien können zahlreiche Fehler auftreten
try {
// Einlesen der Benutzerdaten
// Jede Zeile hat das Format: Familienstand, Anzahl der Kinder, Jahresgehalt
$taxPayersFile = fopen($taxPayersFilename, "r");
if (!$taxPayersFile) {
throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
}
// Die aktuelle Zeile der Benutzerdatendatei wird ausgewertet
// die das Format Familienstand, Anzahl der Kinder, Jahresgehalt hat
$num = 1; // aktuelle Zeilennummer
$nbErreurs = 0; // Anzahl der aufgetretenen Fehler
while ($ligne = fgets($taxPayersFile, 100)) {
// Leere Zeilen werden ignoriert
$ligne = trim($ligne);
if (strlen($ligne) == 0) {
// nächste Zeile
$num++;
// Schleife wiederholen
continue;
}
// Eventuelles Zeilenendezeichen wird entfernt
$ligne = Utilitaires::cutNewLineChar($ligne);
// die drei Felder „verheiratet:Kinder:Gehalt“ werden extrahiert, die $ligne bilden
list($marié, $enfants, $salaire) = explode(",", $ligne);
// man überprüft sie
// Der Familienstand muss „Ja“ oder „Nein“ lauten
$marié = trim(strtolower($marié));
$erreur = ($marié !== "oui" and $marié !== "non");
if (!$erreur) {
// Die Anzahl der Kinder muss eine ganze Zahl sein
$enfants = trim($enfants);
if (!preg_match("/^\d+$/", $enfants)) {
$erreur = TRUE;
} else {
$enfants = (int) $enfants;
}
}
if (!$erreur) {
// Das Gehalt ist eine ganze Zahl ohne Euro-Cent
$salaire = trim($salaire);
if (!preg_match("/^\d+$/", $salaire)) {
$erreur = TRUE;
} else {
$salaire = (int) $salaire;
}
}
// Fehler?
if ($erreur) {
$errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
$nbErreurs++;
} else {
// Die Informationen werden gespeichert
$taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
}
// nächste Zeile
$num++;
}
// Sind wir am Ende der Datei angelangt?
if (!feof($taxPayersFile)) {
// Die Schleife wurde aufgrund eines Lesefehlers verlassen
throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
} else {
// Die Schleife wurde aufgrund der Markierung für das Dateiende verlassen
// Die Fehler werden in einer Textdatei gespeichert
$this->saveString($errorsFilename, implode("\n", $errors));
// Ergebnis der Funktion
return $taxPayersData;
}
} finally {
// Die Datei wird geschlossen, falls sie geöffnet ist
if ($taxPayersFile) {
fclose($taxPayersFile);
}
}
}
// Speichern der Ergebnisse
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// Speichern des Arrays [$taxPayersData] in der Textdatei [$resultsFileName]
// Wenn die Textdatei [$resultsFileName] nicht existiert, wird sie angelegt
$this->saveString($resultsFilename, implode("\n", $taxPayersData));
}
// Speichern der Ergebnisse einer Tabelle in einer Textdatei
private function saveString(string $fileName, string $data): void {
// Speichern der Tabelle [$data] in der Textdatei [$fileName]
// Wenn die Textdatei [$fileName] nicht existiert, wird sie angelegt
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
}
}
}
Anmerkungen
- Zeile 6: Wir definieren hier eine Eigenschaft und keine Klasse;
- Zeilen 9–89: Die Methode [getTaxPayersData] implementiert die gleichnamige Methode der Schnittstelle [InterfaceDao]. Sie ruft aus einer Textdatei namens [$taxPayersFilename] die Daten der Steuerzahler [statut marital, nombre d’enfants, salaire annuel] ab. Sie gibt diese in Form eines Arrays [$taxPayersData] mit Elementen vom Typ [TaxPayerData] zurück (Zeilen 67, 81);
- Die Methode [getTaxPayersData] ist der im Abschnitt „Link“ beschriebenen Methode [AbstractBaseImpots::executeBatchImpots] sehr ähnlich, weist jedoch folgende Unterschiede auf:
- Die Methode [getTaxPayersData] ruft lediglich die Daten der Steuerzahler ab. Sie führt keine Steuerberechnung durch. Dies ist die Aufgabe der Ebene [métier];
- Wie bereits die Methode [executeBatchImpots] meldet sie Fehler. Hier werden die Fehler zunächst in einem Array [$errors] (Zeile 13) gespeichert, das am Ende der Verarbeitung in eine Textdatei geschrieben wird (Zeile 79). Je nach Fall ist es leer oder nicht;
- Im Falle eines nicht behebbaren Fehlers wird eine Ausnahme vom Typ [ExceptionImpots] ausgelöst (Zeilen 20, 75);
- Zeile 73: Zu beachten ist die Verarbeitung nach dem Verlassen der Schleife der Zeilen 26–71. Tatsächlich hat die Funktion [fgets] den Nachteil, dass sie den Booleschen Wert FALSE sowohl dann zurückgibt, wenn beim Einlesen der Zeilen die Dateiende-Markierung erreicht wurde, als auch dann, wenn das Einlesen aufgrund eines Fehlers nicht erfolgreich war. Um zwischen diesen beiden Fällen zu unterscheiden, wird mit der Funktion [feof] geprüft, ob das Dateiende erreicht wurde. Ist das Dateiende noch nicht erreicht, liegt ein Fehler vor, und es wird eine Ausnahme ausgelöst;
- Zeilen 83–88: [finally] wird ausgeführt, unabhängig davon, ob bei der Verarbeitung der Datei eine Ausnahme aufgetreten ist oder nicht;
- Zeile 85: Wenn die Datei geöffnet wurde, hat das „Handle“ [$taxPayersFile] der Datei den booleschen Wert TRUE, andernfalls FALSE;
- Zeilen 99–105: Die private Methode [saveString], die in Zeile 79 verwendet wurde, um das Fehlerarray in einer Textdatei zu speichern;
- Zeile 99: Die Methode [saveString] erhält zwei Parameter:
- [string $filename], den Namen der Textdatei, in die die Daten gespeichert werden sollen;
- [string $data], die Zeichenkette, die in die Textdatei geschrieben werden soll. Diese Zeichenkette besteht aus einer Reihe von Zeilen, die jeweils mit dem Zeilenendezeichen \n enden;
- Zeile 102: Die Funktion PHP [file_puts_contents] schreibt eine Zeichenkette in eine Textdatei. Sie öffnet die Datei, schreibt die Zeichenfolge hinein und schließt die Datei wieder. Sie gibt den Booleschen Wert FALSE zurück, falls ein Fehler aufgetreten ist;
- Zeile 103: Wenn ein Fehler auftritt, wird eine Ausnahme ausgelöst;
- Zeilen 92–96: Implementierung der Methode [saveResults] der Schnittstelle [InterfaceDao]. Dabei wird erneut die private Methode [saveString] verwendet. Hier ist der zweite Parameter von [saveString] eine Zeichenkette, die aus dem Array [$taxPayersData] gebildet wird, dessen Elemente vom Typ [TaxPayerData] sind. Man kann sich fragen, wie das Ergebnis der Operation aussehen wird:
implode("\n", $taxPayersData)
Wir haben in der Klasse [TaxPayerData] (Absatz „Link“) die folgende Methode [__toString] definiert:
public function __toString() {
// JSON-Zeichenkette des Objekts
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
Die Operation
implode("\n", $taxPayersData)
verkettet jedes Element des Arrays [$taxPayersData], das durch dessen Methode [__toString] in eine Zeichenkette umgewandelt wurde, mit dem Zeilenendezeichen \n. Dies ergibt eine Zeichenkette der Form:
json1\njson2\n…
Fazit
Die Eigenschaft [TraitDao] hat zwei der Methoden der Schnittstelle [InterfaceDao] implementiert, nämlich [getTaxPayersData] und [saveResults]:
<?php
// Namensraum
namespace Application;
interface InterfaceDao {
// Auslesen der Steuerpflichtigen-Daten
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// Auslesen der Daten der Steuerbehörde (Steuerklassen)
public function getTaxAdminData(): TaxAdminData;
// Speichern der Ergebnisse
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Nun müssen wir noch die Methode [getTaxAdminData] implementieren, die die Daten der Steuerbehörde abruft.
11.3.3. Die Klasse [ImpotsWithTaxAdminDataInJsonFile]
Die Klasse [ImpotsWithTaxAdminDataInJsonFile] implementiert die Schnittstelle [InterfaceDao] wie folgt:
<?php
// Namensraum
namespace Application;
// Definition einer Klasse ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
// Verwendung eines Merkmals
use TraitDao;
// das Objekt vom Typ TaxAdminData, das die Daten der Steuerklassen enthält
private $taxAdminData;
// der Konstruktor
public function __construct(string $taxAdminDataFilename) {
// Wir möchten das Attribut [$this->taxAdminData] initialisieren
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// gibt die Daten zurück, die die Berechnung der Steuer ermöglichen
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Kommentare
- Zeile 7: Die Klasse [ImpotsWithTaxAdminDataInJsonFile] implementiert die Schnittstelle [InterfaceDao];
- Zeile 9: Die Klasse [ImpotsWithTaxAdminDataInJsonFile] verwendet das Merkmal [traitDao], von dem bekannt ist, dass es die Methoden [getTaxPayersData] und [saveResults] derSchnittstelle [InterfaceDao] implementiert. Somit muss die Klasse [ImpotsWithTaxAdminDataInJsonFile] nur noch die Methode [getTaxAdminData] implementieren, die die Daten der Steuerbehörde abruft;
- Zeile 11: das Attribut vom Typ [TaxAdminData], das von der Methode [getTaxAdminData] der Zeilen 20–22 zurückgegeben wird. Dieses Attribut wird vom Konstruktor der Zeilen 14–17 initialisiert;
Wir sind mit der Schicht [dao] unserer Anwendung fertig: Wir haben nun eine Klasse, die die von uns festgelegte Schnittstelle [InterfaceDao] vollständig implementiert. Wir können nun zur Schicht [métier] übergehen.
11.4. Die Schicht [métier]
Wir werden nun die Schicht [2] unserer Architektur implementieren:

11.4.1. Die Schnittstelle [InterfaceMétier]
Die Schnittstelle der Schicht [métier] sieht wie folgt aus:
<?php
// Namensraum
namespace Application;
interface InterfaceMetier {
// Berechnung der Steuern eines Steuerpflichtigen
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// Steuerberechnung im Batch-Modus
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
Anmerkungen
- Zeile 9: Die Schnittstelle [InterfaceMétier] kann den Steuerbetrag eines bestimmten Steuerpflichtigen berechnen, sofern folgende Informationen vorliegen: Familienstand, Anzahl der Kinder, Jahresgehalt. Die Methode [calculerImpot] nutzt die Schicht [dao] nicht und löst daher keine Ausnahmen aus;
- Zeile 9: Die Schnittstelle [InterfaceMétier] kann ebenfalls den Steuerbetrag für eine Gruppe von Steuerzahlern berechnen, deren Daten in der Textdatei mit dem Namen [$taxPayersFileName] zusammengefasst sind. Sie speichert die Ergebnisse in einer Textdatei mit dem Namen [$resultsFileName]. Die Methode [executeBatchImpots] muss die Schicht [dao] ansprechen, die für den Zugriff auf das Dateisystem zuständig ist. Ausnahmen können dann von der Schicht [dao] weitergeleitet werden, die von der Methode [executeBatchImpots] nicht abgefangen werden: Sie lässt sie an das Hauptskript weiterleiten. Nicht schwerwiegende Fehler werden in der Textdatei mit dem Namen [$errorsFileName] protokolliert;
- Zeile 9: Die Methode [calculerImpot] ist eine reine [métier]-Methode. Sie kümmert sich nicht darum, woher die von ihr verwendeten Daten stammen;
- Zeile 12: Die Methode [executeBatchImpots] wendet sich an die Schicht [dao], um Daten aus Textdateien zu lesen und in diese zu schreiben. Sie ruft wiederholt die Geschäftslogik-Methode [calculerImpot] auf;
11.4.2. Die Klasse [Metier]
Die Klasse [Metier] implementiert die Schnittstelle [InterfaceMetier] wie folgt:
<?php
// Namensraum
namespace Application;
class Metier implements InterfaceMetier {
// Dao-Ebene
private $dao;
// Daten der Steuerverwaltung
private $taxAdminData;
//---------------------------------------------
// Setter der Schicht [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// Es wird eine Referenz auf die Schicht [dao] gespeichert
$this->dao = $dao;
// Die Daten für die Steuerberechnung werden abgerufen
// Die Methode [getTaxAdminData] kann eine Ausnahme ExceptionImpots auslösen
// Die Ausnahme wird dann an den aufrufenden Code weitergeleitet
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// Steuerberechnung
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// Ergebnis
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
…
// Ergebnis
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=Jahresgehalt-Freibetrag
// Der Freibetrag hat einen Mindest- und einen Höchstwert
private function getRevenuImposable(float $salaire): float {
…
// Ergebnis
return floor($revenuImposable);
}
// berechnet einen eventuellen Abschlag
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// Ergebnis
return ceil($décôte);
}
// berechnet einen eventuellen Abschlag
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
…
// Ergebnis
return ceil($réduction);
}
// Steuerberechnung im Batch-Modus
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
…
// Ergebnisse speichern
$this->dao->saveResults($resultsFileName, $results);
}
}
Kommentare
- Zeile 6: Die Klasse [Metier] implementiert die Schnittstelle [InterfaceMetier], d. h. die Methoden [calculerImpot] (Zeilen 30–34) und [executeBatchImpots] (Zeilen 66–70);
- Zeile 8: eine Referenz auf die Schicht [dao]. Diese ist zwingend erforderlich, damit die Schicht [métier] weiß, an wen sie sich wenden muss, wenn sie externe Daten anfordert. Dieses Attribut wird über den Setter in den Zeilen 14–17 oder über den Konstruktor in den Zeilen 19–26 initialisiert;
- Zeile 10: Das Objekt vom Typ [TaxAdminData], das die Daten der Steuerbehörde kapselt. Diese Daten werden von der Geschäftsmethode [calculerImpot] benötigt. Dieses Attribut wird über den Konstruktor in den Zeilen 19–26 initialisiert;
- Zeilen 19–26: Der Konstruktor initialisiert die beiden Attribute der Klasse:
- Das Attribut [$dao] wird mit der als Parameter an den Konstruktor übergebenen Referenz initialisiert. Es ist zu beachten, dass der Typ dieses Parameters dem der Schnittstelle [InterfaceDao] entspricht, wodurch die Klasse [Metier] von jeder Klasse initialisiert werden kann, die diese Schnittstelle implementiert;
- Das Attribut [$taxAdminData] wird durch Aufruf der Methode [getTaxAdminData] der Schicht [dao] initialisiert;
Daraus lässt sich schließen, dass bei der Ausführung der Methoden [calculerImpots] und [executeBatchImpots] die beiden Attribute [$dao] und [$taxAdminData] initialisiert werden.
Die Methode [calculerImpots] lautet wie folgt:
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié: ja, nein
// $enfants: Anzahl der Kinder
// $salaire: Jahresgehalt
// $this->taxAdminData: Daten der Steuerbehörde
//
// Es wird überprüft, ob die Daten der Steuerbehörde vorliegen
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// Steuerberechnung mit Kindern
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// Berechnung der Steuer ohne Kinder
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// Anwendung der Obergrenze für den Familienquotienten
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART Euro für die ersten beiden Kinder
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART Euro für die ersten beiden Kinder, das Doppelte für die folgenden
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// es gilt der höhere Steuersatz
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// Berechnung eines eventuellen Steuerabzugs
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// Berechnung einer möglichen Steuerermäßigung
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// Ergebnis
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
Anmerkungen
- Dieser Code stammt aus der Methode [AbstractBaseImpots::calculerImpot] der Version 3, die im Abschnitt „Link“ erläutert wird. Gleiches gilt für die privaten Methoden [calculerImpot2, getDecôte, getRéduction, getRevenuImposable];
Die Methode [Metier::executeBatchImpots] lautet wie folgt:
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// Ausnahmen, die aus der Ebene [dao] stammen, werden weitergeleitet
// Steuerpflichtige Daten werden abgerufen
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// Ergebnistabelle
$results = [];
// Auswertung der Ergebnisse
foreach ($taxPayersData as $taxPayerData) {
// Die Steuer wird berechnet
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// [$taxPayerData] wird vervollständigt
$taxPayerData->setMontant($result["impôt"]);
$taxPayerData->setDécôte($result["décôte"]);
$taxPayerData->setSurCôte($result["surcôte"]);
$taxPayerData->setTaux($result["taux"]);
$taxPayerData->setRéduction($result["réduction"]);
// das Ergebnis wird in die Ergebnistabelle eingetragen
$results [] = $taxPayerData;
}
// Speichern der Ergebnisse
$this->dao->saveResults($resultsFileName, $results);
}
Anmerkungen
- Zeile 1: Die Methode muss für jeden der in der Textdatei mit dem Namen [$taxPayersFileName] gefundenen Steuerpflichtigen wiederholt die Methode [calculerImpot] aufrufen. Die Ergebnisse müssen in die Textdatei mit dem Namen [$resultsFileName] geschrieben werden. Aufgetretene nicht schwerwiegende Fehler werden in der Textdatei mit dem Namen [$errorsFileName] protokolliert. Die Methode löst selbst keine Ausnahmen aus, sondern lässt diejenigen weiterleiten, die von der Schicht [dao] ausgelöst werden;
- Zeile 4: Die Daten der Steuerzahler werden von der Schicht „[dao]“ angefordert. Diese gibt ein Array von Elementen des Typs „[TaxPayerData]“ zurück, bei dem es sich um eine Attributklasse „[marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux]“ handelt (siehe Abschnitt „Link“). Tritt hier eine Ausnahme auf, wird diese – da sie nicht von einem catch abgefangen wird – automatisch an den aufrufenden Code weitergeleitet. Das bedeutet, dass im Falle einer Ausnahme Zeile 6 nicht ausgeführt wird;
- Zeile 6: das Ergebnisarray vom Typ [TaxPayerData];
- Zeilen 8–22: Die Steuer wird für jedes Element des Steuerzahler-Arrays [$taxPayersData] berechnet. Dazu wird die interne Methode [calculerImpot] aufgerufen (Zeile 10);
- Zeilen 15–19: Das erhaltene Ergebnis wird verwendet, um die noch nicht initialisierten Attribute von [TaxPayerData] zu initialisieren;
- Zeile 21: Das erhaltene Ergebnis wird im Ergebnistableau [$results] kumuliert;
- Zeile 24: Sobald die Steuer für alle Steuerzahler berechnet wurde, werden die Ergebnisse in einer Textdatei gespeichert. Diese Aufgabe übernimmt die Ebene [dao];
Fazit
Im Allgemeinen ist die Schicht [métier] recht einfach zu programmieren, da sie auf die Schicht [dao] zugreift, die wiederum den Datenzugriff samt der dazugehörigen Fehlerbehandlung übernimmt.
11.5. Das Hauptskript
Nun schreiben wir das Skript für die Schicht [3] unserer Architektur:

Das Hauptskript lautet wie folgt: [main.php]:
<?php
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
// Namensraum
namespace Application;
// Fehlerbehandlung durch PHP
//ini_set("display_errors", "0");
// Einbindung von Schnittstellen und Klassen
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// Test -----------------------------------------------------
// Definition der Konstanten
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// Erstellung der Schicht [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// Erstellung der Ebene [métier]
$métier = new Metier($dao);
// Steuerberechnung im Batch-Modus
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// Anzeige des Fehlers
print $ex->getMessage() . "\n";
}
// Ende
print "Terminé\n";
exit;
Kommentare
- Zeile 24: Name der Steuerzahlerdatei;
- Zeile 25: Name der Ergebnisdatei;
- Zeile 26: Name der Fehlerdatei;
- Zeile 27: Name der Datei „jSON“, die die Daten der Steuerbehörde enthält;
- Zeile 31: Erstellung der Ebene [dao];
- Zeile 33: Erstellung der Ebene [métier] auf der Grundlage dieser Ebene [dao];
- Zeile 35: Ausführung der Methode [executeBatchImpots] der Schicht [métier];
- Zeilen 36–39: Wir haben gesehen, dass die Schicht [métier] Ausnahmen weiterleiten kann. Diese werden hier abgefangen;
11.6. Visuelle Tests
11.6.1. Test Nr. 1
Mit der folgenden Steuerzahlerdatei [taxpayersdata.txt]:
oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000
erhält man die folgende Fehlerdatei [errors.txt]:
la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
sowie die folgende Ergebnisdatei [resultats.txt]:
11.6.2. Test Nr. 2
Im Hauptskript gibt man für die Steuerzahlerdatei einen Dateinamen an, der nicht existiert:
Die in der Konsole angezeigten Ergebnisse lauten dann wie folgt:
Warning: fopen(taxpayersdata2.txt): failed to open stream: No such file or directory in C:\Data\st-2019\dev\php7\poly\scripts-console\impots\version-04\TraitDao.php on line 18
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
Done.
- Zeile 1: Warnungen (warning) des Interpreters PHP;
- Zeile 2: Fehlermeldung der von der Schicht ausgelösten Ausnahme [dao];
Es ist möglich, die Fehlermeldungen des Interpreters PHP zu unterdrücken:

Zeile 21 des obigen Codes sorgt dafür, dass die Fehler PHP nicht angezeigt werden. Während der Entwicklungsphase müssen sie angezeigt werden. Im Produktionsmodus müssen sie ausgeblendet werden.
Die Ergebnisse der Ausführung lauten dann wie folgt:
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
11.7. Tests [Codeception]
Die visuellen Tests sind völlig unzureichend:
- Man beschränkt sich in der Regel auf wenige Tests;
- man ist bei dieser visuellen Überprüfung mehr oder weniger aufmerksam, und Details können uns entgehen;
In der Praxis der professionellen Softwareentwicklung werden Tests von spezialisierten Mitarbeitern erstellt, für die dies die Hauptaufgabe ist. Sie sind bestrebt, die Tests so umfassend wie möglich zu gestalten. Dazu nutzen sie Test-Frameworks.
Wir werden hier das Framework Codeception [https://codeception.com/] verwenden, da es in NetBeans integriert werden kann. Es handelt sich um ein Framework mit einem breiten Spektrum an Möglichkeiten. Wir werden jedoch nur einige davon nutzen. Die Idee dahinter ist, nach jeder neuen Version der Anwendungsübung schnell überprüfen zu können, ob diese funktioniert. Erfolgreiche Tests geben dem Entwickler Vertrauen in den von ihm geschriebenen Code. Das ist ein wichtiger Faktor.
11.7.1. Installation des Frameworks [Codeception]
Wie viele PHP-Bibliotheken wird auch das Framework [Codeception] zusammen mit [Composer] installiert. Wir öffnen daher ein Laragon-Terminal (siehe Abschnitt „Link“).
Zunächst müssen wir das Test-Framework PHPUnit [https://phpunit.de/] installieren. Denn Codeception nutzt im Hintergrund das Framework PHPUnit:

Anschließend installieren wir das Codeception-Framework:

Das war’s schon. Sehen wir uns nun die Integration von [Codeception] in NetBeans an.
11.7.2. Integration von [CodeCeption] in NetBeans

- In [1-2] ruft man die Projekteigenschaften auf;
- mit [3-4] wird [Codeception] zu einem der Test-Frameworks des Projekts gemacht;


- In [5-8] wird das Framework [Codeception] für das Projekt initialisiert;

- In [9] wurde ein Ordner [tests] sowie eine Konfigurationsdatei [codeception.yml] in [10-11] erstellt. Die Datei „[11]“ ist identisch mit der Datei „[10]“. Codeception hat lediglich einen Ordner „[Important Files]“ erstellt, um der Datei „[10]“ eine besondere Bedeutung zu geben;
- bei [12-13] kehren wir zu den Projekteigenschaften zurück;

- In [14-16] wird der Ordner [tests] [16] als Testordner des Projekts festgelegt;
- In [16] erscheint der Ordner [tests] dann unter dem neuen Namen [Test Files]. Das Vorhandensein dieses Ordners in einem Projekt PHP zeigt, dass dieses Projekt ein Framework für programmierte Tests enthält;
- wir werden unsere Tests im Ordner [unit] [17] erstellen;
11.7.3. Tests der Schicht [dao]

- Wir werden alle unsere Tests im Ordner „[unit] [1]“ erstellen;
- Die Namen der Testklassen [Codeception] müssen mit dem Schlüsselwort [Test] enden, andernfalls werden die Klassen nicht als Testklassen erkannt;
Unsere Testklassen [Codeception] werden folgende Form haben: [https://codeception.com/docs/05-UnitTests]:
<?php
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
// Namensraum
namespace Application;
// Laden der Testumgebung
…
class DaoTest extends \Codeception\Test\Unit {
// Testattribute
private $attribut1;
public function __construct() {
parent::__construct();
// Initialisierung der Testumgebung
…
}
// Tests
public function testTaxAdminData() {
// Tests
$this->assertEquals($expected, $actual);
$this->assertEqualsWithDelta($expected, $actual, $delta);
$this->assertTrue($actual);
$this->assertFalse($actual);
$this->assertNull($actual);
$this->assertEmpty($actual);
$this→assertSame($expected, $actual);
…
}
}
Anmerkungen
- Zeile 7: Die Testklassen befinden sich im selben Namensraum wie die getestete Anwendung;
- Zeilen 9–10: Hier befinden sich die Operationen [require] zum Laden der getesteten Klassen und Schnittstellen;
- Zeile 12: Der Name der Testklasse muss zwingend mit dem Schlüsselwort [Test] enden. Diese Klasse muss die Klasse [\Codeception\Test\Unit] erweitern;
- Zeilen 16–20: Mit dem Konstruktor können wir die Testumgebung initialisieren;
- Zeile 23: Die Namen der Testmethoden müssen zwingend mit dem Schlüsselwort [test] beginnen;
- Zeilen 25–31: Es können verschiedene Testmethoden verwendet werden;
Die Testklasse [DaoTest] sieht wie folgt aus:
<?php
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
// Namensraum
namespace Application;
// Konstanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Einbindung von Schnittstellen und Klassen
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// Test -----------------------------------------------------
// Definition der Konstanten
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// Erstellung der Ebene [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// Tests
public function testTaxAdminData() {
…
}
}
Anmerkungen
Um die Tests für eine Version der Anwendungsübung zu erstellen, verwenden wir eine Umgebung, die mit der des Hauptskripts der jeweiligen Version identisch ist. Für Version 04 ist dies das folgende Skript [main.php]:
<?php
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
// Namensraum
namespace Application;
// Fehlerbehandlung durch PHP
ini_set("display_errors", "0");
// Einbindung von Schnittstellen und Klassen
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// Test -----------------------------------------------------
// Definition der Konstanten
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// Erstellung der Ebene [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// Erstellung der Ebene [métier]
$métier = new Metier($dao);
// Steuerberechnung im Batch-Modus
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// Fehler wird angezeigt
print $ex->getMessage() . "\n";
}
// Ende
print "Terminé\n";
exit;
Um die Schicht [dao] in der Testklasse zu testen:
- die Umgebung aus den Zeilen 13–27 von [main.php];
- Im Konstruktor der Testklasse erstellen wir die Schicht [dao] wie in Zeile 31;
- wir schreiben die Testmethoden;
Wir gehen bei allen Testklassen auf diese Weise vor.
Kehren wir zum vollständigen Code der Testklasse zurück:
<?php
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
// Namensraum
namespace Application;
// Konstanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Einbindung von Schnittstellen und Klassen
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// Test -----------------------------------------------------
// Definition von Konstanten
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// Erstellung der Schicht [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// Tests
public function testTaxAdminData() {
// Berechnungskonstanten
$this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
$this->assertEquals(21037, $this->taxAdminData->getPlafondRevenusCelibatairePourReduction());
$this->assertEquals(42074, $this->taxAdminData->getPlafondRevenusCouplePourReduction());
$this->assertEquals(3797, $this->taxAdminData->getValeurReducDemiPart());
$this->assertEquals(1196, $this->taxAdminData->getPlafondDecoteCelibataire());
$this->assertEquals(1970, $this->taxAdminData->getPlafondDecoteCouple());
$this->assertEquals(1595, $this->taxAdminData->getPlafondImpotCelibatairePourDecote());
$this->assertEquals(2627, $this->taxAdminData->getPlafondImpotCouplePourDecote());
$this->assertEquals(12502, $this->taxAdminData->getAbattementDixPourcentMax());
$this->assertEquals(437, $this->taxAdminData->getAbattementDixPourcentMin());
// Steuerklassen
$this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $this->taxAdminData->getLimites());
$this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $this->taxAdminData->getCoeffR());
$this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $this->taxAdminData->getCoeffN());
}
}
Kommentare
- Zeilen 10–25: Laden der für die Tests erforderlichen Umgebung und Definition von Konstanten;
- Zeilen 31–36: Aufbau der Schicht [dao] (Zeile 34), anschließend Initialisierung des Attributs [$taxAdminData] aus Zeile 29. Dieses Attribut enthält die Daten der Steuerbehörde;
- Zeilen 39–55: die einzige Testmethode. Dabei wird überprüft, ob der Inhalt des Attributs [$taxAdminData] den Erwartungen entspricht;
- Zeilen 41–50: Überprüfung der Konstanten für die Steuerberechnung;
- Zeilen 52–55: Überprüfung der Steuerklassen. Die Methode [assertSame] überprüft, ob zwei Entitäten PHP – in diesem Fall Tabellen – identisch sind;
Um diese Testklasse auszuführen, gehen Sie wie folgt vor:

- In [1-2] wird der Test ausgeführt;
- [3]: das Fenster mit den Testergebnissen;
- [4]: die ausgeführte Testklasse;
- [5]: die Ergebnisse. Hier wurde die einzige Testmethode erfolgreich abgeschlossen;
- [6]: Wenn der Test fehlschlägt oder – was häufiger vorkommt – wenn kein Test ausgeführt wurde, muss man das Fenster [6] aufrufen. Meistens ist das Laden der Testumgebung fehlgeschlagen, sodass kein Test ausgeführt werden konnte. Die in [6] angezeigten Fehler sind dieselben, die bei der Ausführung eines klassischen Skripts PHP auftreten würden;
Sehen wir uns ein Beispiel für einen fehlerhaften Test an:
In der Testklasse fügen wir einen Fehler in die Definition einer Konstante ein:
// Konstanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");
Anschließend führen wir den Test aus. Das Ergebnis sieht wie folgt aus:

Im Fenster [4]:

11.7.4. Tests der Schicht [métier]
Die Testklasse [MetierTest] folgt denselben Konstruktionsregeln wie die Klasse [DaoTest], enthält jedoch mehr Testmethoden:
<?php
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
// Namensraum
namespace Application;
// Konstanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Einbindung von Schnittstellen und Klassen
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// Test -----------------------------------------------------
// Definition der Konstanten
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class MetierTest extends \Codeception\Test\Unit {
// Geschäftsschicht
private $métier;
public function __construct() {
parent::__construct();
// Erstellung der Schicht [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
// Erstellung der Schicht [métier]
$this->métier = new Metier($dao);
}
// Tests
public function test1() {
$result = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test2() {
$result = $this->métier->calculerImpot("oui", 2, 50000);
$this->assertEqualsWithDelta(1385, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(384, $result["décôte"], 1);
$this->assertEqualsWithDelta(347, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test3() {
$result = $this->métier->calculerImpot("oui", 3, 50000);
$this->assertEqualsWithDelta(0, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(720, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test4() {
$result = $this->métier->calculerImpot("non", 2, 100000);
$this->assertEqualsWithDelta(19884, $result["impôt"], 1);
$this->assertEqualsWithDelta(4480, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test5() {
$result = $this->métier->calculerImpot("non", 3, 100000);
$this->assertEqualsWithDelta(16782, $result["impôt"], 1);
$this->assertEqualsWithDelta(7176, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test6() {
$result = $this->métier->calculerImpot("oui", 3, 100000);
$this->assertEqualsWithDelta(9200, $result["impôt"], 1);
$this->assertEqualsWithDelta(2180, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.3, $result["taux"]);
}
public function test7() {
$result = $this->métier->calculerImpot("oui", 5, 100000);
$this->assertEqualsWithDelta(4230, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test8() {
$result = $this->métier->calculerImpot("non", 0, 100000);
$this->assertEqualsWithDelta(22986, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test9() {
$result = $this->métier->calculerImpot("oui", 2, 30000);
$this->assertEqualsWithDelta(0, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0, $result["taux"]);
}
public function test10() {
$result = $this->métier->calculerImpot("non", 0, 200000);
$this->assertEqualsWithDelta(64210, $result["impôt"], 1);
$this->assertEqualsWithDelta(7498, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.45, $result["taux"]);
}
public function test11() {
$result = $this->métier->calculerImpot("oui", 3, 200000);
$this->assertEqualsWithDelta(42842, $result["impôt"], 1);
$this->assertEqualsWithDelta(17283, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
}
Kommentare
- Zeilen 10–25: Laden der Dateien, die die Testumgebung definieren. Diese ist dieselbe wie für die Schicht [dao];
- Zeilen 31–37: Instanziierung der Schichten [dao] und [métier];
- Zeilen 40–47: Ein Test zur Steuerberechnung;
- Zeile 41: Eine bestimmte Steuerberechnung wird mit der Schicht [métier] durchgeführt;
- Zeilen 42–46: Es wird überprüft, ob die erhaltenen Ergebnisse mit denen des Simulators der Steuerbehörde [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm] übereinstimmen;
- Zeilen 23–26: Die Gleichheitstests werden auf 1 Euro genau durchgeführt. Tatsächlich hat sich gezeigt, dass Rundungsprobleme dazu führten, dass der Algorithmus des Dokuments die erwarteten Ergebnisse auf 1 Euro genau lieferte;
- Zeile 27: Der Steuersatz wird ohne Fehlermarge berechnet;
- Zeilen 49–137: Diese Art von Tests wird zehnmal wiederholt, wobei jedes Mal eine andere Steuerpflichtigenkonfiguration zugrunde gelegt wird;
Die Tests liefern folgende Ergebnisse:

11.7.5. Tests der nächsten Versionen
Im weiteren Verlauf werden die Tests der Schichten [dao] und [métier] identisch mit denen der Version 04 sein. Lediglich die Testumgebung wird sich ändern. Wir werden daher nur diese sowie die Testergebnisse vorstellen.