Skip to content

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:

Image

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

Image

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

Image

Questa architettura sarà implementata dai seguenti script:

Image

18.2.1. Entità scambiate tra i livelli

Image

Le entità scambiate tra i livelli sono quelle della versione 5 descritte nella sezione collegata.

18.2.2. Il livello [dao]

Image

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]

Image

Image

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

Image

Image

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

Image

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

Image

  • in [6], continuiamo;

Image

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

Image

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

Image

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

Image

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

Image

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

18.2.5.1. Test per il livello [dao]

Image

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:

Image

18.2.5.2. Test del livello [business]

Image

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:

Image

18.3. Il client

Ci stiamo concentrando sul lato client dell'applicazione.

Image

Questa architettura sarà implementata dai seguenti script:

Image

18.3.1. Entità scambiate tra i livelli

Image

Le entità sopra elencate sono state tutte descritte e sono già in uso:

18.3.2. Il livello [dao]

Image

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]

Image

Image

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

Image

Image

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:

Image

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

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

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.

Image

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:

Image