Skip to content

19. Toepassingsopdracht – versie 9

In deze versie gaan we de server als volgt verbeteren:

  • momenteel worden bij elk verzoek de gegevens van de belastingdienst in de database opgezocht. We gaan een sessie gebruiken:
    • bij het eerste verzoek van een gebruiker worden de gegevens van de belastingdienst opgezocht in de database en in de sessie opgeslagen;
    • bij volgende verzoeken van dezelfde gebruiker worden de gegevens van de belastingdienst uit de sessie opgehaald. We kunnen een lichte verbetering in de uitvoeringstijd verwachten, aangezien databaseverzoeken veel rekenkracht vergen;
  • de server registreert de belangrijke momenten in een tekstbestand:
    • de geslaagde of mislukte authenticatie;
    • of de door de klant verzonden parameters geldig zijn of niet;
    • het resultaat van de belastingberekening;
    • de verschillende foutgevallen;
  • in geval van een fatale fout wordt er een e-mail verzonden naar de beheerder van de applicatie;

Ook de client moet worden aangepast om de sessiecookie te beheren die we naar de client zullen sturen.

19.1. De server

We richten ons op het servergedeelte van de applicatie.

Image

Deze architectuur wordt geïmplementeerd door de volgende scripts:

Image

19.1.1. Hulpprogramma's

Image

19.1.1.1. De klasse [Logger]

De klasse [Logger] wordt gebruikt om logbestanden naar een tekstbestand te schrijven:


<?php

namespace Application;

class Logger {
  // attribuut
  private $resource;

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

  // een bericht in de logbestanden schrijven
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }

  // logbestand sluiten
  public function close() {
    fclose($this->resource);
  }

}

Opmerkingen

  • regel 7: de bron van het logbestand;
  • regel 10: de constructor van de klasse ontvangt als parameter de naam van het logbestand;
  • regel 12: het tekstbestand wordt geopend in de toevoegmodus (a+): het bestand wordt geopend en de inhoud blijft behouden. Er wordt achter de huidige inhoud geschreven;
  • regels 13-15: als het openen niet is gelukt, wordt er een uitzondering gegenereerd;
  • regels 19-21: met de methode [write] kan het bericht [$message] in het logbestand worden geschreven, voorafgegaan door de datum en de tijd;
  • regels 24-16: met de methode [close] kan het logbestand worden gesloten;

Opmerking: de serverapplicatie kan meerdere clients tegelijkertijd bedienen. Er is echter slechts één logbestand voor alle clients. Er bestaat dus een risico op gelijktijdige toegang om in het bestand te schrijven. De schrijfbewerkingen moeten daarom worden gesynchroniseerd om te voorkomen dat ze door elkaar raken. Hiervoor beschikt PHP over semaforen [https://www.php.net/manual/fr/book.sem.php]. We zullen de synchronisatie van de schrijfbewerkingen hier buiten beschouwing laten, maar men moet zich wel bewust blijven van het probleem.

19.1.1.2. De klasse [SendAdminMail]

Met de klasse [SendAdminMail] kan een e-mail naar de beheerder van de applicatie worden verzonden als deze crasht:


<?php

namespace Application;

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

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

  public function send() {
    // verzendt $this->config['message'] naar de SMTP-server $this->config['smtp-server'] op poort $infos[smt-port]
    // als $this->config['tls'] waar is, wordt de ondersteuning TLS gebruikt
    // de e-mail wordt verzonden namens $this->config['from']
    // voor de ontvanger $this->config['to']
    // het bericht heeft als onderwerp $this->config['subject']
    // aan de e-mail worden de bijlagen van $this->config['attachments'] toegevoegd
    // het resultaat van de methode
    try {
      // het bericht wordt aangemaakt
      $message = (new \Swift_Message())
        // onderwerp van het bericht
        ->setSubject($this->config["subject"])
        // afzender
        ->setFrom($this->config["from"])
        // ontvangers met een woordenboek (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // berichttekst
        ->setBody($this->config["message"])
      ;
      // bijlagen
      foreach ($this->config["attachments"] as $attachment) {
        // pad naar de bijlage
        $fileName = __DIR__ . $attachment;
        // er wordt gecontroleerd of het bestand bestaat
        if (file_exists($fileName)) {
          // het document wordt aan het bericht toegevoegd
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // fout
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // protocol TLS ?
      if ($this->config["tls"] === "TRUE") {
        // TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
          ->setUsername($this->config["user"])
          ->setPassword($this->config["password"]);
      } else {
        // geen TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // de verzendmanager
      $mailer = new \Swift_Mailer($transport);
      // verzending van het bericht
      $mailer->send($message);
      // einde
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // fout
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }

}

Opmerkingen

  • regel 11: de constructor ontvangt twee parameters:
    • [$config]: een associatieve array met alle informatie die nodig is om de e-mail te versturen;
    • [$logger]: een logger waarmee belangrijke momenten tijdens het verzenden van de e-mail kunnen worden gelogd;

Het associatieve array heeft de volgende vorm:

1
2
3
4
5
6
7
8
9
{
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
}
  • regels 16-76: met de methode [send] kan de e-mail worden verzonden. Deze code is gepresenteerd en beschreven in de paragraaf ‘link’;

19.1.2. De laag [dao]

Image

Het script [ServeurDaoWithSession.php] is als volgt:


<?php

// naamruimte
namespace Application;

// definitie van een klasse ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {

  // constructor
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // het eenvoudigste geval
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // het wordt doorgegeven aan de bovenliggende klasse
      parent::__construct($databaseFilename);
    }
  }

}

Opmerkingen

  • regel 7: de klasse [ServerDaoWithSession] van versie 09 breidt de klasse [ServerDao] van versie 08 uit. De klasse [ServerDao] kan namelijk de database gebruiken. Nu hoeven we alleen nog maar rekening te houden met het geval waarin de gegevens van de belastingdienst al zijn verkregen:
  • regel 10: de constructor ontvangt nu twee parameters:
    • [string $databaseFilename]: naam van het bestand met de gegevens om verbinding te maken met de database als de gegevens van de belastingdienst nog niet zijn opgehaald, NULL anders;
    • [TaxAdminData $taxAdminData]: de gegevens van de belastingdienst indien deze al zijn opgehaald, NULL anders;

Bij het starten van een websessie wordt de laag [dao] opgebouwd met een object [$databaseFilename] dat niet NULL is en een object [taxAdminData] NULL. De gegevens van de belastingdienst worden vervolgens uit de database opgehaald en in de sessie opgeslagen. Bij latere verzoeken binnen dezelfde sessie wordt de laag [dao] opgebouwd met een object [databaseFilename], NULL en een object [taxAdminData] uit de sessie, en niet met NULL. Er vindt dus geen zoekopdracht in de database plaats.

19.1.3. Het serverscript

Het serverscript [impots-server.php] wordt geconfigureerd door het volgende bestand jSON [config-server.json]:


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

Het serverscript [impots-server.php] ontwikkelt zich als volgt:


<?php

// strikte naleving van de gedeclareerde typen van de functieparameters
declare (strict_types=1);

// naamruimte
namespace Application;

// foutafhandeling 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";
}
//
// Symfony-afhankelijkheden
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

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

// voorbereiding van het antwoord JSON van de server
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// logbestanden aanmaken
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // interne serverfout
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // voltooid
  exit;
}

// eerste log
$logger->write("\n---nouvelle requête\n");

// het huidige verzoek wordt opgehaald
$request = Request::createFromGlobals();
// authenticatie alleen de eerste keer
if (!$session->has("user")) {
  // log
  $logger->write("Autentification en cours…\n");
  // authenticatie

  }
  // is de gebruiker gevonden?
  if (!$trouvé) {
    // niet gevonden - code 401 HTTP_UNAUTHORIZED
    sendResponse(
      $response,
      ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
      Response::HTTP_UNAUTHORIZED,
      ["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
      $logger
    );
    // voltooid
    exit;
  } else {
    // in de sessie wordt genoteerd dat de gebruiker is geauthenticeerd
    $session->set("user", TRUE);
    // log
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // log
  $logger->write("Authentification prise en session…\n");
}
// er is een geldige gebruiker - de ontvangen parameters worden gecontroleerd
$erreurs = [];
// er moeten drie parameters zijn: GET


// fouten?
if ($erreurs) {
// er wordt een foutcode 400 HTTP_BAD_REQUEST naar de klant verzonden
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // voltooid
  exit;
} else {
  // logbestanden
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// we hebben alles wat we nodig hebben om te werken
// aanmaken van de laag [dao]
if (!$session->has("taxAdminData")) {
  // de gegevens worden uit de database gehaald
  $logger->write("données fiscales prises en base de données\n");
  try {
    // opbouw van de laag [dao]
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    // de gegevens worden in de sessie geplaatst
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // de fout wordt genoteerd
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // voltooid
    exit;
  }
} else {
  // de gegevens worden in de sessie opgenomen
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // logbestanden
  $logger->write("données fiscales prises en session\n");
}
// aanmaken van de laag [métier]
$métier = new ServerMetier($dao);
// berekening van de belasting
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// het antwoord wordt weergegeven
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// einde
exit;

function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // er wordt een e-mail naar de beheerder verzonden
  // SendAdminMail vangt alle uitzonderingen op en logt deze zelf
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // er wordt een 500-foutcode naar de klant gestuurd
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}

// functie voor het verzenden van het antwoord HTTP naar de klant
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $response: antwoord HTTP
  // $result: resultatenoverzicht
  // $statusCode: status HTTP van het antwoord
  // $headers: HTTP-headers die in het antwoord moeten worden opgenomen
  // $logger: de logger van de applicatie
  //
  // status HTTTP
  $response->setStatusCode($statusCode);
  // hoofdtekst
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // headers
  $response->headers->add($headers);
  // verzending
  $response->send();
  // log
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
}

Opmerkingen

  • regels 34-35: er wordt een sessie gestart;
  • regels 38-40: er wordt een antwoord jSON voorbereid;
  • regels 42-50: er wordt geprobeerd het logbestand aan te maken. Als er een uitzondering optreedt, wordt de methode [doInternalServer] (regels 132-140) aangeroepen;
  • regel 132: de methode [doInternalServer] accepteert vier parameters:
    • [$message]: het bericht dat moet worden gelogd. Moet worden gecodeerd in UTF-8;
    • [$response]: het object [Response] dat het antwoord van de server aan zijn client omvat;
    • [$logger]: het object [Logger] waarmee logbestanden kunnen worden aangemaakt;
    • [$infos]: de gegevens waarmee een e-mail naar de beheerder van de applicatie kan worden verzonden;
  • regels 135-137: er wordt een e-mail verzonden naar de beheerder van de applicatie;
  • regel 139: het antwoord wordt naar de klant verzonden:
    • $response: antwoord HTTP;
    • $result: de server verstuurt de tekenreeks jSON uit de array [‘réponse’=>["erreur" => $message]];
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], code 500;
    • $headers: [], geen HTTP-headers om aan het antwoord toe te voegen;
    • $logger: de logger van de applicatie;
  • regel 58: dankzij de ingestelde sessie hoeft de klant slechts één keer te worden geauthenticeerd:
    • zodra de klant is geauthenticeerd, wordt er een sleutel [user] in de sessie geplaatst (regel 78);
    • bij het volgende verzoek van dezelfde klant voorkomt regel 58 een authenticatie die overbodig is geworden;
  • regel 103: dankzij de ingestelde sessie wordt er slechts één keer in de database naar gegevens gezocht:
    • bij het eerste verzoek vindt de zoekopdracht in de database plaats (regel 108). De opgehaalde gegevens worden vervolgens in de sessie opgeslagen (regel 110) en gekoppeld aan de sleutel [taxAdminData];
    • bij de volgende verzoeken wordt de sleutel [taxAdminData] in de sessie gevonden (regel 103) en worden de gegevens op de schijf vervolgens rechtstreeks doorgegeven aan de laag [dao] (regel 119);
  • regels 111-116: het opzoeken van fiscale gegevens in de database kan mislukken. In dat geval wordt een code [500 Internal Server Error] naar de klant verzonden;
  • regel 113: de foutmelding van de uitzondering van de driver MySQL is gecodeerd als ISO 8859-1. Deze wordt omgezet naar UTF-8 om correct te worden gelogd;
  • de rest van de code is vrijwel identiek aan die van de vorige versie;
  • regels 143-164: de functie [sendResponse] stuurt alle antwoorden naar de client;
  • regels 144-148: betekenis van de parameters;
  • regel 153: het antwoord is altijd de tekenreeks jSON uit een array [‘résultat’=>qqChose];
  • regel 156: soms moeten er HTTP-headers aan het antwoord worden toegevoegd. Dit is het geval in regel 71;
  • regel 158: het antwoord wordt verzonden;
  • regels 160-163: het antwoord wordt gelogd en de logger wordt gesloten;

19.1.4. Tests [Codeception]

Image

We gaan alleen de laag [dao] testen, aangezien dit de enige is die is gewijzigd.

De testcode [ServerDaoTest] is als volgt:


<?php

// strikte naleving van de gedeclareerde typen van functieparameters
declare (strict_types=1);

// naamruimte
namespace Application;

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

  // tests
  public function testTaxAdminData() {

  }

}
  • regels 9-24: we creëren een uitvoeringsomgeving die identiek is aan die van het serverscript [impots-server];
  • regel 38: om de laag [dao] op te bouwen, wordt de klasse [ServerDaoWithSession] geïnstantieerd;

Het testresultaat is als volgt:

Image

19.2. De client

We richten ons op het clientgedeelte van de applicatie.

Image

Deze architectuur wordt geïmplementeerd door de volgende scripts:

Image

In de nieuwe versie veranderen alleen:

  • het configuratiebestand [config-client.json];
  • de clientlaag [dao];

19.2.1. de laag [dao]

De laag [Dao] verandert als volgt:


<?php

namespace Application;

// afhankelijkheden
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // gebruik van een Trait
  use TraitDao;
  // attributen
  private $urlServer;
  private $user;
  private $sessionCookie;

  // 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 {
    // sessiecookie?
    if (!$this->sessionCookie) {
      // 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 zonder sessiecookie
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // er wordt een verzoek naar de server gestuurd met de sessiecookie
      // we maken een client aan: HTTP
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // 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);
    }
    if (!$this->sessionCookie) {
      // de sessiecookie wordt opgehaald
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // sessiecookie?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // we sturen het antwoord terug
    return $réponse;
  }

}

Opmerkingen

De wijziging in de laag [dao] houdt in dat er nu een sessie wordt beheerd:

  • regel 14: de sessiecookie;
  • regels 25-39: bij het eerste verzoek bestaat dit cookie nog niet; het verzoek wordt dan naar de server gestuurd met de authenticatiegegevens (regel 28);
  • regels 40-53: bij volgende verzoeken beschikken we normaal gesproken over de sessiecookie. We sturen dan geen authenticatiegegevens mee (regels 42-44);
  • regels 69-82: het antwoord van de server op het eerste verzoek bevat een sessiecookie. Deze wordt opgehaald. Deze code is al gebruikt en uitgelegd in de paragraaf ‘link’;
  • regel 78: het opgehaalde sessiecookie wordt opgeslagen in het klasse-attribuut [$sessionCookie];

Opmerking: we hadden de oude versie van de laag [dao] kunnen behouden en bij elk verzoek authenticatie kunnen uitvoeren, aangezien de kosten hiervan verwaarloosbaar zijn. Om didactische redenen wilden we echter nogmaals laten zien hoe een client HTTP een sessie kan beheren.

19.2.2. Het configuratiebestand

Het configuratiebestand jSON verandert als volgt:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/TaxPayerData.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Utilities/Utilitaires.php",
        "/../version-08/Dao/InterfaceClientDao.php",
        "/../version-08/Dao/TraitDao.php",
        "/Dao/ClientDao.php",
        "/../version-08/Métier/InterfaceClientMetier.php",
        "/../version-08/Métier/ClientMetier.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php"
    ],
    "user": {
        "login": "admin",
        "passwd": "admin"
    },
    "urlServer": "https://localhost:443/php7/scripts-web/impots/version-09/impots-server.php"
}

Alleen de URL op regel 24 verandert.

19.3. Enkele tests

19.3.1. Test 1

Allereerst voeren we de client uit in een foutloze omgeving. De resultaten zijn nog steeds dezelfde als bij de vorige versies. Maar nu hebben we aan de serverzijde een logbestand met de naam [logs.txt]:


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

  • regels 3-7: bij het eerste verzoek vindt authenticatie plaats en worden de gegevens in de database opgezocht;
  • regels 9-14: bij de volgende verzoek vindt er geen authenticatie meer plaats en worden de gegevens uit de sessie opgehaald. Dit herhaalt zich bij de volgende verzoeken (regels 15 en verder);

19.3.2. Test 2

Laten we nu de database MySQL afsluiten. Aan de clientzijde krijgen we de volgende console-uitvoer:


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é

Aan de serverzijde zien we de volgende logbestanden: [logs.txt]:


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

Om de e-mail te bekijken die de beheerder van de applicatie heeft ontvangen, gebruiken we het script [imap-03.php] uit de paragraaf 'Koppeling met het configuratiebestand' met het volgende configuratiebestand:

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

Het resultaat is als volgt:

Image

Het bestand [message_1.txt] bevat de volgende tekst:


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

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

19.3.3. Test 3

Laten we er nu voor zorgen dat het bestand [logs.txt] niet kan worden aangemaakt. Hiervoor volstaat het om een map [logs.txt] aan te maken:

Image

Zodra dit is gebeurd, voeren we de client uit.

Aan de clientzijde krijgen we de volgende console-uitvoer:


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

Aan de serverzijde zijn er geen logbestanden, maar de beheerder ontvangt de volgende e-mail:


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

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

19.3.4. Test 4

Laten we deze keer in het configuratiebestand van de client onjuiste inloggegevens opgeven voor de client die verbinding maakt.

De client geeft de volgende console-uitvoer weer:


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

Aan de serverzijde verschijnen de volgende logberichten:


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

19.3.5. Test 5

Laten we de juiste gebruiker [admin, admin] weer in het configuratiebestand van de client zetten.

Laten we nu de URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] van de server rechtstreeks in een browser opvragen zonder parameters door te geven:

In het logbestand [logs.txt] van de server staan de volgende regels:


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

19.4. Tests [Codeception]

Net als bij eerdere versies gaan we [Codeception]-tests schrijven voor versie 09.

Image

19.4.0.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-09");

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

// testklasse
class ClientMetierTest extends Unit {
  
}

Opmerkingen

  • ten opzichte van de testklasse van versie 08 verandert alleen regel 10, die de hoofdmap van de te testen client aangeeft;

De testresultaten zijn als volgt:

Image

Het is interessant om de logbestanden van de server [logs.txt] te bekijken:


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

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

We zien dat de gegevens van de belastingdienst altijd uit de database worden gehaald en nooit uit de sessie. Laten we teruggaan naar de code van de uitgevoerde test:


<?php

// strikte naleving van de gedeclareerde typen van functieparameters
declare (strict_types=1);

// naamruimte
namespace Application;



// testklasse
class ClientMetierTest extends Unit {
  // bedrijfslaag
  private $métier;

  public function __construct() {
    parent::__construct();
    // de configuratie wordt opgehaald
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // de laag [dao] aanmaken
    $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 test2() {

  }

  public function test3() {

  }



}

In een testklasse [Codeception] wordt de constructor voor elke test uitgevoerd.

  • regel 21: voor elke test wordt dus een nieuwe [ClientDao] aangemaakt met een sessiecookie NULL. Dit verklaart waarom deze klant geen gebruik maakt van een sessie;

Dit voorbeeld laat zien dat de sessie niet de juiste plek is om gegevens van de belastingdienst op te slaan. Deze gegevens zijn namelijk gemeenschappelijk voor alle gebruikers van de applicatie. Hier worden ze echter in elke sessie van deze gebruikers gedupliceerd.

Bij webprogrammering onderscheiden we drie soorten zichtbaarheid voor gedeelde gegevens:

  • gegevens die door alle gebruikers van de webapplicatie worden gedeeld. Dit zijn over het algemeen alleen-lezen-gegevens. PHP beschikt niet standaard over dit geheugen;
  • gegevens die worden gedeeld door de verzoeken van dezelfde klant. Deze gegevens worden opgeslagen in de sessie. We spreken dan van een klantsessie om het geheugen van de klant aan te duiden. Alle verzoeken van een klant hebben toegang tot deze sessie. Ze kunnen er informatie opslaan en lezen. In de voorgaande scripts wordt deze sessie geïmplementeerd door het Symfony-object [HttpFoundation\Session\Session];
  • het verzoekgeheugen, of de verzoekcontext. Het verzoek van een gebruiker kan door meerdere opeenvolgende acties worden verwerkt. Dankzij de verzoekcontext kan actie 1 informatie doorgeven aan actie 2. In de voorgaande scripts wordt het verzoek geïmplementeerd door het Symfony-object [HttpFoundation\Request] en het geheugen ervan door het attribuut [HttpFoundation\Request::attributes];

Image

Er bestaan bibliotheken van derden om PHP een applicatiegeheugen te geven. De nieuwe versie van de praktijkopdracht laat het gebruik van een van deze bibliotheken zien.