18. Toepassingsopdracht – versie 8
We gaan de voorbeeldtoepassing – versie 5 (paragraaf ‘link’) weer oppakken en er een client/server-toepassing van maken.
18.1. Introduction
De architectuur van versie 5 zag er als volgt uit:

- de laag met de naam [dao] (Data Access Objects) zorgt voor de communicatie met de database MySQL en het lokale bestandssysteem;
- de laag met de naam [métier] voert de belastingberekening uit;
- het hoofdscript fungeert als regisseur: het instantiëert de lagen [dao] en [métier] en communiceert vervolgens met de laag [métier] om de nodige taken uit te voeren;
We gaan deze architectuur migreren naar de volgende client/server-architectuur:

- In [2] zullen we de laag [dao] uit versie 5 overnemen en daaruit de methoden voor toegang tot het lokale bestandssysteem verwijderen. Deze methoden worden gemigreerd naar de laag [dao] van de client [6, 7];
- in [3], de laag [métier] blijft die van versie 5 zonder de methoden [executeBatchImpôts, saveResults], die worden gemigreerd naar de laag [dao] en [7] van de client;
- in [4] moet het serverscript worden geschreven: het moet:
- de lagen [métier], [dao] en [3, 2] aanmaken;
- communiceren met het clientscript [5, 7];
- in [7] moet de clientlaag [dao] worden geschreven:
- deze zal een client HTTP zijn van het serverscript [4, 5] ;
- deze zal de methoden voor toegang tot het lokale bestandssysteem van de laag [dao] uit versie 5 overnemen;
- in [8] zal de [métier]-laag van de client voldoen aan de [InterfaceMetier]-interface van versie 5. De implementatie ervan zal echter anders zijn. In versie 5 voerde de laag [métier] de belastingberekening uit. Hier voert de laag [métier] van de server deze berekening uit. De laag [métier] zal dus een beroep doen op de lagen [dao] en [7] om met de server te communiceren en deze te vragen de belasting te berekenen;
- in [9] moet het consolescript de clientlagen [dao, métier] instantiëren en de uitvoering ervan starten;
18.2. De server
We richten ons op het servergedeelte van de applicatie.

Deze architectuur wordt geïmplementeerd door de volgende scripts:

18.2.1. De entiteiten die tussen de lagen worden uitgewisseld

De entiteiten die tussen de lagen worden uitgewisseld, zijn die van versie 5, zoals beschreven in de paragraaf 'link'.
18.2.2. De laag [dao]

De laag [dao] implementeert de volgende interface [InterfaceServerDao]:
<?php
// naamruimte
namespace Application;
interface InterfaceServerDao {
// gegevens van de belastingdienst uitlezen
public function getTaxAdminData(): TaxAdminData;
}
- regel 9: de methode [getTaxAdminData] haalt de gegevens van de belastingdienst op uit een database;
De interface [InterfaceServerDao] wordt geïmplementeerd door de volgende klasse [ServerDao]:
<?php
// naamruimte
namespace Application;
// definitie van een klasse ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
// het object van het type TaxAdminData dat de gegevens van de belastingschijven bevat
private $taxAdminData;
// het object van het type [Database] dat de kenmerken van de BD bevat
private $database;
// fabrikant
public function __construct(string $databaseFilename) {
// de configuratie JSON van de database wordt opgeslagen
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// het attribuut wordt voorbereid
$this->taxAdminData = new TaxAdminData();
try {
// de verbinding met de database wordt geopend
$connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// we willen dat bij elke fout van SGBD een uitzondering wordt gegenereerd
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// we starten een transactie
$connexion->beginTransaction();
// de tabel met belastingschijven wordt gevuld
$this->getTranches($connexion);
// de tabel met constanten wordt gevuld
$this->getConstantes($connexion);
// de transactie wordt succesvol afgerond
$connexion->commit();
} catch (\PDOException $ex) {
// is er een transactie bezig?
if (isset($connexion) && $connexion->inTransaction()) {
// de transactie wordt bij een fout beëindigd
$connexion->rollBack();
}
// de uitzondering wordt doorgegeven aan de aanroepende code
throw new ExceptionImpots($ex->getMessage());
} finally {
// de verbinding wordt verbroken
$connexion = NULL;
}
}
// gegevens uit de database lezen
private function getTranches($connexion): void {
…
}
// de constantentabel wordt gelezen
private function getConstantes($connexion): void {
…
}
// de gegevens retourneren waarmee de belasting kan worden berekend
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Deze code is in de paragraaf ‘link’ besproken.
18.2.3. De laag [métier]


De laag [métier] implementeert de volgende interface [InterfaceServerMetier]:
<?php
// naamruimte
namespace Application;
interface InterfaceServerMetier {
// berekening van de belastingen van een belastingplichtige
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
De interface [InterfaceServerMetier] wordt geïmplementeerd door de volgende klasse [ServerMetier]:
<?php
// naamruimte
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// Dao-laag
private $dao;
// gegevens belastingdienst
private $taxAdminData;
//---------------------------------------------
// setter-laag [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $dao) {
// er wordt een referentie opgeslagen op de laag [dao]
$this->dao = $dao;
// de gegevens voor de berekening van de belasting worden opgehaald
// de methode [getTaxAdminData] kan een uitzondering genereren ExceptionImpots
// deze wordt vervolgens doorgegeven naar de aanroepende code
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// belastingberekening
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// resultaat
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 {
…
// resultaat
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=jaarsalaris-aftrek
// de vrijstelling heeft een minimum en een maximum
private function getRevenuImposable(float $salaire): float {
…
// resultaat
return floor($revenuImposable);
}
// berekent een eventuele korting
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// resultaat
return ceil($décôte);
}
// berekent een eventuele korting
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
..
// resultaat
return ceil($réduction);
}
}
Deze code is al besproken in versie 1 in de paragraaf ‘link’. De objectversie met een database is gepresenteerd in de paragraaf ‘link’.
18.2.4. Het serverscript


Het serverscript implementeert de laag [web] [4]. Het script [impots-server] wordt geconfigureerd door het volgende bestand jSON [config-server.json]:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
"databaseFilename": "Data/database.json",
"taxAdminDataFileName": "Data/taxadmindata.json",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/ExceptionImpots.php",
"/Entities/TaxAdminData.php",
"/Entities/Database.php",
"/Dao/InterfaceServerDao.php",
"/Dao/ServerDao.php",
"/Métier/InterfaceServerMetier.php",
"/Métier/ServerMetier.php"
],
"absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
"users": [
{
"login": "admin",
"passwd": "admin"
}
]
}
- regel 1: de hoofdmap van waaruit de bestandspaden worden gemeten;
- regel 2: het configuratiebestand jSON voor de database MySQL;
- regel 3: het bestand jSON met de gegevens van de belastingdienst;
- regels 5-14: de applicatiebestanden;
- regel 15: de vereiste afhankelijkheid van bibliotheken van derden, in dit geval Symfony;
- regels 16-20: de lijst met gebruikers die bevoegd zijn om de applicatie te gebruiken;
De bestanden jSON en [database.json, taxadmindata.json] zijn die van versie 5, zoals beschreven in de paragraaf ‘link’.
Het script [impots-server] implementeert de laag [web] als volgt:
<?php
// strikte naleving van de gedeclareerde typen van de functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
// foutbeheer door PHP
//ini_set("display_errors", "0");
//
// pad naar het configuratiebestand
define("CONFIG_FILENAME", "Data/config-server.json");
// de configuratie wordt opgehaald
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// de benodigde afhankelijkheden worden in het script opgenomen
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute afhankelijkheden (bibliotheken van derden)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definitie van constanten
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// Symfony-afhankelijkheden
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// voorbereiding van het antwoord JSON van de server
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// het ophalen van de huidige verzoek
$request = Request::createFromGlobals();
// authenticatie
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// Bestaat de gebruiker?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// de statuscode van het antwoord wordt ingesteld
if (!$trouvé) {
// niet gevonden - code 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// foutmelding
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// einde
exit;
}
// er is een geldige gebruiker - de ontvangen parameters worden gecontroleerd
$erreurs = [];
// er moeten drie parameters zijn GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// fout?
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// de burgerlijke staat wordt opgehaald
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// fout?
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// het aantal kinderen wordt opgehaald
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// het aantal kinderen moet een geheel getal zijn >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// fout?
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// het jaarsalaris wordt opgehaald
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// het salaris moet een geheel getal zijn >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// fout?
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// andere parameters in de aanvraag?
foreach (\array_keys($request->query->all()) as $key) {
// geldige parameter?
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// fouten?
if ($erreurs) {
// er wordt een foutcode 400 naar de klant verzonden
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// we hebben alles wat we nodig hebben om te werken
// de serverarchitectuur wordt opgezet
$msgErreur = "";
try {
// opzetten van de laag [dao]
$dao = new ServerDao($config["databaseFilename"]);
// aanmaken van de laag [métier]
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// er is een fout opgemerkt
$msgErreur = utf8_encode($ex->getMessage());
}
// fout?
if ($msgErreur) {
// er wordt een foutcode 500 naar de client verzonden
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// de belasting wordt berekend
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// het antwoord wordt teruggestuurd
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
Opmerkingen
- regel 16: het configuratiebestand wordt verwerkt;
- regels 18-26: alle afhankelijkheden worden geladen;
- regel 29: de naam van het bestand [database.json];
- regels 32-33: de klassen van de bibliotheken van derden die we gaan gebruiken worden gedeclareerd;
- regels 36-38: er wordt een antwoord jSON voorbereid;
- regels 40-52: er wordt gecontroleerd of de gebruiker die het verzoek indient, inderdaad tot de geautoriseerde gebruikers behoort;
- regels 54-63: als dat niet het geval is, wordt de code HTTP 401 verzonden, wat aangeeft dat de toegang wordt geweigerd. Bij ontvangst van deze code en de header HTTP [WWW-Authenticate => Basic realm=] geven de meeste browsers een authenticatievenster weer waarin de gebruiker wordt gevraagd zich te authenticeren;
- regel 59: het antwoord jSON van de server legt de oorzaak van de fout uit. Alle antwoorden van de server zullen de tekenreeks jSON uit een tabel [‘réponse’=>’qq chose’] zijn;
- regels 64-117: de geldigheid van het verzoek wordt gecontroleerd:
- een verzoek GET met precies drie parameters;
- een parameter [marié] waarvan de waarde ‘ja’ of ‘nee’ moet zijn;
- een parameter [enfants] waarvan de waarde een geheel getal >=0 moet zijn;
- een parameter [salaire] waarvan de waarde een geheel getal >=0 moet zijn;
- regel 65: telkens wanneer er een fout wordt gedetecteerd, wordt er een foutmelding toegevoegd aan de tabel [$erreurs];
- regels 120-126: als er een fout is, wordt de code HTTP [400 Bad Request] naar de klant verzonden (regel 122);
- regel 123: het antwoord jSON van de server geeft de oorzaak van de fout weer;
- vanaf regel 132 is alles gecontroleerd. We kunnen de lagen [dao, métier] instantiëren. Deze instantiëring brengt kosten met zich mee en mag alleen worden uitgevoerd als we zeker weten dat het een geldig verzoek betreft;
- regels 130-138: de serverarchitectuur wordt opgezet. Bij het opbouwen van de laag [dao] kan een uitzondering van het type [ExceptionImpots] worden gegenereerd. Als deze uitzondering optreedt, wordt de fout genoteerd;
- regels 135-138: als er een uitzondering is opgetreden, wordt de code HTTP 500 naar de client verzonden. Deze code betekent dat de server een fout heeft gemaakt;
- regel 143: het antwoord geeft de oorzaak van de fout weer;
- regel 148 : de berekening van de belasting wordt gedelegeerd aan de laag [métier];
- regels 150-151: verzending van het antwoord;
Laten we dit script testen met een browser. Laten we de beveiligde URL opvragen via [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

- in [1], de gevraagde beveiligde URL;
- in [2], de drie parameters [marié, enfants, salaire];
- in [3] heeft de Apache-server van Laragon een zelfondertekend certificaat SSL verzonden. De browser heeft dit opgemerkt en geeft een veiligheidswaarschuwing weer: hij beschouwt de website van de server als onbetrouwbaar;
- in [4] gaan we verder;

- in [6] gaan we verder;

- in [7] geeft de browser een venster weer waarin de gebruiker zich kan authenticeren;
- in [9,10], typ je [admin] en [admin];

- in [13], het antwoord jSON van de server;
Laten we een paar fouttests uitvoeren:
We vragen om URL en [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]
We krijgen het volgende resultaat:

We verwijderen de SGBD MySQL en vragen de URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000] op:

18.2.5. Tests [Codeception]
Telkens wanneer we een nieuwe versie van de server bouwen, zullen we de lagen [métier] en [dao] testen, zoals dat al sinds versie 04 gebeurt (zie de paragrafen link en link).
Allereerst koppelen we het project [scripts-web] aan de tests [Codeception]. Volg hiervoor dezelfde procedure als voor het project [scripts-console] in de paragraaf link. We krijgen een project [scripts-web] met een map [Test Files]:

We gaan een test aanmaken voor de laag [dao] en een voor de laag [métier].
18.2.5.1. Tests van de laag [dao]

De test [ServerDaoTest] ziet er als volgt uit:
<?php
// strikte naleving van de gedeclareerde typen van de functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
// definitie van constanten
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// pad naar het configuratiebestand
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// de configuratie wordt opgehaald
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// de benodigde afhankelijkheden worden in het script opgenomen
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute afhankelijkheden (bibliotheken van derden)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// bovenliggend
parent::__construct();
// de configuratie wordt opgehaald
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// aanmaken van de laag [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
Opmerkingen
- regels 9-24: we bouwen dezelfde werkomgeving op als die van de server [impots-server.php]. Dit gebeurt in de regels 9-12 met de definitie van de twee constanten waarvan de omgeving afhankelijk is;
- regels 32-40: er wordt een instantie van de te testen laag [dao] aangemaakt, net zoals in het serverscript [impots-server.php];
- vanaf nu gelden dezelfde omstandigheden als in het serverscript [impots-server.php]: we kunnen de tests starten;
- regels 43-45: de methode [testTaxAdminData] is dezelfde als beschreven in de paragraaf ‘link’;
De testresultaten zijn als volgt:

18.2.5.2. Tests van de laag [métier]

De test [ServerMetierTest] zal als volgt zijn:
<?php
// strikte naleving van de gedeclareerde typen van de functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
// definitie van constanten
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// pad naar het configuratiebestand
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// de configuratie wordt opgehaald
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// de benodigde afhankelijkheden worden in het script opgenomen
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute afhankelijkheden (bibliotheken van derden)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// testklasse
class ServerMetierTest extends \Codeception\Test\Unit {
// bedrijfslaag
private $métier;
public function __construct() {
parent::__construct();
// de configuratie wordt opgehaald
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// aanmaken van de laag [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// aanmaken van de laag [métier]
$this->métier = new ServerMetier($dao);
}
// tests
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
Opmerkingen
- regels 9-24: we bouwen dezelfde werkomgeving op als die van de server [impots-server.php]. Dit gebeurt in de regels 9-12 met de definitie van de twee constanten waarvan de omgeving afhankelijk is;
- regels 30-38: er wordt een instantie van de te testen laag [métier] aangemaakt, net zoals in het serverscript [impots-server.php];
- vanaf nu gelden dezelfde omstandigheden als in het serverscript [impots-server.php]: we kunnen de tests starten;
- regels 40-53: de methoden van [test1, test2…, test11] zijn dezelfde als die beschreven in de paragraaf ‘link’;
De testresultaten zijn als volgt:

18.3. De client
We richten ons op het clientgedeelte van de applicatie.

Deze architectuur wordt geïmplementeerd door de volgende scripts:

18.3.1. De entiteiten die tussen de lagen worden uitgewisseld

De bovenstaande entiteiten zijn allemaal beschreven en al gebruikt:
- [BaseEntity] in de paragraaf 'link';
- [ExceptionImpots] in de paragraaf 'link';
- [TaxPayerData] in de paragraaf 'link';
18.3.2. De laag [dao]

De laag [dao] implementeert de volgende interface [InterfaceClientDao]:
<?php
// naamruimte
namespace Application;
interface InterfaceClientDao {
// het inlezen van belastinggegevens
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// berekening van de belastingen van een belastingplichtige
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// resultaten opslaan
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- regel 9: de functie [getTaxPayersData] laadt de gegevens van de belastingplichtigen uit het bestand [$taxPayersFilename] in het geheugen. Als er fouten zijn, worden deze vastgelegd in het bestand [$errorsFilename];
- regel 12: de functie [calculerImpots] berekent de belasting van een belastingplichtige;
- regel 15: de functie [saveResults] slaat in het bestand [$resultsFilename] de gegevens op uit de tabel [$taxPayersData], die de resultaten van verschillende belastingberekeningen weergeven;
De interface [InterfaceClientDao] wordt geïmplementeerd door de volgende klasse [ClientDao]:
<?php
namespace Application;
// afhankelijkheden
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// gebruik van een Trait
use TraitDao;
// attributen
private $urlServer;
private $user;
// constructor
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// belastingberekening
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// een klant aanmaken HTTP
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// er wordt een verzoek naar de server gestuurd
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// het antwoord wordt opgehaald
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logbestanden
// print "$json=json\n";
// de status van het antwoord wordt opgehaald
$statusCode = $response->getStatusCode();
// fout?
if ($statusCode !== 200) {
// er is een fout opgetreden – er wordt een uitzondering gegenereerd
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// we geven het antwoord terug
return $réponse;
}
}
Opmerkingen
- regel 10: we voegen [TraitDao] in (zie paragraaf ‘link’), die de methoden [getTaxPayersData] en [saveResults] implementeert. Er hoeft dus alleen nog de methode [calculerImpots] te worden geïmplementeerd. Deze wordt geïmplementeerd op de regels 22-49;
- regels 16-19: de constructor van de klasse [ClientDao] ontvangt twee parameters:
- de URL [$urlServer] van de belastingberekeningsserver;
- de array [$user] met de sleutels ‘login’ en ‘passwd’, die de gebruiker definieert die de aanvraag indient;
- regel 22: de methode [calculerImpots] ontvangt de drie parameters die naar de belastingberekeningsserver moeten worden verzonden;
- regels 24-27: er wordt een client HTTP aangemaakt met:
- regel 25: de inloggegevens van de gebruiker die het verzoek indient;
- regel 26: de optie die ervoor zorgt dat de client HTTP de geldigheid van het door de server verzonden certificaat SSL niet controleert;
- regels 29-34: de server wordt benaderd met de drie parameters die hij verwacht;
- regel 36: het antwoord jSON wordt van de server opgehaald. Als de parameter [false] niet wordt doorgegeven aan de methode [Response::getContent], en de status van het antwoord van de server valt binnen het bereik [3xx-5xx] (foutgeval), genereert het object [Response] een uitzondering zodra men de inhoud van het antwoord [Response::getContent] of de headers HTTP en [Response::getHeaders] probeert op te halen. Hier willen we, ongeacht de status HTTP van het antwoord, toegang hebben tot de inhoud ervan, al was het maar om deze te loggen (regel 40);
- regels 37-38: het antwoord van de server is de tekenreeks jSON uit een array [‘réponse’=>qqChose]. We halen de waarde [qqChose] eruit;
- regel 40: we loggen het antwoord jSON in de ontwikkelingsmodus;
- regel 42: we halen de statuscode van het antwoord op;
- regels 44-49: als de statuscode HTTP niet 200 is, dan heeft onze server een probleem ondervonden. Vervolgens wordt een uitzondering van het type [ExceptionImpots] gegenereerd met als bericht het antwoord jSON van de server, aangevuld met de statuscode HTTP van het antwoord;
- regel 51: we geven het resultaat terug, dat een associatieve array is met de sleutels [impôt, surcôte, décôte, réduction, taux];
18.3.3. De laag [métier]


De laag [métier] [8] implementeert de volgende interface [InterfaceClientMetier]:
<?php
// naamruimte
namespace Application;
interface InterfaceClientMetier {
// berekening van de belastingen van een belastingplichtige
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// berekening van belastingen in batchmodus
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- regel 9: de functie [calculerImpots] berekent de belasting;
- regel 12: de functie [executeBatchImpots] berekent de belasting voor belastingplichtigen waarvan de gegevens in het bestand [$taxPayersFileName] staan, slaat de verkregen resultaten op in het bestand [$resultsFileName] en de opgetreden fouten in het bestand [$errorsFileName];
De interface [InterfaceClientMetier] wordt geïmplementeerd door de volgende klasse [ClientMetier]:
<?php
// naamruimte
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// attribuut
private $clientDao;
// constructor
public function __construct(InterfaceClientDao $clientDao) {
// de referentie wordt opgeslagen in de laag [dao]
$this->clientDao = $clientDao;
}
// belastingberekening
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// berekening van de belastingen in batchmodus
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// uitzonderingen die afkomstig zijn van de laag [dao] worden doorgegeven
// de belastingplichtigengegevens worden opgehaald
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// resultatenoverzicht
$results = [];
// de gegevens worden verwerkt
foreach ($taxPayersData as $taxPayerData) {
// de belasting wordt berekend
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// we vullen het aan [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// het resultaat wordt in de resultatenlijst geplaatst
$results [] = $taxPayerData;
}
// de resultaten worden opgeslagen
$this->clientDao->saveResults($resultsFileName, $results);
}
}
Opmerkingen
- regels 11-14: de constructor van de klasse [ClientMetier] ontvangt als parameter een verwijzing naar de laag [dao];
- regels 17-19: de berekening van de belasting wordt gedelegeerd aan de laag [dao];
- regels 20-38: de functie [executeBatchImpots] is beschreven in de paragraaf ‘link’;
18.3.4. Het hoofdscript


Het clientscript [MainImpotsClient.php] implementeert de lagen [console] en [9]. Het wordt geconfigureerd door het volgende bestand jSON en [conf-client.json]:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": [
"Entities/BaseEntity.php",
"Entities/TaxPayerData.php",
"Entities/ExceptionImpots.php",
"Utilities/Utilitaires.php",
"Dao/InterfaceClientDao.php",
"Dao/TraitDao.php",
"Dao/ClientDao.php",
"Métier/InterfaceClientMetier.php",
"Métier/ClientMetier.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php"
],
"user": {
"login": "admin",
"passwd": "admin"
},
"urlServer": "https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php"
}
- regel 1: de hoofdmap van de klant;
- regel 2: het bestand jSON met de gegevens van de belastingplichtigen;
- regel 3: het bestand jSON met de resultaten;
- regel 4: het bestand jSON met de fouten;
- regels 6-19: de verschillende afhankelijkheden van het klantproject;
- regels 20-23: de gebruiker die verzoeken indient bij de belastingberekeningsserver;
- regel 24: het beveiligde URL-bestand van de belastingberekeningsserver;
De code van het script [MainImpotsClient.php] is als volgt:
<?php
// strikte naleving van de gedeclareerde typen van de functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
// foutbeheer door PHP
//ini_set("display_errors", "0");
//
// pad naar het configuratiebestand
define("CONFIG_FILENAME", "../Data/config-client.json");
// de configuratie wordt opgehaald
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// de benodigde afhankelijkheden worden in het script opgenomen
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute afhankelijkheden (bibliotheken van derden)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definitie van constanten
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// Symfony-afhankelijkheden
use Symfony\Component\HttpClient\HttpClient;
// aanmaken van de laag [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// aanmaken van de laag [métier]
$clientMetier = new ClientMetier($clientDao);
// berekening van de belastingen in batchmodus
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// de fout wordt weergegeven
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// einde
print "Terminé\n";
exit;
Opmerkingen
- regel 13: pad naar het configuratiebestand;
- regel 16: verwerking van het configuratiebestand;
- regels 18-26: laden van de afhankelijkheden;
- regel 37: aanmaken van de laag [dao]. We geven de twee gegevens die de laagconstructor verwacht door:
- de URL van de belastingberekeningsserver;
- de inloggegevens van de gebruiker die de verzoeken gaat uitvoeren;
- regel 39: aanmaken van de laag [métier]. We geven aan de constructor van de laag een verwijzing naar de zojuist aangemaakte laag [dao];
- regel 43: de laag [métier] wordt gevraagd om:
- de belastingen te berekenen voor alle belastingplichtigen in het bestand $config["taxPayerDataFileName"];
- de resultaten op te slaan in het bestand $config["resultsFileName"];
- de fouten opslaan in het bestand $config["errorsFileName"];
- regel 43 kan uitzonderingen genereren;
- regel 46: weergave van het foutbericht van de uitzondering;
De uitvoering van de client levert dezelfde resultaten op als de vorige versies. Controleer de volgende bestanden:
- [Data/taxpayersdata.json]: gegevens van de belastingplichtigen voor wie het belastingbedrag wordt berekend;
- [Data/results.json]: resultaten voor de verschillende belastingplichtigen uit het bestand [Data/taxpayersdata.json];
- [Data/errors.json]: de fouten die mogelijk zijn opgetreden bij de verwerking van het bestand [Data/taxpayersdata.json];
Laten we eens kijken naar de mogelijke foutgevallen. Laten we eerst de Laragon-server stoppen. De resultaten in de clientconsole zijn dan als volgt:
Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé
Laten we nu alleen de Apache-server starten en niet de SGBD MySQL:

De resultaten in de clientconsole zijn dan als volgt:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}
Terminé
Laten we nu MySQL starten en vervolgens in [config-client] de gebruiker wijzigen die inlogt:
De resultaten in de clientconsole zijn dan als volgt:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
18.3.5. Tests [Codeception]
Net zoals bij de vorige versies gaan we [Codeception]-tests schrijven voor versie 08.

18.3.5.1. Test van de laag [métier]
De test [ClientMetierTest.php] is als volgt:
<?php
// strikte naleving van de gedeclareerde typen van de functieparameters
declare (strict_types=1);
// naamruimte
namespace Application;
// definitie van constanten
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
// pad naar het configuratiebestand
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// de configuratie wordt opgehaald
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// de benodigde afhankelijkheden worden in het script opgenomen
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute afhankelijkheden (bibliotheken van derden)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// testklasse
class ClientMetierTest extends \Codeception\Test\Unit {
// bedrijfslaag
private $métier;
public function __construct() {
parent::__construct();
// de configuratie wordt opgehaald
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// aanmaken van de laag [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// aanmaken van de laag [métier]
$this->métier = new ClientMetier($clientDao);
}
// tests
public function test1() {
…
}
-------------
public function test11() {
…
}
}
Opmerkingen
- regels 10-26: definitie van de testomgeving. We gebruiken dezelfde omgeving als die van het hoofdscript [MainImpotsClient], beschreven in de paragraaf 'link';
- regels 33-41: opbouw van de lagen [dao] en [métier];
- regel 40: het attribuut [$this→métier] verwijst naar de laag [métier];
- regels 44-51: de methoden [test1, test2…, test11] zijn de methoden die worden beschreven in de paragraaf 'link';
De testresultaten zijn als volgt:
