Skip to content

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:

Image

  • 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:

Image

  • 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.

Image

Deze architectuur wordt geïmplementeerd door de volgende scripts:

Image

18.2.1. De entiteiten die tussen de lagen worden uitgewisseld

Image

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]

Image

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]

Image

Image

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

Image

Image

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]:

Image

  • 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;

Image

  • in [6] gaan we verder;

Image

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

Image

  • 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:

Image

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:

Image

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]:

Image

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]

Image

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:

Image

18.2.5.2. Tests van de laag [métier]

Image

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:

Image

18.3. De client

We richten ons op het clientgedeelte van de applicatie.

Image

Deze architectuur wordt geïmplementeerd door de volgende scripts:

Image

18.3.1. De entiteiten die tussen de lagen worden uitgewisseld

Image

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]

Image

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]

Image

Image

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

Image

Image

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:

Image

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:

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

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.

Image

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:

Image