Skip to content

19. Esercizio pratico – Versione 9

In questa versione, apporteremo i seguenti miglioramenti al server:

  • Attualmente, ad ogni richiesta, i dati dell'amministrazione fiscale vengono recuperati dal database. Utilizzeremo una sessione:
    • Durante la prima richiesta di un utente, i dati dell'autorità fiscale vengono recuperati dal database e memorizzati nella sessione;
    • Per le richieste successive dello stesso utente, i dati dell'autorità fiscale vengono recuperati dalla sessione. Possiamo aspettarci un leggero miglioramento nei tempi di esecuzione poiché le query al database richiedono molte risorse;
  • il server registrerà gli eventi importanti in un file di testo:
    • autenticazione riuscita o fallita;
    • se i parametri inviati dal client sono validi;
    • il risultato del calcolo delle imposte;
    • vari casi di errore;
  • in caso di errore irreversibile, verrà inviata un'e-mail all'amministratore dell'applicazione;

Il client deve inoltre essere modificato per gestire il cookie di sessione che gli verrà inviato.

19.1. Il server

Ci stiamo concentrando sul lato server dell'applicazione.

Image

Questa architettura sarà implementata dai seguenti script:

Image

19.1.1. Utilità

Image

19.1.1.1. La classe [Logger]

La classe [Logger] verrà utilizzata per scrivere i log in un file di testo:


<?php
 
namespace Application;
 
class Logger {
  // attribute
  private $resource;
 
  // manufacturer
  public function __construct(string $logsFilename) {
    // open file
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  // writing a message to the logs
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }
 
  // close log file
  public function close() {
    fclose($this->resource);
  }
 
}

Commenti

  • riga 7: la risorsa del file di log;
  • riga 10: il costruttore della classe riceve il nome del file di log come parametro;
  • riga 12: il file di testo viene aperto in modalità di aggiunta (a+): il file verrà aperto e il suo contenuto conservato. I nuovi dati verranno scritti dopo il contenuto corrente;
  • righe 13–15: se non è stato possibile aprire il file, viene generata un'eccezione;
  • righe 19–21: il metodo [write] scrive il messaggio [$message] nel file di log, preceduto dalla data e dall'ora;
  • righe 24–16: il metodo [close] chiude il file di log;

Nota: L'applicazione server può servire più client contemporaneamente. Tuttavia, esiste un solo file di log per tutti loro. Esiste quindi il rischio di accesso simultaneo durante la scrittura sul file. Le operazioni di scrittura devono quindi essere sincronizzate per evitare che si confondano. PHP fornisce i semafori [https://www.php.net/manual/fr/book.sem.php] a questo scopo. In questa sede ignoreremo la sincronizzazione della scrittura, ma dobbiamo rimanere consapevoli del problema.

19.1.1.2. La classe [SendAdminMail]

La classe [SendAdminMail] consente di inviare un'e-mail all'amministratore dell'applicazione in caso di crash:


<?php
 
namespace Application;
 
class SendAdminMail {
  // attributes
  private $config;
  private $logger;
 
  // manufacturer
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }
 
  public function send() {
    // sends $this->config['message'] to smtp server $this->config['smtp-server'] on port $infos[smt-port]
    // if $this->config['tls'] is true, TLS support will be used
    // mail is sent from $this->config['from']
    // for recipient $this->config['to']
    // message has subject $this->config['subject']
    // attachments from $this->config['attachments'] are attached to the mail
    // the result of the method
    try {
      // message creation
      $message = (new \Swift_Message())
        // message subject
        ->setSubject($this->config["subject"])
        // sender
        ->setFrom($this->config["from"])
        // recipients with a dictionary (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // message text
        ->setBody($this->config["message"])
      ;
      // attachments
      foreach ($this->config["attachments"] as $attachment) {
        // path of attachment
        $fileName = __DIR__ . $attachment;
        // check that the file exists
        if (file_exists($fileName)) {
          // attach the document to the message
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // error
            $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 {
        // no TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // the shipment manager
      $mailer = new \Swift_Mailer($transport);
      // sending the message
      $mailer->send($message);
      // end
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // error
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }
 
}

Commenti

  • Riga 11: Il costruttore accetta due parametri:
    • [$config]: un array associativo contenente tutte le informazioni necessarie per inviare l'e-mail;
    • [$logger]: un logger utilizzato per registrare i momenti chiave durante il processo di invio dell'e-mail;

L'array associativo avrà la seguente forma:

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": []
}
  • Righe 16–76: Il metodo [send] viene utilizzato per inviare l'e-mail. Questo codice è stato presentato e descritto nella sezione collegata;

19.1.2. Il livello [dao]

Image

Lo script [ServeurDaoWithSession.php] è il seguente:


<?php
 
// namespace
namespace Application;
 
// definition of a ImpotsWithDataInDatabase class
class ServerDaoWithSession extends ServerDao {
 
  // manufacturer
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // simplest case
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // hand over to parent class
      parent::__construct($databaseFilename);
    }
  }
 
}

Commenti

  • riga 7: la classe [ServerDaoWithSession] nella versione 09 estende la classe [ServerDao] della versione 08. Infatti, la classe [ServerDao] sa come utilizzare il database. Non resta che gestire il caso in cui i dati dell'amministrazione fiscale siano già stati recuperati:
  • riga 10: il costruttore ora accetta due parametri:
    • [string $databaseFilename]: nome del file contenente le informazioni necessarie per connettersi al database se i dati dell'amministrazione fiscale non sono ancora stati recuperati, NULL in caso contrario;
    • [TaxAdminData $taxAdminData]: i dati dell'amministrazione fiscale se già recuperati, NULL in caso contrario;

All'avvio di una sessione web, il livello [dao] verrà costruito con un oggetto [$databaseFilename] non NULL e un oggetto [taxAdminData] NULL. I dati dell'amministrazione fiscale verranno quindi recuperati dal database e memorizzati nella sessione. Per le richieste successive all'interno della stessa sessione, il livello [dao] verrà costruito con un oggetto [databaseFilename] NULL e un oggetto [taxAdminData] recuperato dalla sessione (che non è NULL). Pertanto, non verrà effettuata alcuna ricerca nel database.

19.1.3. Lo script del server

Lo script del server [impots-server.php] è configurato dal seguente file 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"
}

Lo script del server [impots-server.php] cambia 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";
}
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
// session
$session = new Session();
$session->start();
 
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
 
// log file creation
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // internal server error
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // completed
  exit;
}
 
// 1st log
$logger->write("\n---nouvelle requête\n");
 
// retrieve the current query
$request = Request::createFromGlobals();
// authentication only the 1st time
if (!$session->has("user")) {
  // log
  $logger->write("Autentification en cours…\n");
  // authentication

  }
  // has the user been found?
  if (!$trouvé) {
    // not found - 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
    );
    // completed
    exit;
  } else {
    // we note in the session that we have authenticated the user
    $session->set("user", TRUE);
    // log
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // log
  $logger->write("Authentification prise en session…\n");
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET

 
// mistakes?
if ($erreurs) {
// an error code 400 HTTP_BAD_REQUEST is sent to the customer
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // completed
  exit;
} else {
  // logs
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// we have everything you need to work
// creation of the [dao] layer
if (!$session->has("taxAdminData")) {
  // the data is taken from the database
  $logger->write("données fiscales prises en base de données\n");
  try {
    // construction of the [dao] layer
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    // put data in session
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // we note the error
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // completed
    exit;
  }
} else {
  // data are taken from the session
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // logs
  $logger->write("données fiscales prises en session\n");
}
// creation of the [business] layer
$métier = new ServerMetier($dao);
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// end
exit;
 
function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // send an e-mail to the administrator
  // SendAdminMail intercepts all exceptions and logs them itself
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // an error code 500 is sent to the customer
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}
 
// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $response : answer HTTP
  // $result: results table
  // $statusCode: HTTP response status
  // $headers: HTTP headers to be included in the response
  // $logger: application logger
  //
  // status HTTTP
  $response->setStatusCode($statusCode);
  // body
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // headers
  $response->headers->add($headers);
  // shipping
  $response->send();
  // log
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
}

Commenti

  • righe 34-35: avvia una sessione;
  • righe 38–40: prepara una risposta JSON;
  • righe 42–50: tentare di creare il file di log. Se si verifica un'eccezione, viene chiamato il metodo [doInternalServer] (righe 132–140);
  • riga 132: il metodo [doInternalServer] accetta quattro parametri:
    • [$message]: il messaggio da registrare. Deve essere codificato in UTF-8;
    • [$response]: l'oggetto [Response] che incapsula la risposta del server al proprio client;
    • [$logger]: l'oggetto [Logger] utilizzato per la registrazione;
    • [$infos]: le informazioni utilizzate per inviare un'e-mail all'amministratore dell'applicazione;
  • righe 135–137: viene inviata un'e-mail all'amministratore dell'applicazione;
  • riga 139: la risposta viene inviata al client:
    • $response: risposta HTTP;
    • $result: il server invia la stringa JSON dall'array [‘response’=>["error" => $message]];
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], codice 500;
    • $headers: [], nessun'intestazione HTTP da aggiungere alla risposta;
    • $logger: il logger dell'applicazione;
  • riga 58: grazie alla configurazione della sessione, autenticheremo il client una sola volta:
    • una volta che il client è autenticato, imposteremo una chiave [user] nella sessione (riga 78);
    • durante la richiesta successiva dallo stesso client, la riga 58 impedisce un'autenticazione non necessaria;
  • riga 103: grazie alla sessione stabilita, effettueremo la ricerca nel database una sola volta:
    • durante la prima richiesta, verrà eseguita la ricerca nel database (riga 108). I dati recuperati vengono quindi memorizzati nella sessione (riga 110) associati alla chiave [taxAdminData];
    • per le richieste successive, la chiave [taxAdminData] verrà trovata nella sessione (riga 103) e i dati su disco verranno quindi passati direttamente al livello [dao] (riga 119);
  • righe 111–116: la ricerca dei dati fiscali nel database potrebbe fallire. In questo caso, viene inviato al client un codice [500 Internal Server Error];
  • riga 113: il messaggio di errore derivante dall'eccezione del driver MySQL è codificato in ISO 8859-1. Viene convertito in UTF-8 per essere registrato correttamente;
  • il resto del codice è quasi identico a quello della versione precedente;
  • righe 143–164: la funzione [sendResponse] invia tutte le risposte al client;
  • righe 144–148: significato dei parametri;
  • riga 153: la risposta è sempre la stringa JSON di un array [‘result’=>qualcosa];
  • riga 156: a volte ci sono intestazioni HTTP da aggiungere alla risposta. Questo è il caso della riga 71;
  • riga 158: la risposta viene inviata;
  • righe 160–163: la risposta viene registrata e il logger viene chiuso;

19.1.4. [Codeception] Test

Image

Testeremo solo il livello [dao], che è l'unico ad essere cambiato.

Il codice di test [ServerDaoTest] è 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-09");
// 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 ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  public function testTaxAdminData() {

  }
 
}
  • righe 9–24: creiamo un ambiente di runtime identico a quello dello script del server [impots-server];
  • riga 38: per costruire il livello [dao], istanziamo la classe [ServerDaoWithSession];

I risultati del test sono i seguenti:

Image

19.2. Il client

Ci stiamo concentrando sul lato client dell'applicazione.

Image

Questa architettura sarà implementata dai seguenti script:

Image

Nella nuova versione, le uniche modifiche sono:

  • il file di configurazione [config-client.json];
  • il livello [dao] del client;

19.2.1. Il livello [dao]

Il livello [Dao] si evolve come segue:


<?php
 
namespace Application;
 
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
 
class ClientDao implements InterfaceClientDao {
  // using a Trait
  use TraitDao;
  // attributes
  private $urlServer;
  private $user;
  private $sessionCookie;
 
  // 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 {
    // session cookie ?
    if (!$this->sessionCookie) {
      // create a HTTP customer
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // make the request to the server without a session cookie
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // make a request to the server with the session cookie
      // create a HTTP customer
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // 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);
    }
    if (!$this->sessionCookie) {
      // retrieve the session cookie
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // session cookie ?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // we return the answer
    return $réponse;
  }
 
}

Commenti

La modifica al livello [dao] ora comporta la gestione di una sessione:

  • riga 14: il cookie di sessione;
  • righe 25–39: durante la prima richiesta, questo cookie non esiste; inviamo quindi la richiesta al server inviando le informazioni di autenticazione (riga 28);
  • righe 40–53: per le richieste successive, normalmente disponiamo del cookie di sessione. Pertanto non inviamo le informazioni di autenticazione (righe 42–44);
  • righe 69-82: la risposta del server alla prima richiesta includerà un cookie di sessione. Lo recuperiamo. Questo codice è già stato utilizzato e spiegato nella sezione collegata;
  • riga 78: il cookie di sessione recuperato viene memorizzato nell'attributo della classe [$sessionCookie];

Nota: avremmo potuto mantenere la vecchia versione del livello [dao] ed eseguire l'autenticazione su ogni richiesta, poiché il costo è trascurabile. A scopo didattico, abbiamo voluto dimostrare come un client HTTP possa gestire una sessione.

19.2.2. Il file di configurazione

Il file di configurazione JSON si evolve come segue:


{
    "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"
}

Cambia solo l'URL alla riga 24.

19.3. Alcuni test

19.3.1. Test 1

Per prima cosa, eseguiamo il client in un ambiente privo di errori. I risultati sono gli stessi delle versioni precedenti. Ma ora, sul lato server, abbiamo un file di log [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

  • righe 3-7: durante la prima richiesta, avviene l'autenticazione e i dati vengono recuperati dal database;
  • righe 9-14: durante la richiesta successiva, non viene eseguita alcuna ulteriore autenticazione e i dati vengono recuperati dalla sessione. Questo si ripete per le richieste successive (righe 15 e seguenti);

19.3.2. Test 2

Ora chiudiamo il database MySQL. Sul lato client, otteniamo il seguente output della console:


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é

Sul lato server, abbiamo i seguenti log [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

Per recuperare l'e-mail ricevuta dall'amministratore dell'applicazione, utilizziamo lo script [imap-03.php] dalla sezione collegata al seguente file di configurazione [config-imap-01.json]:

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

Si ottiene il seguente risultato:

Image

Il file [message_1.txt] contiene il seguente testo:


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

Ora assicuriamoci che il file [logs.txt] non possa essere creato. Per farlo, basta creare una cartella [logs.txt]:

Image

Una volta fatto questo, avviamo il client.

Sul lato client, otteniamo il seguente output della console:


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

Sul lato server non sono presenti log, ma l'amministratore riceve la seguente email:


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

Questa volta, forniamo credenziali errate al client in connessione nel file di configurazione del client.

Il client visualizza il seguente output della console:


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

Sul lato server compaiono i seguenti log:


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

Reinseriamo il nome utente e la password corretti [admin, admin] nel file di configurazione del client.

Ora richiediamo l'URL del server [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] direttamente in un browser senza passare alcun parametro:

Nel file di log del server [logs.txt], vediamo le seguenti righe:


---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. Test [Codeception]

Come abbiamo fatto per le versioni precedenti, scriveremo i test [Codeception] per la versione 09.

Image

19.4.0.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-09");
 
// 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";
}
//
// uses
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;
 
// test class
class ClientMetierTest extends Unit {
  
}

Commenti

  • Rispetto alla classe di test della versione 08, l'unica modifica è la riga 10, che specifica la directory principale del client da testare;

I risultati del test sono i seguenti:

Image

Vale la pena controllare i log del server [logs.txt]:


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}}

Possiamo notare che i dati dell'autorità fiscale vengono sempre recuperati dal database e mai dalla sessione. Torniamo al codice del test eseguito:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 

 
// test class
class ClientMetierTest extends 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 test2() {

  }
 
  public function test3() {

  }
 

 
}

In una classe di test [Codeception], il costruttore viene eseguito per ogni test.

  • Riga 21: Viene quindi creato un nuovo [ClientDao] per ogni test con un cookie di sessione NULL. Questo spiega perché questo client non beneficia di alcuna sessione;

Questo esempio ci mostra che la sessione non è il luogo adatto per memorizzare i dati dell'amministrazione fiscale. Infatti, questi dati sono condivisi tra tutti gli utenti dell'applicazione. Tuttavia, qui vengono duplicati in ciascuna delle loro sessioni.

Nella programmazione web, esistono tre tipi di visibilità per i dati condivisi:

  • dati condivisi da tutti gli utenti dell'applicazione web. Si tratta generalmente di dati di sola lettura. PHP non supporta nativamente questo tipo di archiviazione;
  • dati condivisi tra le richieste provenienti dallo stesso client. Questi dati sono memorizzati nella sessione. Ci riferiamo a questa come alla sessione del client per indicare l'archivio del client. Tutte le richieste provenienti da un client hanno accesso a questa sessione. Possono memorizzare e leggere informazioni al suo interno. Negli script precedenti, questa sessione è implementata dall'oggetto Symfony [HttpFoundation\Session\Session];
  • la memoria della richiesta, o contesto della richiesta. La richiesta di un utente può essere elaborata da diverse azioni successive. Il contesto della richiesta consente all'Azione 1 di passare informazioni all'Azione 2. Negli script precedenti, la richiesta è implementata dall'oggetto Symfony [HttpFoundation\Request] e la sua memoria dall'attributo [HttpFoundation\Request::attributes];

Image

Esistono librerie di terze parti per fornire a PHP lo stato dell'applicazione. La nuova versione dell'esercizio sull'applicazione dimostra l'uso di una di esse.