Skip to content

19. Esercizio pratico – versione 9

In questa versione, miglioreremo il server nel modo seguente:

  • attualmente, ad ogni richiesta i dati dell’amministrazione fiscale vengono ricercati nel database. Utilizzeremo una sessione:
    • alla prima richiesta di un utente, i dati dell’amministrazione fiscale vengono recuperati dal database e inseriti nella sessione;
    • nelle richieste successive dello stesso utente, i dati dell’amministrazione fiscale vengono recuperati dalla sessione. Ci si può aspettare un leggero risparmio in termini di tempo di esecuzione, poiché le query al database sono costose;
  • il server registrerà in un file di testo i momenti salienti:
    • l’autenticazione riuscita o fallita;
    • la validità o meno dei parametri inviati dal cliente;
    • il risultato del calcolo dell’imposta;
    • i diversi casi di errore;
  • in caso di errore grave, verrà inviata un'e-mail all'amministratore dell'applicazione;

Anche il client dovrà essere modificato per gestire il cookie di sessione che gli verrà inviato.

19.1. Il server

Ci occupiamo della parte server dell’applicazione.

Image

Questa architettura verrà 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 {
  // attributo
  private $resource;

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

  // scrittura di un messaggio nei log
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }

  // chiusura del file di log
  public function close() {
    fclose($this->resource);
  }

}

Commenti

  • riga 7: la risorsa del file di log;
  • riga 10: il costruttore della classe riceve come parametro il nome del file di log;
  • riga 12: si apre il file di testo in modalità aggiunta (a+): il file verrà aperto e il suo contenuto conservato. Le scritture verranno effettuate alla fine del contenuto attuale;
  • righe 13-15: se l’apertura non è andata a buon fine, viene generata un’eccezione;
  • righe 19-21: il metodo [write] consente di scrivere il messaggio [$message] nel file di log, preceduto dalla data e dall’ora;
  • righe 24-16: il metodo [close] consente di chiudere il file di log;

Nota: l’applicazione server può servire più client contemporaneamente. Tuttavia, è presente un unico file di log per tutti. Esiste quindi il rischio di accessi concorrenti per la scrittura nel file. Sarebbe quindi necessario sincronizzare le operazioni di scrittura per evitare che si mescolino. A tal fine, PHP dispone dei semafori [https://www.php.net/manual/fr/book.sem.php]. In questa sede tralasceremo la sincronizzazione delle operazioni di scrittura, ma è necessario essere 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 arresto anomalo della stessa:


<?php

namespace Application;

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

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

  public function send() {
    // invia $this->config['message'] al server SMTP $this->config['smtp-server'] sulla porta $infos[smt-port]
    // se $this->config['tls'] è vero, verrà utilizzato il supporto TLS
    // l'e-mail viene inviata a nome di $this->config['from']
    // per il destinatario $this->config['to']
    // il messaggio ha come oggetto $this->config['subject']
    // all'e-mail vengono allegati i file di $this->config['attachments']
    // il risultato del metodo
    try {
      // creazione del messaggio
      $message = (new \Swift_Message())
        // oggetto del messaggio
        ->setSubject($this->config["subject"])
        // mittente
        ->setFrom($this->config["from"])
        // destinatari con un dizionario (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // testo del messaggio
        ->setBody($this->config["message"])
      ;
      // allegati
      foreach ($this->config["attachments"] as $attachment) {
        // percorso dell'allegato
        $fileName = __DIR__ . $attachment;
        // si verifica che il file esista
        if (file_exists($fileName)) {
          // si allega il documento al messaggio
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // errore
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // protocollo 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 {
        // nessun TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // il gestore dell'invio
      $mailer = new \Swift_Mailer($transport);
      // invio del messaggio
      $mailer->send($message);
      // fine
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // errore
      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 riceve due parametri:
    • [$config]: un array associativo contenente tutte le informazioni necessarie per l'invio dell'e-mail;
    • [$logger]: un logger che consente di registrare i momenti salienti dell’invio dell’e-mail;

L'array associativo avrà la seguente struttura:

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] consente di inviare l’e-mail. Questo codice è stato presentato e descritto nel paragrafo link;

19.1.2. Il livello [dao]

Image

Lo script [ServeurDaoWithSession.php] è il seguente:


<?php

// spazio dei nomi
namespace Application;

// definizione di una classe ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {

  // costruttore
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // caso più semplice
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // si passa il controllo alla classe padre
      parent::__construct($databaseFilename);
    }
  }

}

Commenti

  • riga 7: la classe [ServerDaoWithSession] della versione 09 estende la classe [ServerDao] della versione 08. Infatti, la classe [ServerDao] è in grado di utilizzare il database. Non ci resta che prevedere il caso in cui i dati dell’amministrazione fiscale siano già stati acquisiti:
  • riga 10: il costruttore ora riceve 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 acquisiti, NULL in caso contrario;
    • [TaxAdminData $taxAdminData]: i dati dell’amministrazione fiscale se già acquisiti, NULL in caso contrario;

All’avvio di una sessione web, il livello [dao] verrà costruito con un oggetto [$databaseFilename] diverso da NULL e un oggetto [taxAdminData] NULL. I dati dell’amministrazione fiscale verranno quindi recuperati dal database e memorizzati nella sessione. Nelle successive richieste della stessa sessione, il livello [dao] verrà costruito con un oggetto [databaseFilename], NULL e un oggetto [taxAdminData] provenienti dalla sessione e non NULL. Non verrà quindi eseguita 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] si evolve come segue:


<?php

// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);

// spazio dei nomi
namespace Application;

// gestione degli errori tramite PHP
ini_set("display_errors", "0");
//
// percorso del file di configurazione
define("CONFIG_FILENAME", "Data/config-server.json");

// si recupera la configurazione
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// si includono le dipendenze necessarie allo script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// dipendenze assolute (librerie di terze parti)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// dipendenze Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

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

// preparazione della risposta JSON del server
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// creazione del file di log
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // errore interno del server
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // completato
  exit;
}

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

// recupero della richiesta corrente
$request = Request::createFromGlobals();
// autenticazione solo la prima volta
if (!$session->has("user")) {
  // log
  $logger->write("Autentification en cours…\n");
  // autenticazione

  }
  // l'utente è stato trovato?
  if (!$trouvé) {
    // non trovato - codice 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
    );
    // completato
    exit;
  } else {
    // si registra nella sessione che l'utente è stato autenticato
    $session->set("user", TRUE);
    // log
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // log
  $logger->write("Authentification prise en session…\n");
}
// si dispone di un utente valido - si verificano i parametri ricevuti
$erreurs = [];
// devono esserci tre parametri GET


// errori?
if ($erreurs) {
// viene inviato al cliente un codice di errore 400 HTTP_BAD_REQUEST
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // completato
  exit;
} else {
  // log
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// abbiamo tutto il necessario per lavorare
// creazione del livello [dao]
if (!$session->has("taxAdminData")) {
  // i dati vengono prelevati dal database
  $logger->write("données fiscales prises en base de données\n");
  try {
    // costruzione del livello [dao]
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    // i dati vengono inseriti nella sessione
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // si registra l'errore
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // completato
    exit;
  }
} else {
  // i dati vengono acquisiti nella sessione
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // log
  $logger->write("données fiscales prises en session\n");
}
// creazione del livello [métier]
$métier = new ServerMetier($dao);
// calcolo dell'imposta
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// viene fornita la risposta
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// fine
exit;

function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // invio di un'e-mail all'amministratore
  // SendAdminMail intercetta tutte le eccezioni e le registra autonomamente
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // si invia un codice di errore 500 al cliente
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}

// funzione di invio della risposta HTTP al cliente
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $response: risposta HTTP
  // $result: tabella dei risultati
  // $statusCode: stato HTTP della risposta
  // $headers: intestazioni HTTP da inserire nella risposta
  // $logger: il logger dell'applicazione
  //
  // stato HTTTP
  $response->setStatusCode($statusCode);
  // corpo
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // intestazioni
  $response->headers->add($headers);
  // invio
  $response->send();
  // log
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
}

Commenti

  • righe 34-35: si avvia una sessione;
  • righe 38-40: si prepara una risposta jSON;
  • righe 42-50: si tenta di creare il file di log. In caso di 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] che consente di generare i log;
    • [$infos]: le informazioni necessarie per inviare un'e-mail all'amministratore dell'applicazione;
  • righe 135-137: si invia un'e-mail all'amministratore dell'applicazione;
  • riga 139: si invia la risposta al cliente:
    • $response: risposta HTTP;
    • $result: il server invia la stringa jSON dall’array [‘réponse’=>["erreur" => $message]];
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], codice 500;
    • $headers: [], nessuna intestazione HTTP da aggiungere alla risposta;
    • $logger: il logger dell'applicazione;
  • riga 58: grazie alla sessione impostata, l'autenticazione del cliente verrà effettuata una sola volta:
    • una volta autenticato il cliente, si inserirà una chiave [user] nella sessione (riga 78);
    • alla successiva richiesta dello stesso cliente, la riga 58 evita un’autenticazione ormai superflua;
  • riga 103: grazie alla sessione creata, i dati verranno recuperati dal database una sola volta:
    • alla prima richiesta, verrà eseguita la ricerca nel database (riga 108). I dati recuperati vengono quindi inseriti nella sessione (riga 110) associati alla chiave [taxAdminData];
    • nelle richieste successive, la chiave [taxAdminData] verrà individuata nella sessione (riga 103) e quindi i dati presenti su disco verranno comunicati direttamente al livello [dao] (riga 119);
  • righe 111-116: la ricerca dei dati fiscali nel database potrebbe non andare a buon fine. In tal caso, viene inviato al cliente un codice [500 Internal Server Error];
  • riga 113: il messaggio di errore dell’eccezione del driver MySQL è codificato come ISO 8859-1. Viene convertito in UTF-8 per essere correttamente registrato nel log;
  • 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 [‘résultat’=>qqChose];
  • riga 156: a volte è necessario aggiungere alla risposta delle intestazioni HTTP. È il caso della riga 71;
  • riga 158: la risposta viene inviata;
  • righe 160-163: la risposta viene registrata nel log e il logger viene chiuso;

19.1.4. Test [Codeception]

Image

Testeremo solo il livello [dao], l'unico ad essere stato modificato.

Il codice del test [ServerDaoTest] è il seguente:


<?php

// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);

// spazio dei nomi
namespace Application;

// definizione delle costanti
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// percorso del file di configurazione
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");

// si recupera la configurazione
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// si includono le dipendenze necessarie allo script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// dipendenze assolute (librerie di terze parti)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// test -----------------------------------------------------

class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    // genitore
    parent::__construct();
    // si recupera la configurazione
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creazione del livello [dao]
    $dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // test
  public function testTaxAdminData() {

  }

}
  • righe 9-24: si crea un ambiente di esecuzione identico a quello dello script server [impots-server];
  • riga 38: per costruire il livello [dao], si istanzia la classe [ServerDaoWithSession];

Il risultato dei test è il seguente:

Image

19.2. Il client

Ci concentriamo sulla parte client dell’applicazione.

Image

Questa architettura sarà implementata dai seguenti script:

Image

Nella nuova versione cambiano solo:

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

19.2.1. il livello [dao]

Il livello [Dao] subisce le seguenti modifiche:


<?php

namespace Application;

// dipendenze
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // utilizzo di un Trait
  use TraitDao;
  // attributi
  private $urlServer;
  private $user;
  private $sessionCookie;

  // costruttore
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }

  // calcolo dell'imposta
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // cookie di sessione?
    if (!$this->sessionCookie) {
      // si crea un cliente HTTP
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // si invia la richiesta al server senza cookie di sessione
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // si invia la richiesta al server con il cookie di sessione
      // si crea un client HTTP
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // si recupera la risposta
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // log
    print "$json=json\n";
    // si recupera lo stato della risposta
    $statusCode = $response->getStatusCode();
    // errore?
    if ($statusCode !== 200) {
      // si verifica un errore - viene generata un'eccezione
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    if (!$this->sessionCookie) {
      // si recupera il cookie di sessione
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // cookie di sessione?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // si restituisce la risposta
    return $réponse;
  }

}

Commenti

La modifica del livello [dao] consiste ora nella gestione di una sessione:

  • riga 14: il cookie di sessione;
  • righe 25-39: alla prima richiesta questo cookie non esiste; si effettua quindi la richiesta al server inviando le informazioni di autenticazione (riga 28);
  • righe 40-53: nelle richieste successive, di norma si dispone del cookie di sessione. In tal caso non si inviano le informazioni di autenticazione (righe 42-44);
  • righe 69-82: la risposta del server alla prima richiesta conterrà un cookie di sessione. Lo si recupera. Questo codice è già stato utilizzato e spiegato nel paragrafo link;
  • riga 78: il cookie di sessione recuperato viene memorizzato nell’attributo di classe [$sessionCookie];

Nota: avremmo potuto mantenere la versione precedente del livello [dao] ed eseguire l’autenticazione ad ogni richiesta, poiché il costo di tale operazione è trascurabile. Per motivi didattici, abbiamo voluto ricordare come un client HTTP potesse gestire una sessione.

19.2.2. Il file di configurazione

Il file di configurazione jSON viene modificato 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 il valore URL della 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 sempre quelli delle versioni precedenti. Ma ora, sul lato server, abbiamo un file di log denominato [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 la ricerca dei dati nel database;
  • righe 9-14: durante la richiesta successiva, non avviene più l’autenticazione e i dati vengono prelevati dalla sessione. Ciò si ripete nelle richieste successive (righe 15 e seguenti);

19.3.2. Test 2

Ora disattiviamo il database MySQL. Sul lato client, si ottiene il seguente risultato in 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, si ottengono 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 visualizzare l’e-mail ricevuta dall’amministratore dell’applicazione, utilizziamo lo script [imap-03.php] descritto nel paragrafo relativo al 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 facciamo in modo che il file [logs.txt] non possa essere creato. Per farlo, basta creare una cartella denominata [logs.txt]:

Image

Fatto ciò, eseguiamo il client.

Sul lato client, si ottengono i seguenti risultati in 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 ci sono log, ma l’amministratore riceve la seguente e-mail:


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

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

19.3.4. Test 4

Questa volta, nel file di configurazione del client, inseriamo credenziali errate per il client che si connette.

Il client visualizza i seguenti risultati in 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 l'utente corretto [admin, admin] nel file di configurazione del client.

Ora richiediamo l'accesso al server URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] direttamente da un browser senza specificare alcun parametro:

Nel file di log [logs.txt] del server sono presenti 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 già fatto per le versioni precedenti, scriveremo dei test [Codeception] per la versione 09.

Image

19.4.0.1. Test del livello [métier]

Il test [ClientMetierTest.php] è il seguente:


<?php

// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);

// spazio dei nomi
namespace Application;

// definizione delle costanti
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09");

// percorso del file di configurazione
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");

// si recupera la configurazione
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// si includono le dipendenze necessarie allo script
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// dipendenze assolute (librerie di terze parti)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// utilizza
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;

// classe di test
class ClientMetierTest extends Unit {
  
}

Commenti

  • rispetto alla classe di test della versione 08, cambia solo la riga 10 che specifica la cartella radice del client da testare;

I risultati del test sono i seguenti:

Image

È interessante dare un'occhiata ai 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}}

Si nota che i dati dell’amministrazione fiscale vengono sempre prelevati dal database e mai dalla sessione. Torniamo al codice del test eseguito:


<?php

// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);

// spazio dei nomi
namespace Application;



// classe di test
class ClientMetierTest extends Unit {
  // livello business
  private $métier;

  public function __construct() {
    parent::__construct();
    // si recupera la configurazione
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creazione del livello [dao]
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // creazione del livello [métier]
    $this->métier = new ClientMetier($clientDao);
  }

  // test
  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 cliente 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 sono comuni a tutti gli utenti dell’applicazione. In questo caso, però, vengono duplicati in ciascuna delle loro sessioni.

Nella programmazione web si distinguono tre tipi di visibilità per i dati condivisi:

  • dati condivisi da tutti gli utenti dell’applicazione web. Si tratta in genere di dati in sola lettura. PHP non dispone nativamente di questa memoria;
  • dati condivisi tra le richieste dello stesso client. Questi dati vengono memorizzati nella sessione. Si parla quindi di “sessione client” per indicare la memoria del client. Tutte le richieste di un client hanno accesso a questa sessione e possono memorizzarvi e leggerne le informazioni. 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 a un’azione 1 di trasmettere informazioni a un’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 una memoria dell’applicazione. La nuova versione dell’esercizio pratico mostra l’utilizzo di una di esse.