18. Esercizio pratico – Versione 8
Rivedremo l'applicazione di esempio – versione 5 (paragrafo del link) e la trasformeremo in un'applicazione client/server.
18.1. Introduzione
L'architettura della versione 5 era la seguente:

- il livello denominato [dao] (Data Access Objects) gestisce le interazioni con il database MySQL e il file system locale;
- il livello denominato [business] esegue il calcolo delle imposte;
- lo script principale funge da coordinatore: istanzia i livelli [DAO] e [logica di business] e poi comunica con il livello [logica di business] per eseguire le operazioni necessarie;
Migreremo questa architettura alla seguente architettura client/server:

- In [2], riutilizzeremo il livello [DAO] della versione 5, eliminando i metodi per l'accesso al file system locale. Questi metodi saranno trasferiti al livello [DAO] del client [6, 7];
- In [3], il livello [business] rimarrà lo stesso della versione 5, ad eccezione dei metodi [executeBatchImpôts, saveResults], che saranno migrati al livello [DAO] del client [7];
- In [4], è necessario scrivere lo script del server: dovrà:
- creare i livelli [business] e [DAO] [3, 2];
- comunicare con lo script client [5, 7];
- In [7], deve essere scritto il livello [dao] del client:
- sarà un client HTTP dello script del server [4, 5];
- riutilizzerà i metodi per accedere al file system locale dal livello [dao] della versione 5;
- in [8], il livello [business] del client sarà conforme all'interfaccia [BusinessInterface] della versione 5. La sua implementazione sarà, tuttavia, diversa. Nella versione 5, il livello [business] eseguiva il calcolo delle imposte. Qui, è il livello [business] del server che esegue questo calcolo. Il livello [business] richiederà quindi al livello [DAO] [7] di comunicare con il server e di richiedere il calcolo delle imposte;
- in [9], lo script della console dovrà istanziare i livelli [DAO, business] del client e avviarne l'esecuzione;
18.2. Il server
Ci stiamo concentrando sul lato server dell'applicazione.

Questa architettura sarà implementata dai seguenti script:

18.2.1. Entità scambiate tra i livelli

Le entità scambiate tra i livelli sono quelle della versione 5 descritte nella sezione collegata.
18.2.2. Il livello [dao]

Il livello [dao] implementa la seguente interfaccia [InterfaceServerDao]:
<?php
// namespace
namespace Application;
interface InterfaceServerDao {
// reading tax administration data
public function getTaxAdminData(): TaxAdminData;
}
- Riga 9: Il metodo [getTaxAdminData] recupera i dati dell'amministrazione fiscale da un database;
L'interfaccia [InterfaceServerDao] è implementata dalla seguente classe [ServerDao]:
<?php
// namespace
namespace Application;
// definition of a ImpotsWithDataInDatabase class
class ServerDao implements InterfaceServerDao {
// the TaxAdminData object containing tax bracket data
private $taxAdminData;
// the [Database] type object containing the characteristics of the BD
private $database;
// manufacturer
public function __construct(string $databaseFilename) {
// store the JSON configuration of the bd
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// we prepare the attribute
$this->taxAdminData = new TaxAdminData();
try {
// open the database connection
$connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// we want every SGBD error to trigger an exception
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// start a transaction
$connexion->beginTransaction();
// fill in the tax bracket table
$this->getTranches($connexion);
// fill in the constants table
$this->getConstantes($connexion);
// the transaction is completed successfully
$connexion->commit();
} catch (\PDOException $ex) {
// is there a transaction in progress?
if (isset($connexion) && $connexion->inTransaction()) {
// transaction ends in failure
$connexion->rollBack();
}
// trace the exception back to the calling code
throw new ExceptionImpots($ex->getMessage());
} finally {
// close the connection
$connexion = NULL;
}
}
// reading data from the database
private function getTranches($connexion): void {
…
}
// reading the constants table
private function getConstantes($connexion): void {
…
}
// returns data for tax calculation
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Questo codice è stato presentato nella sezione collegata.
18.2.3. Il livello [business]


Il livello [business] implementa la seguente interfaccia [InterfaceServerMetier]:
<?php
// namespace
namespace Application;
interface InterfaceServerMetier {
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
L'interfaccia [InterfaceServerMetier] è implementata dalla seguente classe [ServerMetier]:
<?php
// namespace
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// dao layer
private $dao;
// tax administration data
private $taxAdminData;
//---------------------------------------------
// setter couche [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $dao) {
// a reference is stored on the [dao] layer
$this->dao = $dao;
// recover data for tax calculation
// method [getTaxAdminData] may throw a ExceptionImpots exception
// we then let it go back to the calling code
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// tAX CALCULATION
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// result
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 {
…
// result
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=annualwage-discount
// the allowance has a minimum and a maximum
private function getRevenuImposable(float $salaire): float {
…
// result
return floor($revenuImposable);
}
// calculates any discount
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// result
return ceil($décôte);
}
// calculates any reduction
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
..
// result
return ceil($réduction);
}
}
Questo codice è già stato trattato nella Versione 1 nella sezione collegata. La sua versione orientata agli oggetti con un database è stata presentata nella sezione collegata.
18.2.4. Lo script del server


Lo script del server implementa il livello [web] [4]. Lo script [impots-server] è configurato dal seguente file 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"
}
]
}
- riga 1: la directory principale da cui verranno misurati i percorsi dei file;
- riga 2: il file di configurazione JSON per il database MySQL;
- riga 3: il file JSON contenente i dati dell'amministrazione fiscale;
- righe 5–14: i file dell'applicazione;
- riga 15: la dipendenza da librerie di terze parti, in questo caso Symfony;
- righe 16–20: l'array degli utenti autorizzati a utilizzare l'applicazione;
I file JSON [database.json, taxadmindata.json] sono quelli della versione 5, come descritto nella sezione collegata.
Lo script [impots-server] implementa il livello [web] come segue:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definition of constants
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// retrieve the current query
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// does the user exist?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// set the response status code
if (!$trouvé) {
// not found - code 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// error msg
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// end
exit;
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// mistake?
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// marital status is restored
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// mistake?
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// the number of children
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// number of children must be an integer >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// mistake?
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// we recover the annual salary
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// salary must be an integer >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// mistake?
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// other parameters in the query?
foreach (\array_keys($request->query->all()) as $key) {
// valid parameter?
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// mistakes?
if ($erreurs) {
// an error code 400 is sent to the customer
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// we have everything you need to work
// server architecture creation
$msgErreur = "";
try {
// creation of the [dao] layer
$dao = new ServerDao($config["databaseFilename"]);
// creation of the [business] layer
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// we note the error
$msgErreur = utf8_encode($ex->getMessage());
}
// mistake?
if ($msgErreur) {
// an error code 500 is sent to the customer
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
Commenti
- riga 16: carichiamo il file di configurazione;
- righe 18–26: carichiamo tutte le dipendenze;
- riga 29: il nome del file [database.json];
- righe 32–33: dichiariamo le classi delle librerie di terze parti che verranno utilizzate;
- righe 36–38: prepariamo una risposta JSON;
- righe 40–52: verifichiamo che l'utente che effettua la richiesta sia effettivamente un utente autorizzato;
- righe 54–63: in caso contrario, inviamo un codice HTTP 401 che indica l'accesso negato. Alla ricezione di questo codice e dell'intestazione HTTP [WWW-Authenticate => Basic realm=], la maggior parte dei browser visualizza una finestra di autenticazione che richiede all'utente di effettuare il login;
- riga 59: la risposta JSON del server spiega la causa dell'errore. Tutte le risposte del server saranno una stringa JSON sotto forma di array [‘response’=>’something’];
- Righe 64–117: verifichiamo la validità della richiesta:
- una richiesta GET con esattamente tre parametri;
- un parametro [married] il cui valore deve essere ‘yes’ o ‘no’;
- un parametro [children] il cui valore deve essere un numero intero >= 0;
- un parametro [salary] il cui valore deve essere un numero intero >=0;
- riga 65: ogni volta che viene rilevato un errore, viene aggiunto un messaggio di errore all'array [$errors];
- righe 120–126: se si verifica un errore, al client viene inviato il codice HTTP [400 Bad Request] (riga 122);
- riga 123: la risposta JSON del server spiega la causa dell'errore;
- A partire dalla riga 132, tutto è stato verificato. Possiamo istanziare i livelli [dao, business]. Questa istanziazione ha un costo e dovrebbe essere eseguita solo se siamo sicuri di avere una richiesta valida;
- righe 130–138: viene creata l'architettura del server. La costruzione del livello [DAO] può generare un'eccezione [ExceptionImpots]. Se si verifica questa eccezione, l'errore viene registrato;
- righe 135–138: se si è verificata un'eccezione, inviamo il codice di stato HTTP 500 al client. Questo codice indica che il server si è bloccato;
- riga 143: la risposta spiega la causa dell'errore;
- riga 148 : il calcolo delle imposte viene delegato al livello [business];
- righe 150–151: invio della risposta;
Proviamo questo script con un browser. Richiediamo l'URL sicuro [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

- in [1], l'URL protetto richiesto;
- in [2], i tre parametri [married, children, salary];
- in [3], il server Apache di Laragon ha inviato un certificato SSL autofirmato. Il browser lo ha rilevato e visualizza un avviso di sicurezza: considera il sito del server non attendibile;
- in [4], proseguiamo;

- in [6], continuiamo;

- In [7], il browser visualizza una finestra in cui l'utente può effettuare l'accesso;
- in [9,10], digitare [admin] e [admin];

- in [13], la risposta JSON del server;
Eseguiamo alcuni test di errore:
Richiediamo l'URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]
Otteniamo il seguente risultato:

Arrestiamo il DBMS MySQL e richiediamo l'URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

18.2.5. [Codeception] Test
Ogni volta che creiamo una nuova versione del server, testeremo i livelli [business] e [DAO] come è stato fatto a partire dalla versione 04 (vedi i paragrafi link e link).
Per prima cosa, associamo il progetto [scripts-web] ai test [Codeception]. Per farlo, segui la stessa procedura utilizzata per il progetto [scripts-console] descritta nel paragrafo precedente. Alla fine otterremo un progetto [scripts-web] contenente una cartella [Test Files]:

Creeremo un test per il livello [dao] e uno per il livello [business].
18.2.5.1. Test per il livello [dao]

Il [ServerDaoTest] sarà il seguente:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// parent
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
Commenti
- righe 9–24: Configuriamo lo stesso ambiente di lavoro del server [impots-server.php]. Questo viene fatto nelle righe 9–12 definendo le due costanti da cui dipende l'ambiente;
- righe 32–40: creiamo un'istanza del livello [dao] da testare, come è stato fatto nello script del server [impots-server.php];
- Da questo punto in poi, ci troviamo nelle stesse condizioni dello script del server [impots-server.php]: possiamo avviare i test;
- righe 43–45: il metodo [testTaxAdminData] è quello descritto nella sezione collegata;
I risultati del test sono i seguenti:

18.2.5.2. Test del livello [business]

Il test [ServerMetierTest] sarà il seguente:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test class
class ServerMetierTest extends \Codeception\Test\Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// creation of the [business] layer
$this->métier = new ServerMetier($dao);
}
// tests
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
Commenti
- Righe 9–24: configuriamo lo stesso ambiente di lavoro del server [impots-server.php]. Questo viene fatto nelle righe 9–12 definendo le due costanti da cui dipende l'ambiente;
- righe 30–38: creiamo un'istanza del livello [business] da testare, come è stato fatto nello script del server [impots-server.php];
- Da questo punto in poi, ci troviamo nelle stesse condizioni dello script del server [impots-server.php]: possiamo avviare i test;
- righe 40–53: i metodi [test1, test2…, test11] sono quelli descritti nella sezione collegata;
I risultati dei test sono i seguenti:

18.3. Il client
Ci stiamo concentrando sul lato client dell'applicazione.

Questa architettura sarà implementata dai seguenti script:

18.3.1. Entità scambiate tra i livelli

Le entità sopra elencate sono state tutte descritte e sono già in uso:
- [BaseEntity] nella sezione dei collegamenti;
- [ExceptionImpots] nella sezione dei collegamenti;
- [TaxPayerData] nella sezione dei collegamenti;
18.3.2. Il livello [dao]

Il livello [dao] implementa la seguente interfaccia [InterfaceClientDao]:
<?php
// namespace
namespace Application;
interface InterfaceClientDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// recording results
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- riga 9: la funzione [getTaxPayersData] carica in memoria i dati dei contribuenti dal file [$taxPayersFilename]. Eventuali errori vengono registrati nel file [$errorsFilename];
- riga 12: la funzione [calculateTaxes] calcola l'imposta di un contribuente;
- Riga 15: la funzione [saveResults] salva i dati dall'array [$taxPayersData] — che rappresenta i risultati di diversi calcoli fiscali — nel file [$resultsFilename];
L'interfaccia [InterfaceClientDao] è implementata dalla seguente classe [ClientDao]:
<?php
namespace Application;
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// using a Trait
use TraitDao;
// attributes
private $urlServer;
private $user;
// manufacturer
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// create a HTTP customer
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// make a request to the server
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// the answer is retrieved
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logs
// print "$json=json\n";
// retrieve response status
$statusCode = $response->getStatusCode();
// mistake?
if ($statusCode !== 200) {
// we have an error - we throw an exception
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// we return the answer
return $réponse;
}
}
Commenti
- riga 10: inseriamo [TraitDao] (vedi paragrafo collegato) che implementa i metodi [getTaxPayersData] e [saveResults]. Rimane da implementare solo il metodo [calculateTaxes]. Questo è implementato alle righe 22–49;
- righe 16–19: il costruttore della classe [ClientDao] accetta due parametri:
- l'URL [$urlServer] del server di calcolo delle imposte;
- l'array [$user] delle chiavi ‘login’ e ‘passwd’ che definisce l'utente che effettua la richiesta;
- riga 22: il metodo [calculateTaxes] riceve i tre parametri da inviare al server di calcolo delle imposte;
- righe 24–27: viene creato un client HTTP con:
- riga 25: le credenziali dell'utente che effettua la richiesta;
- riga 26: l'opzione che impedisce al client HTTP di verificare la validità del certificato SSL inviato dal server;
- righe 29–34: il server viene interrogato con i tre parametri che si aspetta;
- riga 36: recuperiamo la risposta JSON dal server. Se non impostiamo il parametro [false] sul metodo [Response::getContent], allora se lo stato della risposta del server rientra nell'intervallo [3xx-5xx] (caso di errore), l'oggetto [Response] genera un'eccezione non appena tentiamo di recuperare il contenuto della risposta [Response::getContent] o le sue intestazioni HTTP [Response::getHeaders]. Qui, indipendentemente dallo stato HTTP della risposta, vogliamo poter accedere al suo contenuto, se non altro per registrarlo (riga 40);
- righe 37-38: la risposta del server è una stringa JSON di un array [‘response’=>something]. Recuperiamo il [something];
- riga 40: registriamo la risposta JSON in modalità sviluppo;
- riga 42: recuperiamo il codice di stato della risposta;
- righe 44-49: se il codice di stato HTTP non è 200, allora il nostro server ha riscontrato un problema. Lanciamo quindi un'eccezione [ExceptionImpots] con un messaggio costituito dalla risposta JSON del server a cui è stato aggiunto il codice di stato HTTP;
- riga 51: restituiamo il risultato, che è un array associativo con le chiavi [tax, surcharge, discount, reduction, rate];
18.3.3. Il livello [aziendale]


Il livello [business] [8] implementa la seguente [BusinessClientInterface]:
<?php
// namespace
namespace Application;
interface InterfaceClientMetier {
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- riga 9: la funzione [calculateTaxes] calcola l'imposta;
- riga 12: la funzione [executeBatchImpots] calcola l'imposta per i contribuenti i cui dati si trovano nel file [$taxPayersFileName], scrive i risultati nel file [$resultsFileName] e scrive eventuali errori riscontrati nel file [$errorsFileName];
L'interfaccia [BusinessClientInterface] è implementata dalla seguente classe [BusinessClient]:
<?php
// namespace
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// attribute
private $clientDao;
// manufacturer
public function __construct(InterfaceClientDao $clientDao) {
// the reference is stored on the [dao] layer
$this->clientDao = $clientDao;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// we let the exceptions coming from the [dao] layer flow upwards
// retrieve taxpayer data
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// results table
$results = [];
// we exploit them
foreach ($taxPayersData as $taxPayerData) {
// tax calculation
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// complete [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// put the result in the results table
$results [] = $taxPayerData;
}
// recording results
$this->clientDao->saveResults($resultsFileName, $results);
}
}
Commenti
- righe 11–14: il costruttore della classe [ClientMetier] riceve come parametro un riferimento al livello [dao];
- righe 17–19: il calcolo delle imposte viene delegato al livello [dao];
- righe 20–38: la funzione [executeBatchImpots] è stata descritta nella sezione dei link;
18.3.4. Lo script principale


Lo script client [MainImpotsClient.php] implementa il livello [console] [9]. È configurato dal seguente file JSON [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"
}
- Riga 1: la directory principale del client;
- riga 2: il file JSON contenente i dati dei contribuenti;
- riga 3: il file JSON contenente i risultati;
- riga 4: il file JSON contenente gli errori;
- righe 6–19: le varie dipendenze del progetto client;
- righe 20–23: l'utente che invia richieste al server di calcolo delle imposte;
- riga 24: l'URL sicuro del server di calcolo delle imposte;
Il codice dello script [MainImpotsClient.php] è il seguente:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "../Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definition of constants
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// symfony dependencies
use Symfony\Component\HttpClient\HttpClient;
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$clientMetier = new ClientMetier($clientDao);
// tax calculation in batch mode
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// error is displayed
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;
Commenti
- riga 13: percorso del file di configurazione;
- riga 16: elaborazione del file di configurazione;
- righe 18–26: caricamento delle dipendenze;
- riga 37: creazione del livello [dao]. Passiamo le due informazioni richieste dal costruttore del livello:
- l'URL del server di calcolo delle imposte;
- le credenziali dell'utente che effettuerà le richieste;
- riga 39: creazione del livello [business]. Passiamo al costruttore del livello un riferimento al livello [dao] appena creato;
- riga 43: chiediamo al livello [business] di:
- calcolare le imposte per tutti i contribuenti nel file $config["taxPayerDataFileName"];
- scrivere i risultati nel file $config["resultsFileName"];
- scrivere gli errori nel file $config["errorsFileName"];
- La riga 43 potrebbe generare delle eccezioni;
- Riga 46: Visualizza il messaggio di errore dell'eccezione;
L'esecuzione del client produce gli stessi risultati delle versioni precedenti. Controllare i seguenti file:
- [Data/taxpayersdata.json]: dati dei contribuenti per i quali viene calcolato l'importo dell'imposta;
- [Data/results.json]: risultati relativi ai vari contribuenti presenti nel file [Data/taxpayersdata.json];
- [Data/errors.json]: errori che potrebbero essersi verificati durante l'elaborazione del file [Data/taxpayersdata.json];
Esaminiamo i possibili casi di errore. Innanzitutto, fermiamo il server Laragon. I risultati nella console client sono quindi i seguenti:
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é
Ora avviamo solo il server Apache e non il DBMS MySQL:

I risultati nella console client sono i seguenti:
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é
Ora avviamo MySQL e modifichiamo l'utente che si connette in [config-client]:
I risultati nella console del client sono i seguenti:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
18.3.5. Test [Codeception]
Come abbiamo fatto per le versioni precedenti, scriveremo i test [Codeception] per la versione 08.

18.3.5.1. Test del livello [business]
Il test [ClientMetierTest.php] è il seguente:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// test class
class ClientMetierTest extends \Codeception\Test\Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$this->métier = new ClientMetier($clientDao);
}
// tests
public function test1() {
…
}
-------------
public function test11() {
…
}
}
Commenti
- righe 10–26: definizione dell'ambiente di test. Utilizziamo lo stesso dello script principale [MainImpotsClient] descritto nella sezione collegata;
- righe 33–41: costruzione dei livelli [dao] e [business];
- riga 40: l'attributo [$this→business] fa riferimento al livello [business];
- righe 44–51: i metodi [test1, test2…, test11] sono quelli descritti nella sezione collegata;
I risultati del test sono i seguenti:
