Skip to content

23. Esercizio pratico – Versione 12

In questo capitolo scriveremo un'applicazione web che segue l'architettura MVC (Model-View-Controller). L'applicazione sarà in grado di restituire risposte in tre formati: JSON, XML e HTML. C'è un aumento significativo di complessità tra ciò che stiamo per fare e ciò che abbiamo fatto in precedenza. Riutilizzeremo la maggior parte dei concetti trattati finora e descriveremo in dettaglio tutti i passaggi che portano all'applicazione finale.

23.1. Architettura MVC

Implementeremo il modello architettonico MVC (Model–View–Controller) come segue:

Image

L'elaborazione di una richiesta del client procederà come segue:

  • 1 - Richiesta

Gli URL richiesti avranno il formato http://machine:port/contexte/….?action=anAction&param1=v1&param2=v2&… Il [Controller principale] utilizzerà un file di configurazione per "instradare" la richiesta al controller corretto e all'azione corretta all'interno di quel controller. Per farlo, utilizzerà il campo [action] nell'URL. Il resto dell'URL [param1=v1&param2=v2&…] è costituito da parametri opzionali che verranno passati all'azione. La C in MVC in questo caso è la catena [Controller principale, Controller / Azione]. Se nessun controller è in grado di gestire l'azione richiesta, il server web risponderà che l'URL richiesto non è stato trovato.

  • 2 - Elaborazione
    • L'azione selezionata [2a] può utilizzare i parametri che il [Controller principale] le ha passato. Questi possono provenire da diverse fonti:
      • il percorso [/param1/param2/…] dell'URL,
      • i parametri dell'URL [param1=v1&param2=v2],
      • i parametri inviati dal browser con la sua richiesta;
    • Durante l'elaborazione della richiesta dell'utente, l'azione potrebbe richiedere il livello [business] [2b]. Una volta elaborata la richiesta del client, questa può innescare varie risposte. Un esempio classico è:
      • una risposta di errore se la richiesta non è stata elaborata correttamente;
      • una risposta di conferma in caso contrario;
    • il [Controller / Action] restituirà la sua risposta [2c] al controller principale insieme a un codice di stato. Questi codici di stato rappresenteranno in modo univoco lo stato dell'applicazione. Saranno codici di successo o codici di errore;
  • 3 - Risposta
    • A seconda che il client abbia richiesto una risposta JSON, XML o HTML, il [Controller principale] istanzierà [3a] il tipo di risposta appropriato e gli darà istruzioni di inviare la risposta al client. Il [Controller principale] gli passerà sia la risposta che il codice di stato fornito dal [Controller/Azione] che è stato eseguito;
    • se la risposta desiderata è di tipo JSON o XML, la risposta selezionata formatterà la risposta proveniente dal [Controller/Azione] che le è stata fornita e la invierà [3c]. Il client in grado di elaborare questa risposta può essere uno script della console PHP o uno script JavaScript incorporato in una pagina HTML;
    • Se la risposta desiderata è di tipo HTML, la risposta selezionata sceglierà [3b] una delle viste HTML [Vuei] utilizzando il codice di stato che le è stato fornito. Questa è la "V" in MVC. Una singola vista corrisponde a un codice di stato. Questa vista V visualizzerà la risposta proveniente dal [Controller / Action] che è stato eseguito. Avvolge i dati di questa risposta in HTML, CSS e JavaScript. Questi dati sono chiamati modello di vista. Questa è la M in MVC. Il client è molto spesso un browser;

Ora, chiariamo la relazione tra l'architettura web MVC e l'architettura a livelli. A seconda di come è definito il modello, questi due concetti possono essere correlati o meno. Consideriamo un'applicazione web MVC a singolo livello:

Image

Nell'esempio sopra riportato, il [Controller / Action] incorporano ciascuno parti dei livelli [business] e [DAO]. Nel livello [web] è presente un'architettura MVC, ma l'applicazione nel suo complesso non presenta un'architettura a livelli. In questo caso, c'è un unico livello che si occupa di tutto.

Ora, consideriamo un'architettura web a più livelli:

Image

Il livello [Web] può essere implementato senza seguire il modello MVC. Abbiamo quindi un'architettura a più livelli, ma il livello Web non implementa il modello MVC.

Ad esempio, nel mondo .NET, il livello [web] sopra indicato può essere implementato con ASP.NET MVC, ottenendo un'architettura a livelli con un livello [web] in stile MVC. Fatto ciò, possiamo sostituire questo livello ASP.NET MVC con un livello ASP.NET classico (WebForms) mantenendo il resto (logica di business, DAO, driver) invariato. Otterremo così un'architettura a livelli con un livello [web] che non è più basato su MVC.

In MVC, abbiamo detto che il modello M era quello della vista V, ovvero l'insieme di dati visualizzati dalla vista V. Viene fornita un'altra definizione del modello M in MVC:

Image

Molti autori ritengono che ciò che si trova a destra del livello [web] costituisca il modello M dell'MVC. Per evitare ambiguità, possiamo fare riferimento a:

  • il modello di dominio quando ci si riferisce a tutto ciò che si trova a destra del livello [web];
  • il modello di vista quando ci si riferisce ai dati visualizzati da una vista V;

23.2. Albero del progetto NetBeans

Per il progetto NetBeans, adotteremo un'architettura che riflette il modello MVC:

Image

  • [3]: [main.php] è il controller principale del nostro modello MVC. È la C in MVC;
  • [4]: La cartella [Controllers] conterrà i controller secondari. Ciascuno gestisce un'azione specifica. Questa azione è indicata nell'URL, ad esempio […/main.php?action=authenticate-user]. Con questa azione, il [Controllore principale] [main.php] selezionerà un [Controllore secondario], in questo caso [AuthentifierUtilisateurController], per gestire l'azione richiesta. Anche questi controllori fanno parte della C in MVC;
  • [5]: La cartella [Model] conterrà i livelli [business] e [DAO] dell'applicazione. Secondo i termini adottati in precedenza, questi elementi rappresentano il modello di dominio e, secondo la terminologia adottata per la M, possono rappresentare la M in MVC;
  • [6]: La cartella [Responses] contiene le classi responsabili dell'invio della risposta al client. Esiste una classe per ogni tipo di risposta desiderata:
    • [JsonResponse]: per una risposta JSON;
    • [XmlResponse]: per una risposta XML;
    • [HtmlResponse]: per una risposta HTML;
  • [7]: La cartella [Views] contiene le viste HTML quando si desidera una risposta HTML. Questa è la V in MVC. Sono attivate dalla classe [HtmlResponse], che passa loro i dati da visualizzare. Questi dati sono il modello di vista. A seconda della terminologia adottata per la M, questi dati possono rappresentare la M in MVC;
  • [8]: La cartella [Utilities] contiene programmi di utilità:
    • [Logger]: la classe che consente di registrare in un file di testo;
    • [Sendmail]: la classe che consente di inviare e-mail;
  • [9]: La cartella [Logs] contiene il file di log [logs.txt];
  • [10]: La cartella [Entities] contiene le classi utilizzate dai vari controller;

Utilizzando questa struttura di directory, possiamo descrivere il flusso di elaborazione di un'azione richiesta da un client:

  • [main.php] [3] riceve la richiesta;
  • dopo aver eseguito alcuni controlli preliminari (l'azione è una di quelle accettate?), inoltra la richiesta al controller secondario [4] responsabile dell'elaborazione di questa azione;
  • il controller secondario esegue il proprio compito. Nel farlo, potrebbe aver bisogno dei livelli [business] e [DAO] [5] nonché delle entità dalla cartella [10]. Restituisce la propria risposta al controller principale [main.php] che lo ha attivato;
  • A seconda del tipo di risposta [JSON, XML, HTML] richiesta dal client, il controller principale [main.php] attiva una delle risposte dalla cartella [Responses] [6];
  • le risposte [JsonResponse] e [XmlResponse] inviano rispettivamente la risposta JSON o XML al client;
  • [HtmlResponse] utilizza una delle viste dalla cartella [Views] [7] per inviare una risposta HTML al client;
  • I vari controller hanno accesso alla classe [Logger] nella cartella [8] per scrivere i log nel file di log nella cartella [9]. Vengono registrati:
    • l'azione richiesta;
    • la risposta del controller. Questa viene registrata in formato JSON indipendentemente dal tipo richiesto [JSON, XML, HTML];
  • in caso di errore fatale (HTTP_INTERNAL_SERVER_ERROR), il controller principale [main.php] invia un'e-mail all'amministratore utilizzando la classe [SendMail] nella cartella [8];

23.3. Azioni dell'applicazione

Il client invia l'azione da eseguire al server web come parametro [action] nell'URL [/main.php?action=xxx]. Le azioni consentite sono elencate nel file [config.json] che configura il controller principale [main.php]:


"actions":
            {
                "init-session": "\\InitSessionController",
                "authentifier-utilisateur": "\\AuthentifierUtilisateurController",
                "calculer-impot": "\\CalculerImpotController",
                "lister-simulations": "\\ListerSimulationsController",
                "supprimer-simulation": "\\SupprimerSimulationController",
                "fin-session": "\\FinSessionController",
                "afficher-calcul-impot": "\\AfficherCalculImpotController"
},
  • riga 1: la chiave [actions] del dizionario JSON;
  • righe 3–9: un dizionario [azione:controller]. Ogni azione è associata al controller secondario responsabile della sua elaborazione;
  • riga 3: [init-session]: avvia una sessione di simulazioni di calcolo delle imposte. Questa azione specifica il tipo di risposta desiderato [JSON, XML, HTML];
  • riga 4: una volta impostato il tipo di sessione, il client deve autenticarsi utilizzando l'azione [authenticate-user]. Finché il client non è autenticato, tutte le altre azioni sono vietate tranne [init-session];
  • riga 5: una volta autenticato, il client può eseguire una serie di calcoli fiscali utilizzando l'azione [calculate-tax];
  • riga 6: in qualsiasi momento, il client può richiedere di visualizzare l'elenco delle simulazioni che ha eseguito utilizzando l'azione [list-simulations];
  • riga 7: può eliminarne alcune utilizzando l'azione [delete-simulation];
  • riga 8: il cliente termina la propria sessione di simulazione utilizzando l'azione [end-session]. Da quel momento in poi, dovrà effettuare nuovamente l'accesso se desidera utilizzare l'applicazione;
  • riga 9: nell'applicazione HTML, l'azione [display-tax-calculation] visualizza il modulo utilizzato per calcolare l'imposta;

23.4. Configurazione dell'applicazione web

L'applicazione è configurata dal seguente file JSON [config.json]:


{
    "databaseFilename": "database.json",
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
    "relativeDependencies": [
 
        "/Entities/BaseEntity.php",
        "/Entities/Simulation.php",
        "/Entities/Database.php",
        "/Entities/TaxAdminData.php",
        "/Entities/ExceptionImpots.php",
 
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php",        
 
        "/Model/InterfaceServerDao.php",
        "/Model/ServerDao.php",
        "/Model/ServerDaoWithSession.php",
        "/Model/InterfaceServerMetier.php",
        "/Model/ServerMetier.php",
 
        "/Responses/InterfaceResponse.php",
        "/Responses/ParentResponse.php",
        "/Responses/JsonResponse.php",
        "/Responses/XmlResponse.php",
        "/Responses/HtmlResponse.php",
 
        "/Controllers/InterfaceController.php",
        "/Controllers/InitSessionController.php",
        "/Controllers/ListerSimulationsController.php",
        "/Controllers/AuthentifierUtilisateurController.php",
        "/Controllers/CalculerImpotController.php",
        "/Controllers/SupprimerSimulationController.php",
        "/Controllers/FinSessionController.php",
        "/Controllers/AfficherCalculImpotController.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php",
        "C:/myprograms/laragon-lite/www/vendor/predis/predis/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": "Logs/logs.txt",
    "actions":
            {
                "init-session": "\\InitSessionController",
                "authentifier-utilisateur": "\\AuthentifierUtilisateurController",
                "calculer-impot": "\\CalculerImpotController",
                "lister-simulations": "\\ListerSimulationsController",
                "supprimer-simulation": "\\SupprimerSimulationController",
                "fin-session": "\\FinSessionController",
                "afficher-calcul-impot": "\\AfficherCalculImpotController"
            },
    "types": {
        "json": "\\JsonResponse",
        "html": "\\HtmlResponse",
        "xml": "\\XmlResponse"
    },
    "vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
    "vue-erreurs": "vue-erreurs.php"
}

Commenti

  • riga 2: nome del file JSON contenente la configurazione di accesso al database;
  • righe 3–39: configurazione delle dipendenze del progetto. Qui sono elencati tutti gli script PHP presenti nella struttura di directory del progetto;
  • righe 40–44: l'utente autorizzato a utilizzare l'applicazione;
  • righe 46–54: l'indirizzo e-mail dell'amministratore dell'applicazione;
  • riga 55: il percorso del file di log;
  • righe 56–65: associazioni [azione => controller secondario responsabile della sua gestione];
  • righe 66–70: mappature [tipo di risposta => classe Response responsabile dell'invio della risposta al client];
  • righe 71–75: mappature [vista HTML => tabella dei codici di stato che portano a questa vista];
  • riga 76: la vista [error-view] viene visualizzata in una sessione HTML ogni volta che si verifica un errore anomalo:
    • Un'applicazione JSON o XML viene in genere interrogata utilizzando un client programmato. Questo client passa al server parametri che potrebbero essere mancanti o errati. I controller gestiscono questi casi e restituiscono codici di errore al client. Tutti i possibili casi di errore devono essere gestiti;
    • Con un'applicazione HTML, è leggermente diverso. In condizioni di utilizzo normale, l'applicazione web utilizza solo un sottoinsieme dei possibili casi d'uso per i client JSON e XML. Facciamo un esempio: l'azione [calculate-tax] si aspetta tre parametri inviati (tramite una richiesta POST): [married, children, salary].
      • Se abbiamo un client JSON che consente l'inserimento manuale degli URL, possiamo richiedere l'azione [calculate-tax] utilizzando una richiesta GET invece di una POST, oppure con una richiesta POST che non contiene parametri quando ne sono richiesti tre, ecc. Il server JSON deve gestire tutti questi casi;
      • Con un'applicazione web, l'azione [calculate-tax] verrà richiesta tramite un modulo web in cui nessuno dei due casi precedenti è possibile: l'azione [calculate-tax] verrà richiesta tramite una richiesta POST con tutti e tre i parametri [married, children, salary]. Alcuni di questi parametri potrebbero avere un valore errato, ma saranno presenti. Tuttavia, l'utente può riprodurre determinati errori digitando lui stesso gli URL nel browser. Per motivi di sicurezza, dobbiamo gestire questo caso;
      • la [vista-errore] verrà visualizzata ogni volta che un controller secondario restituisce un codice di stato incompatibile con l'applicazione web, ovvero un codice di stato non elencato nelle righe 72–74 del file di configurazione. Abbiamo optato per questa soluzione a scopo didattico. Un'altra opzione possibile sarebbe quella di non fare nulla e semplicemente visualizzare nuovamente la vista attualmente mostrata nel browser del client, in modo che l'utente abbia l'impressione che il server non stia rispondendo ai suoi URL creati manualmente;

23.5. Installazione di strumenti e librerie

23.5.1. Postman

[Postman] è lo strumento che ci permetterà di interrogare i vari URL della nostra applicazione web. Ci permette di:

  • utilizzare qualsiasi URL: questi sono creati manualmente;
  • interrogare il server web utilizzando GET, POST, PUT, OPTIONS, ecc.;
  • specificare i parametri GET o POST;
  • impostare le intestazioni HTTP per la richiesta;
  • ricevere una risposta in formato JSON, XML o HTML;
  • accedere alle intestazioni HTTP della risposta. Questo ci permette di accedere alla risposta HTTP completa del server;

Dato che stiamo costruendo manualmente gli URL da interrogare, potremo testare tutti i possibili scenari di errore e vedere come reagisce il server.

[Postman] è disponibile all'indirizzo [https://www.getpostman.com/downloads/]. La versione disponibile a giugno 2019 è la 7.2. Questa versione presenta un bug: quando si effettuano richieste successive al server web, il client [Postman 7.2] non restituisce automaticamente i cookie inviati dal server, in particolare il cookie di sessione. Per mantenere la sessione, è necessario copiare manualmente il cookie di sessione nelle intestazioni HTTP delle richieste successive. Non è molto complicato, ma non è pratico. Si tratta di un bug che non era presente nelle versioni precedenti. Consapevole del bug, il team di [Postman] lo ha risolto in una versione alpha (che potrebbe essere instabile) chiamata [Postman Canary], disponibile all'URL [https://www.getpostman.com/downloads/canary]. Questa è la versione utilizzata qui. Descriveremo come installarla. Se è disponibile una versione stabile [Postman 7.3] o successiva, è possibile scaricarla: il bug sarà probabilmente stato risolto.

Procedete con l’installazione della vostra versione di [Postman]. Durante l’installazione, vi verrà chiesto di creare un account: qui non sarà necessario. L’account [Postman] serve a sincronizzare diversi dispositivi in modo che la configurazione di uno venga replicata su un altro. Qui non serve nulla di tutto ciò.

Una volta installato, [Postman] mostra la seguente interfaccia:

Image

  • in [2-3], puoi accedere alle impostazioni del prodotto;

Image

  • in [6], la versione utilizzata in questo documento;
  • se hai creato un account, avviene la sincronizzazione tra il tuo computer e un server [Postman] remoto. Ciò è indicato dalla rotellina che gira [7] che appare ogni volta che apporti modifiche al progetto [Postman]. Per interrompere questa sincronizzazione non necessaria, esci tramite [8-9];

23.5.2. La libreria Symfony / Serializer

Per serializzare gli oggetti in JSON e XML, useremo la libreria [Symfony / Serializer]. In questo caso offre due vantaggi:

  • è coerente nel suo utilizzo per la serializzazione in JSON o XML: questo evita di dover imparare due librerie con API (Application Programming Interface) diverse;
  • in modo nativo, può serializzare oggetti in JSON o XML, anche se i loro attributi sono privati. Ricordiamo che in JSON, per serializzare un oggetto, la sua classe doveva implementare l’interfaccia [\JsonSerializable]. Il risultato ottenuto era una stringa JSON di un array associativo con gli attributi della classe come chiavi. Durante la deserializzazione di questa stringa JSON, si recuperava l’array associativo primitivo, che doveva poi essere convertito in un oggetto della classe che era stata serializzata. Con [Symfony / Serializer], la deserializzazione produce immediatamente un oggetto della classe serializzata. È più semplice;

La documentazione della libreria [Symfony / Serializer] è disponibile all'URL: [https://symfony.com/doc/current/components/serializer.html] (giugno 2019).

Per installare questa libreria, apri un terminale Laragon (vedi sezione link) e digita il seguente comando:

Image

  • in [1], il comando per installare la libreria [symfony/serializer];
  • in [2], un'altra libreria necessaria per il nostro progetto: abilita la serializzazione degli oggetti;

Image

23.6. Entità dell'applicazione

Image

Le entità [BaseEntity, Database, ExceptionImports, TaxAdminData] sono state utilizzate a partire dalla versione 08 del servizio web (vedere la sezione dei link).

La classe [Simulation] verrà utilizzata per incapsulare gli elementi di una simulazione di calcolo fiscale:


<?php
 
namespace Application;
 
class Simulation extends BaseEntity {
  // attributes of a tax calculation simulation
  protected $marié;
  protected $enfants;
  protected $salaire;
  protected $impôt;
  protected $surcôte;
  protected $décôte;
  protected $réduction;
  protected $taux;
 
  // getters
  public function getMarié() {
    return $this->marié;
  }
 
  public function getEnfants() {
    return $this->enfants;
  }
 
  public function getSalaire() {
    return $this->salaire;
  }
 
  public function getImpôt() {
    return $this->impôt;
  }
 
  public function getSurcôte() {
    return $this->surcôte;
  }
 
  public function getDécôte() {
    return $this->décôte;
  }
 
  public function getRéduction() {
    return $this->réduction;
  }
 
  public function getTaux() {
    return $this->taux;
  }
 
}

Commenti

  • Riga 5: La classe [Simulation] estende la classe [BaseEntity] e quindi eredita i seguenti metodi:
    • [setFromArrayOfAttributes($arrayOfAttributes)]: che consente di inizializzare gli attributi della classe;
    • [__toString]: che restituisce la stringa JSON dell'oggetto;
  • righe 7–14: gli attributi della simulazione;
  • righe 16–47: i getter della classe;

23.7. Utilità dell'applicazione

Image

La classe [Logger] consente di registrare gli eventi in un file di testo. Questa classe è descritta nella sezione collegata.

La classe [SendAdminMail] consente di inviare un'e-mail all'amministratore dell'applicazione. Questa classe è descritta nella sezione collegata.

23.8. I livelli [business] e [DAO]

Image

Image

Le classi e le interfacce dei livelli [business] e [DAO] sono raggruppate nella cartella [Model]. Sono state tutte definite e utilizzate nelle versioni precedenti:

ExceptionImports
La classe per le eccezioni generate dal livello [DAO]. Definita nella sezione collegata.
InterfaceServerDao
Interfaccia implementata dal livello [dao] del server. Definita nella sezione collegata.
ServerDao
Implementazione dell'interfaccia [InterfaceServerDao]. Implementa il livello [dao] del server. Definita nella sezione link.
ServerDaoWithSession
Implementazione dell'interfaccia [InterfaceServerDao]. Implementa il livello [dao] del server. Definita nella sezione "link".
InterfaceServerMetier
Interfaccia implementata dal livello [business] del server. Definita nella sezione link.
ServerBusiness
Implementazione dell'interfaccia [InterfaceMetier]. Implementa il livello [business] del server. Definito nella sezione "link".

L'applicazione attualmente in fase di sviluppo fa ampio uso di elementi già presentati e utilizzati:

  • i livelli [business] e [DAO];
  • le utility [Logger] e [SendAdminMail];
  • le entità [ExceptionImpots, TaxAdminData, Database];

Ci concentreremo sul livello [web] dell'applicazione:

Image

23.9. Il controller principale [main.php]

23.9.1. Introduzione

Image

  • [1-2]: Il controller principale [main.php] [1] è configurato dal file [config.json] [2];

Esaminiamo la posizione del controller principale nella nostra architettura MVC:

Image

In [1], il controller principale [main.php] è il primo elemento dell'architettura MVC a elaborare la richiesta del client. Ha diversi ruoli:

  • In primo luogo, esegue dei controlli di base:
    • il file di configurazione esiste ed è valido;
    • carica tutte le dipendenze del progetto. Ciò equivale a caricare tutti gli elementi dell'architettura MVC;
    • L'azione richiesta è stata specificata? Se sì, è valida?
    • Se l'azione richiesta è valida, seleziona [2a] il controller secondario che la elaborerà e gli passa le informazioni necessarie: la richiesta HTTP, la sessione e la configurazione dell'applicazione;
    • Recupera [2c] la risposta dal controller secondario. A seconda del tipo di applicazione (JSON, XML, HTML) richiesta dal client, seleziona [3a] la risposta (JsonResponse, XmlResponse, HtmlResponse) responsabile dell'invio della risposta al client e le passa tutte le informazioni di cui ha bisogno (la richiesta HTTP, la sessione, la configurazione dell'applicazione, la risposta dal controller secondario);
    • una volta inviata questa risposta [3c], liberare tutte le risorse che potrebbero essere state allocate per l'elaborazione della richiesta;

23.9.2. [main.php] - 1

Il codice per il controller principale [main.php] è il seguente:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
 
// error handling by PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// we retrieve the configuration
$configFilename = "config.json";
$fileContents = \file_get_contents($configFilename);
$erreur = FALSE;
// mistake?
if (!$fileContents) {
  // we note the error
  $état = 131;
  $erreur = TRUE;
  $message = "Le fichier de configuration [$configFilename] n'existe pas";
}
if (!$erreur) {
  // retrieve the JSON code from the configuration file in an associative array
  $config = \json_decode($fileContents, true);
  // mistake?
  if (!$config) {
    // we note the error
    $erreur = TRUE;
    $état = 132;
    $message = "Le fichier de configuration [$configFilename] n'a pu être exploité correctement";
  }
}
// mistake?
if ($erreur) {
  // preparation of JSON server response
  // you can't use the configuration file
  // symfony dependencies
  require_once "C:/myprograms/laragon-lite/www/vendor/autoload.php";
  // response preparation
  $response = new Response();
  $response->headers->set("content-type", "application/json");
  $response->setCharset("utf-8");
  // status code
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  // content
  $response->setContent(json_encode(["action" => "", "état" => $état, "réponse" => $message], JSON_UNESCAPED_UNICODE));
  // shipping
  $response->send();
  // end
  exit;
}

Commenti

  • righe 10–12: il controller principale utilizza i seguenti oggetti Symfony:
    • [Request]: la richiesta HTTP attualmente in elaborazione;
    • [Session]: la sessione dell'applicazione web;
    • [Response]: la risposta HTTP al client;
  • riga 15: durante lo sviluppo, questa riga rimarrà commentata: gli errori PHP vengono quindi inclusi nel flusso di testo inviato al client. Se il client è un browser, ciò consente all'utente di vedere gli errori riscontrati dal server. Questo aiuta nel debug;
  • riga 16: vengono segnalati tutti gli errori (E_ALL) tranne gli avvisi (! E_WARNING) e le notifiche non gravi (! E_NOTICE). Ad esempio, se un file non può essere aperto, PHP genera un errore [E_NOTICE]. Se la riga 15 abilita la visualizzazione degli errori, l'errore di apertura del file appare nel browser del client. Questo va bene se avete dimenticato di testare il risultato dell'apertura del file, ma meno se avevate intenzione di testarlo: una riga [notice] ingombra quindi la risposta del server al client. Durante lo sviluppo, anche la riga 16 dovrebbe essere commentata: non volete perdere nessun errore;
  • riga 19: viene letto il file di configurazione;
  • righe 22–27: se questa operazione di lettura fallisce, l'errore viene registrato (riga 25), l'applicazione viene impostata sullo stato [131] e viene preparato un messaggio di errore;
  • riga 30: la stringa JSON dal file di configurazione viene decodificata;
  • righe 32–37: se la decodifica fallisce, l'errore viene registrato (riga 34), l'applicazione viene impostata sullo stato [132] e viene preparato un messaggio di errore;
  • righe 40–57: se si verifica un errore durante la lettura del file di configurazione, non è possibile procedere. Si prepara quindi una risposta JSON per il client:
  • riga 44: poiché il file di configurazione non è stato letto, il file [autoload] richiesto da [Symfony] deve essere importato manualmente;
  • righe 46–47: viene preparata una risposta JSON;
  • riga 50: il codice di stato HTTP della risposta sarà 500 INTERNAL_SERVER_ERROR;
  • riga 52: impostiamo il contenuto JSON della risposta. Tutte le risposte generate dall'applicazione web in esame avranno tre chiavi:
      • [action]: l'azione richiesta dal client;
      • [status]: lo stato dell'applicazione dopo l'esecuzione di questa azione;
      • [response]: la risposta del server web;
  • riga 54: la risposta JSON viene inviata al client;

23.9.3. [Postman] Test - 1

Verificheremo il comportamento del server quando il file di configurazione è mancante o errato:

Image

Organizzeremo in raccolte le varie richieste che il nostro client [Postman] invierà al server fiscale.

  • In [1], crea una nuova raccolta;
  • In [2], assegnagli un nome;
  • In [3], la descrizione è facoltativa;

Image

  • Nelle raccolte [4], ora compare una raccolta denominata [impots-server-tests-version12] [5];
  • In [6], è possibile aggiungere una nuova richiesta alla raccolta;

Image

  • In [7], viene assegnato un nome alla query;
  • in [8], la descrizione è facoltativa;

Image

  • in [9-11], la richiesta viene aggiunta alla raccolta;
  • in [12], selezionare il tipo di richiesta; in questo caso, una richiesta [GET]. In [19], i diversi tipi di richiesta disponibili;
  • in [13], inserisci qui l'URL del server;
  • in [14], inserisci qui i parametri aggiunti all'URL; questi saranno parametri GET. Il vantaggio di inserirli qui piuttosto che direttamente nell'URL è che [Postman] li codificherà come URL. Se li inserisci direttamente nell'URL, dovrai codificarli come URL tu stesso;
  • in [15], [Authorization] viene utilizzato per definire l'utente che effettuerà l'accesso. Non avremo bisogno di utilizzare questa opzione;
  • in [16], le intestazioni HTTP che accompagneranno la richiesta. Alcune intestazioni sono incluse automaticamente nella richiesta. È possibile aggiungerne di nuove qui;
  • In [17], [Body] si riferisce ai parametri di un'operazione [POST]. Dovremo utilizzare questa opzione;

Eseguiremo il seguente test:

  • in [main.php], specifichiamo che il file di configurazione è [config2.json], che non esiste:

Image

  • La riga 16 del codice deve essere rimossa dal commento;
  • Riga 18: l'errore relativo al nome del file di configurazione;

Apriamo [Postman] [13, 20], inseriamo l'URL del server web di calcolo delle imposte ed eseguiamo l'operazione [21]:

Image

La risposta restituita dal server (Laragon deve, ovviamente, essere in esecuzione) è la seguente:

Image

  • in [22], il server ha restituito un codice HTTP [500 Internal Server Error];
  • in [23], [Body] si riferisce al corpo della risposta, ovvero al documento inviato dal server dietro le intestazioni HTTP [28];
  • in [26], vediamo che [Postman] ha ricevuto una risposta JSON;
  • in [27], la risposta JSON formattata;
  • in [28], la risposta JSON grezza senza formattazione;
  • in [29], la modalità [Preview] viene utilizzata quando la risposta è in formato HTML. La modalità [Preview] visualizza quindi la pagina ricevuta;
  • In [30], la risposta JSON dal server. Questa è effettivamente quella che ci aspettavamo;

In [25], le intestazioni HTTP inviate nella risposta del server sono le seguenti:

Image

  • in [32], il tipo JSON della risposta;

Questo test iniziale ci ha permesso di constatare che:

  • possiamo inviare qualsiasi tipo di richiesta al server testato;
  • possiamo impostare i parametri GET o POST;
  • abbiamo a disposizione l'intera risposta: le intestazioni HTTP e il documento che segue tali intestazioni [Body];

Ora, eseguiamo un secondo test:

Image

  • in [1-3], il file [config3.json] è un file JSON sintatticamente errato;
  • In [4], [main.php] è configurato per utilizzare [config3.json];

Aggiungiamo una nuova richiesta in [Postman]:

Image

  • [1-3], clicchiamo con il tasto destro su [2] e selezioniamo l'opzione [duplica] per duplicare la richiesta [2];
  • In [4], la nuova richiesta ha un nome predefinito, che modifichiamo in [5];

Image

  • In [6], la richiesta rinominata;
  • In [9-10], inviamo la stessa richiesta GET di prima;

Image

  • in [11], la risposta JSON del server;

Qui abbiamo mostrato come verranno testate le varie azioni del servizio web di calcolo delle imposte.

23.9.4. [main.php] – 2

Riprendiamo l'analisi del codice del controller principale [main.php]:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
 
// error handling by PHP
//ini_set("display_errors", "0");
error_reporting(E_ALL && !E_WARNING && !E_NOTICE);
// we retrieve the configuration
$configFilename = "config.json";

// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require_once "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require_once "$dependency";
}
 
// log file creation
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // log file could not be created - internal server error
  $état = 133;
  (new JsonResponse())->send(
    NULL, NULL, $config,
    Response::HTTP_INTERNAL_SERVER_ERROR,
    ["action" => "non déterminée", "état" => $état, "réponse" => "Le fichier de logs [{$config['logsFilename']}] n'a pu être créé"],
    []);
  // completed
  exit;
}

Commenti

  • riga 18: ora abbiamo un file di configurazione [config.json] che esiste ed è sintatticamente corretto. Dovremmo anche verificare che le chiavi previste siano presenti in questo file. Supponiamo che questo faccia parte del normale processo di debug dello sviluppatore. Avremmo potuto applicare lo stesso ragionamento ai due errori precedenti;
  • righe 20–28: includiamo tutte le dipendenze necessarie per il progetto web. Abbiamo già incontrato questo codice diverse volte;
  • righe 31–43: proviamo a creare l'oggetto [Logger], che ci permetterà di registrare gli eventi nel file [$config['logsFilename']]. Questa creazione potrebbe fallire;
  • righe 33–43: gestione dell'errore durante la creazione dell'oggetto [Logger];
  • riga 35: impostiamo un numero di stato;
  • righe 36–40: inviamo una risposta JSON;
  • riga 42: interrompiamo lo script;

Tutte le risposte inviate al client implementano la seguente interfaccia [InterfaceResponse]:

Image

Il codice per l'interfaccia [InterfaceResponse] è il seguente:


<?php

namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
interface InterfaceResponse {
 
  // Request $request : requête en cours de traitement
  // Session $session: the web application session
  // array $config: application configuration
  // int statusCode: HTTP response status code
  // array $content: server response
  // array $headers: HTTP headers to be added to the response
  // Logger $logger: the logger for writing logs
  
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void;
}
  • righe 19–27: l'interfaccia [InterfaceResponse] ha un unico metodo [send] per inviare la risposta al client;
  • righe 11–17: il significato dei vari parametri del metodo [send];
  • righe 23–25: i parametri [$statusCode, $content, $headers] fanno parte dell'output standard dei controller secondari dell'applicazione. Tuttavia, la risposta potrebbe richiedere ulteriori informazioni. Pertanto, le forniamo i primi tre parametri (righe 20–22), che le consentono di accedere a tutte le informazioni relative alla richiesta, alla sessione e alla configurazione;
  • riga 26: la risposta richiede il [Logger] perché registrerà la risposta inviata al client;

La classe [JsonResponse] implementa l'interfaccia [InterfaceResponse] come segue:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
class JsonResponse extends ParentResponse implements InterfaceResponse {
 
  // Request $request : requête en cours de traitement
  // Session $session: the web application session
  // array $config: application configuration
  // int statusCode: HTTP response status code
  // array $content: server response
  // array $headers: HTTP headers to be added to the response
  // Logger $logger: the logger for writing logs
 
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void {
 
    // symfony serializer preparation
    $serializer = new Serializer(
      [
      // required for object serialization
      new ObjectNormalizer()],
      // encoder jSON
      // for options, make OU between the different options
      [new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
    );
    // serialization jSON
    $json = $serializer->serialize($content, 'json');
    // headers
    $headers = array_merge($headers, ["content-type" => "application/json"]);
    // sending reply
    parent::sendResponse($statusCode, $json, $headers);
    // log
    if ($logger !== NULL) {
      $logger->write("réponse=$json\n");
    }
  }
 
}

Commenti

  • riga 13: la classe implementa l'interfaccia [InterfaceResponse];
  • riga 13: la classe estende la classe [ParentResponse]. Tutti i tipi [Response] estendono questa classe. È questa classe padre che invia la risposta al client (riga 46). Poiché questo codice era comune a tutti i tipi [Response], è stato estrapolato in una classe padre;
  • righe 33–40: istanziazione del serializzatore [Symfony], che convertirà la risposta del server [$content] in una stringa JSON (riga 42);
  • righe 34–36: il primo parametro del costruttore [Serializer] è un array. Al suo interno inseriamo un'istanza della classe [ObjectNormalizer] necessaria per la serializzazione degli oggetti. In questa applicazione, ciò avviene con un elenco di simulazioni in cui ogni simulazione è un'istanza della classe [Simulation];
  • Riga 39: anche il secondo parametro del costruttore [Serializer] è un array: contiene tutti gli encoder utilizzati in una serializzazione (XML, JSON, CSV, ecc.);
  • riga 39: qui ci sarà un solo encoder, di tipo [JsonEncoder]. Il costruttore senza parametri sarebbe stato sufficiente. In questo caso, abbiamo passato un parametro [JsonEncode] al costruttore, esclusivamente per specificare le opzioni di codifica JSON;
  • riga 39: il parametro del costruttore [JsonEncode] è un array di opzioni. Qui usiamo l'opzione [JSON_UNESCAPED_UNICODE] per richiedere che i caratteri UTF-8 nella stringa JSON vengano renderizzati in modo nativo anziché "escapati";
  • riga 42: il corpo della risposta HTTP viene serializzato in JSON utilizzando il serializzatore precedente;
  • riga 44: aggiungiamo l'intestazione HTTP che comunica al client che stiamo inviando JSON;
  • riga 46: alla classe padre viene chiesto di inviare la risposta al client;
  • Righe 48–50: registriamo la risposta JSON;

Il codice per la classe padre [ParentResponse] è il seguente:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Response;
 
class ParentResponse {
 
  // int $statusCode: HTTP response status code
  // string $content: the body of the response to be sent
  // depending on the case, this is a jSON, XML, HTML string
  // array $headers: HTTP headers to be added to the response
 
  public function sendResponse(
    int $statusCode,
    string $content,
    array $headers): void {
 
    // preparing the server's text response
    $response = new Response();
    $response->setCharset("utf-8");
    // status code
    $response->setStatusCode($statusCode);
    // headers
    foreach ($headers as $text => $value) {
      $response->headers->set($text, $value);
    }
    // we send the answer
    $response->setContent($content);
    $response->send();
  }
}

Commenti

  • righe 10–13: significato dei tre parametri del metodo [send];
  • riga 17: si noti che il corpo della risposta è di tipo [string] e quindi pronto per essere inviato (riga 30);
  • riga 22: la risposta conterrà caratteri UTF-8;
  • riga 24: codice di stato HTTP della risposta;
  • righe 26–28: aggiunta delle intestazioni HTTP fornite dal codice chiamante;
  • righe 30–31: invio della risposta al client;

Abbiamo descritto in dettaglio l'intero ciclo di vita di una risposta JSON. Non torneremo su questo argomento in seguito. È sufficiente ricordare la firma dell'interfaccia [InterfaceResponse]:


interface InterfaceResponse {
 
  // Request $request : requête en cours de traitement
  // Session $session: the web application session
  // array $config: application configuration
  // int statusCode: HTTP response status code
  // array $content: server response
  // array $headers: HTTP headers to be added to the response
  // Logger $logger: the logger for writing logs
  
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void;
}

Il controller principale [main.php] deve rispettare questa firma ogni volta che richiede l'invio di una risposta al client.

23.9.5. Test [Postman] – 2

Modifichiamo il file [config.json] come segue:

Image

  • in [1], specifichiamo che il file di log è [Logs], che è una cartella [2]. La creazione del file [Logs] dovrebbe quindi fallire;

Creiamo una nuova richiesta [Postman] [3], denominata [error-133]:

Image

  • [2-4]: Definiamo la stessa richiesta dei due test precedenti;
  • [5-7]: Recuperiamo con successo la risposta JSON prevista;

23.9.6. [main.php] – 3

Continuiamo l'analisi del controller principale [main.php]:


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

// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
 
// error handling by PHP

 
// log file creation

 
// 1st log
$logger->write("\n---nouvelle requête\n");
// current query
$request = Request::createFromGlobals();
 
// session
$session = new Session();
$session->start();
// error list
$erreurs = [];
$erreur = FALSE;
// we manage the requested action
if (!$request->query->has("action")) {
  $erreurs[] = "paramètre [action] manquant";
  $erreur = TRUE;
  $état = 101;
  $action = "";
} else {
  // memorize the action
  $action = strtolower($request->query->get("action"));
}
// we log the action
$logger->write("action [$action] demandée\n");
 
// does the action exist?
if (!$erreur && !array_key_exists($action, $config["actions"])) {
  $erreurs[] = "action [$action] invalide";
  $erreur = TRUE;
  $état = 102;
}
 
// the session type must be known before performing certain actions
if (!$erreur && !$session->has("type") && $action !== "init-session") {
  $erreurs[] = "pas de session en cours. Commencer par action [init-session]";
  $erreur = TRUE;
  $état = 103;
}
 
// some actions require authentication
if (!$erreur && !$session->has("user") && $action !== "authentifier-utilisateur" && $action !== "init-session") {
  $erreurs[] = "action demandée par utilisateur non authentifié";
  $erreur = TRUE;
  $état = 104;
}
 
// mistakes?
if ($erreurs) {
  // we prepare the answer without sending it  
  $statusCode = Response::HTTP_BAD_REQUEST;
  $content = ["réponse" => $erreurs];
  $headers = [];
} else {
  // ---------------------------
  // execute the action using its controller
  $controller = __NAMESPACE__ . $config["actions"][$action];
  $logger->write("contrôleur : $controller\n");
  list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
 
// --------------------- we send the answer
// cas de l'erreur fatale HTTP_INTERNAL_SERVER_ERROR
// send an e-mail to the administrator if you can
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
  $infosMail = $config['adminMail'];
  $infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
  $sendAdminMail = new SendAdminMail($infosMail, $logger);
  $sendAdminMail->send();
}
// the answer depends on the session type
if ($session->has("type")) {
  // the session type is in the session
  $type = $session->get("type");
} else {
  // if no type in session, then the default response is jSON
  $type = "json";
}
// we add the keys [action, state] to the controller response
$content = ["action" => $action, "état" => $état] + $content;
// instantiate the [Response] object responsible for sending the response to the client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
 
// the reply has been sent - resources are released
$logger->close();
exit;

Commenti

  • Una volta eseguiti i controlli iniziali e appurato che può procedere, il controller principale si concentra sull'azione richiesta: deve soddisfare determinate condizioni;
  • riga 21: registriamo il fatto che abbiamo una nuova richiesta. Non potevamo farlo prima perché non eravamo sicuri di avere un file di log valido;
  • riga 23: incapsuliamo tutte le informazioni della richiesta del client nell'oggetto Symfony [Request];
  • riga 26: avviamo una nuova sessione o recuperiamo quella esistente, se presente;
  • riga 27: la sessione viene attivata;
  • riga 29: un array di messaggi di errore;
  • riga 30: un valore booleano che ci indica, durante l'esecuzione dei test, se si è verificato o meno un errore;
  • Riga 32: il parametro [action] deve essere incluso nell'URL nella forma [main.php?action=someAction]. Il parametro [action] viene quindi incluso nei parametri [$request→query];
  • righe 33–36: caso in cui il parametro [action] è assente dall'URL. L'errore viene registrato e gli viene assegnato un codice di stato [101];
  • riga 39: se il parametro [action] è presente nell'URL, viene memorizzato;
  • riga 42: il tipo di azione viene registrato;
  • righe 45–49: se il parametro [action] è presente, deve essere valido. Tutte le azioni autorizzate sono definite nell'array associativo [$config["actions"]];
  • righe 46–48: se l'azione non è valida, l'errore viene registrato e gli viene assegnato lo stato [102];
  • righe 52–56: abbiamo un'azione valida. Deve comunque soddisfare altre condizioni. L'applicazione web fornisce tre tipi di risposta (JSON, XML, HTML). Questo tipo è impostato dall'azione [init-session]. Questa azione inserisce il tipo di sessione nella chiave [type];
  • riga 52: al di fuori dell'azione [init-session], qualsiasi altra azione deve avvenire con una chiave [type] nella sessione;
  • righe 53–55: se così non fosse, l'errore viene registrato e gli viene assegnato lo stato [103];
  • righe 58–63: al di fuori delle azioni [init-session] e [authenticate-user], tutte le altre azioni devono avvenire dopo l'autenticazione. Ciò avviene utilizzando l'azione [authenticate-user] che, se l'autenticazione ha esito positivo, inserisce una chiave [user] nella sessione;
  • riga 59: se l'azione non è né [init-session] [authenticate-user] e la chiave [user] non è presente nella sessione, si verifica un errore;
  • righe 60–62: l'errore viene registrato e gli viene assegnato lo stato [104];
  • righe 66–71: si verifica se l'array [$errors] è non vuoto. In tal caso, l'azione richiesta o il suo contesto di esecuzione non sono corretti;
  • righe 68–70: si prepara la risposta da inviare al client, ma non la si invia ancora;
  • riga 68: codice di stato HTTP;
  • riga 69: corpo della risposta;
  • riga 70: intestazioni da aggiungere alla risposta; nessuna in questo caso;
  • riga 73: abbiamo un'azione valida. Chiederemo al suo controller (secondario) di elaborarla;
  • riga 74: costruiamo il nome della classe del controller da eseguire. [__NAMESPACE__] è lo spazio dei nomi in cui ci troviamo, qui [Application] (riga 7);
  • i nomi delle classi dei controller secondari si trovano nel file [config.json]:

"actions":
            {
                "init-session": "\\InitSessionController",
                "authentifier-utilisateur": "\\AuthentifierUtilisateurController",
                "calculer-impot": "\\CalculerImpotController",
                "lister-simulations": "\\ListerSimulationsController",
                "supprimer-simulation": "\\SupprimerSimulationController",
                "fin-session": "\\FinSessionController",
                "afficher-calcul-impot": "\\AfficherCalculImpotController"
            },

Ogni azione corrisponde a un controller secondario. Se l'azione è [authenticate-user], la variabile [$controller] alla riga 74 avrà quindi il valore [Application/AuthentifierUtilisateurController];

  • riga 75: registriamo il nome del controller secondario per la verifica durante lo sviluppo;
  • riga 76: il controller secondario viene eseguito. Torneremo sui controller secondari più avanti;
  • riga 76: tutti i controller secondari restituiscono lo stesso tipo di risultato, ovvero un array:
    • il primo elemento dell'array [$statusCode] è il codice di stato HTTP della risposta da inviare;
    • il secondo elemento [$state] è lo stato dell'applicazione dopo l'esecuzione del controller;
    • il terzo elemento [$content] è un array associativo con una singola chiave [response], che è il corpo della risposta da inviare al client;
    • il quarto elemento [$headers] è un array di intestazioni HTTP da aggiungere alla risposta inviata al client;
  • riga 79: arriviamo qui:
    • o perché si è verificato un errore (righe 68–70);
    • oppure dopo l'esecuzione di un controller (righe 72–76);
    • in entrambi i casi, gli elementi [$statusCode, $status, $content, $headers] necessari per costruire la risposta al client sono noti;
  • righe 82–87: gestiscono il caso specifico del codice di stato [500 Internal Server Error]. Se un controller ha impostato questo codice di stato, significa che l'applicazione non può funzionare. Questo è il caso, ad esempio, dei calcoli fiscali se il DBMS utilizzato non è stato avviato o non risponde più. Viene quindi inviata un'e-mail all'amministratore dell'applicazione per avvisarlo. Non commenteremo specificamente questo codice. L'uso della classe [SendAdminMail] è già stato presentato (vedi sezione collegata);
  • righe 89–95: Determiniamo il tipo [jSON, XML, HTML] dell'applicazione web. Se l'azione [init-session] è stata eseguita con successo, questo tipo si trova nella sessione associata alla chiave [type] (riga 91). In caso contrario, impostiamo arbitrariamente un tipo per la risposta, ovvero il tipo JSON (riga 94);
  • riga 97: [$content] è un array con una singola chiave [response] e un singolo valore, il corpo della risposta da inviare al client. Vi vengono aggiunte le chiavi [action] e [status]. La chiave [action] renderà più facile tracciare i log nel file [logs.txt]. La chiave [status] avrà due scopi:
    • permetterà ai client JSON e XML di conoscere lo stato in cui l'azione eseguita ha portato l'applicazione web;
    • nel caso di una risposta HTML, ci permetterà di scegliere la vista HTML da inviare al browser del cliente;
  • riga 99: selezioniamo il tipo di classe [Response] da eseguire per inviare la risposta al client;

Abbiamo già introdotto la classe [JsonResponse] nella sezione precedente. Essa implementa l'interfaccia [InterfaceResponse] ed estende la classe [ParentResponse]. Questo vale anche per le altre due classi, [XmlResponse] e [HtmlResponse].

Le risposte sono raggruppate nella cartella [Responses]:

Image

Tutte queste classi implementano l'interfaccia [InterfaceResponse], che è presentata anche nella sezione collegata:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
interface InterfaceResponse {
 
  // Request $request : requête en cours de traitement
  // Session $session: the web application session
  // array $config: application configuration
  // int statusCode: HTTP response status code
  // array $content: server response
  // array $headers: HTTP headers to be added to the response
  // Logger $logger: the logger for writing logs
  
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void;
}

Questa interfaccia ha un unico metodo, [send], responsabile dell'invio della risposta al client. Questo metodo ha i 7 parametri descritti nelle righe 11–17. Tutte le classi e le interfacce nella cartella [Responses] si trovano nello spazio dei nomi [Application] (riga 3).

Torniamo al codice in [main.php]:



// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
 
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
  • Riga 5: Istanziamo la classe [Response] corrispondente al tipo di applicazione. Queste classi sono definite nel file [config.json] come segue:

"types": {
        "json": "\\JsonResponse",
        "html": "\\HtmlResponse",
        "xml": "\\XmlResponse"
    },
  • riga 5: al nome della classe viene anteposto il suo namespace;
  • riga 6: la classe [Response] viene istanziata e il suo metodo [send] viene chiamato con i 7 parametri previsti. Questi parametri sono quelli dell'interfaccia [InterfaceResponse] che tutte le classi di risposta implementano. Questo invia la risposta al client;
  • riga 9: il file di log viene chiuso;
  • riga 10: il controller principale ha terminato il suo lavoro;

23.9.7. Test [Postman] – 3

Testeremo vari casi di errore per il parametro [action] dell'URL.

Image

  • in [1]:
    • [error-101]: caso in cui il parametro [action] manca dall'URL;
    • [error-102]: caso in cui il parametro [action] è presente nell'URL ma non viene riconosciuto;
    • [errore-103]: caso in cui il parametro [action] è presente nell'URL e viene riconosciuto, ma il tipo di risposta previsto [json, xml, html] non è stato definito;

Ogni richiesta viene eseguita. Presentiamo direttamente i risultati:

Sopra:

  • nei punti [2-4], una richiesta senza il parametro [action] nell'URL [4];
  • in [5-7], il risultato JSON;

Image

Sopra:

  • in [5-9], una richiesta con un parametro [action] non valido;
  • in [10-13], la risposta JSON;

Image

Sopra:

  • in [14-19], un'azione riconosciuta ma il tipo (json, xml, html) non è stato ancora specificato;
  • in [20-23], la risposta JSON del server;

23.10. Controller secondari

Ogni azione viene eseguita da uno dei controller presenti nella cartella [Controllers]:

Image

Image

Nell'architettura generale dell'applicazione sopra riportata, i controller secondari si trovano in [2a].

Ciascun controller implementa la seguente interfaccia [InterfaceController]:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
interface InterfaceController {
 
  // $config is the application configuration
  // traitement d'une requête Request
  // session and can modify it
  // $infos is additional information specific to each controller
  
  // renders an array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos=NULL): array;
}

Commenti

  • Tutti i controller secondari vengono eseguiti tramite il metodo [execute] alla riga 17. Passiamo le informazioni note dal controller principale a questo metodo:
    • riga 18: [array $config], che incapsula la configurazione dell'applicazione;
    • riga 19: [Request $request], che è la richiesta HTTP attualmente in elaborazione;
    • riga 20: [Session $session], che è la sessione corrente dell'applicazione web;
    • riga 21: [array $infos=NULL], che è un array aggiuntivo di informazioni per il controller nel caso in cui i primi tre parametri del metodo non fossero sufficienti. In questa applicazione, questo parametro non è mai stato utilizzato. È incluso a titolo precauzionale;
  • riga 21: il metodo [execute] restituisce l'array [$statusCode, $status, $content, $headers]
    • [int $statusCode]: il codice di stato della risposta HTTP;
    • [int $state]: lo stato dell'applicazione al termine dell'esecuzione;
    • [array $content]: un array associativo [response=>result] dove [result] è di qualsiasi tipo: questo è il risultato prodotto dal controller e verrà inviato al client una volta serializzato come stringa;
    • [array $headers]: l'elenco delle intestazioni HTTP da includere nella risposta HTTP del server;

Ogni controller secondario viene chiamato dal seguente codice nel controller principale:


 // on exécute l'action à l'aide de son contrôleur
 $controller = __NAMESPACE__ . $config["actions"][$action];
 list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);

Alla riga 3, vediamo che il quarto parametro [array $infos=NULL] del metodo [execute] non viene utilizzato.

23.11. Azioni

Esamineremo ora le varie azioni possibili del servizio web:

Azione
Ruolo
Contesto di esecuzione
init-session
Utilizzato per impostare il tipo (json, xml, html) delle risposte desiderate
Richiesta GET main.php?action=init-session&type=x
può essere inviata in qualsiasi momento
authenticate-user
Autorizza o nega l'accesso di un utente
Richiesta POST main.php?action=authenticate-user
La richiesta deve avere due parametri inviati [user, password]
Può essere emessa solo se il tipo di sessione (json, xml, html) è noto
calcola-imposta
Esegue una simulazione del calcolo delle imposte
Richiesta POST a main.php?action=calculate-tax
La richiesta deve avere tre parametri inviati [married, children, salary]
Può essere emessa solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato
list-simulations
Richiesta per visualizzare l'elenco delle simulazioni eseguite dall'inizio della sessione
Richiesta GET a main.php?action=list-simulations
La richiesta non accetta altri parametri
Può essere emessa solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato
delete-simulation
Elimina una simulazione dall'elenco delle simulazioni
Richiesta GET main.php?action=list-simulations&number=x
La richiesta non accetta altri parametri
Può essere emessa solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato
end-session
Termina la sessione di simulazione.
Tecnicamente, la vecchia sessione web viene eliminata e ne viene creata una nu
Può essere emessa solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato

Tutti i controller secondari procedono allo stesso modo:

  • verificano i propri parametri. Questi si trovano nell’oggetto [Request→query] per i parametri presenti nell’URL e nell’oggetto [Request→request] per quelli inviati tramite POST (richiesta POST);
  • Un controller è simile a una funzione o a un metodo che verifica la validità dei propri parametri. Per il controller, tuttavia, è un po’ più complicato:
    • i parametri attesi potrebbero mancare;
    • i parametri attesi sono tutti stringhe, mentre una funzione può specificare il tipo dei propri parametri. Se il parametro atteso è un numero, allora è necessario verificare che la stringa del parametro sia effettivamente quella di un numero;
    • una volta verificato che i parametri attesi sono presenti e sintatticamente corretti, è necessario verificare che siano validi nel contesto di esecuzione corrente. Questo contesto è presente nella sessione. L'esempio di autenticazione è un esempio di contesto di esecuzione. Alcune azioni dovrebbero essere elaborate solo dopo che il client è stato autenticato. Generalmente, una chiave nella sessione indica se questa autenticazione ha avuto luogo o meno;
    • una volta completati i controlli precedenti, il controller secondario può procedere. Questo processo di verifica dei parametri è molto importante. Non possiamo accettare che un client ci invii dati arbitrari in qualsiasi momento durante il ciclo di vita dell'applicazione. Dobbiamo mantenere il pieno controllo sul ciclo di vita dell'applicazione;
    • Una volta completato il proprio lavoro, il controller secondario restituisce l'array [$statusCode, $state, $content, $headers] richiesto dal controller principale che lo ha chiamato;

Ora esamineremo i vari controller — o, in altre parole, le varie azioni che guidano il ciclo di vita dell'applicazione web.

23.11.1. L'azione [init-session]

L'azione [init-session] è gestita dal seguente [InitSessionController]:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
 
class InitSessionController implements InterfaceController {
 
  // $config is the application configuration
  // traitement d'une requête Request
  // session and can modify it
  // $infos is additional information specific to each controller
  
  // renders an array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {
 
    // you must have a GET and a single parameter other than [action]
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 2;
    if ($erreur) {
      $état = 701;
      $message = "méthode GET exigée avec paramètres [action, type] dans l'URL";
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // retrieve the GET parameters
    $erreur = FALSE;
    // type
    if (!$request->query->has("type")) {
      $erreur = TRUE;
      $état = 702;
      $message = "paramètre [type] manquant";
    } else {
      $type = strtolower($request->query->get("type"));
    }
    // type verification
    if (!$erreur && !array_key_exists($type, $config["types"])) {
      $erreur = TRUE;
      $état = 703;
      $message = "paramètre type [$type] invalide";
    }
    // mistake?
    if ($erreur) {
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // put the session type in the session
    $session->set("type", $type);
    // message of success
    $message = "session démarrée avec type [$type]";
    $état = 700;
    return [Response::HTTP_OK, $état, ["réponse" => $message], []];
  }
 
}

Commenti

  • Ci aspettiamo una richiesta [GET main.php?action=init-session&type=xxx]
  • righe 25-26: verifichiamo che la richiesta sia una richiesta GET con due parametri nell'URL;
  • righe 27–31: se così non fosse, registriamo l'errore e inviamo una risposta [$statusCode, $status, $content, $headers] al controller principale;
  • righe 35-39: verifichiamo che il parametro [type] sia presente nell'URL. In caso contrario, registriamo l'errore;
  • riga 40: registriamo il tipo di sessione;
  • righe 43–47: verifichiamo che il tipo di sessione sia uno dei termini (json, xml, html). In caso contrario, registriamo l'errore;
  • righe 49–51: se si è verificato un errore, viene inviato un risultato [$statusCode, $status, $content, $headers] al controller principale;
  • riga 53: il tipo di sessione viene memorizzato nella sessione dell'applicazione web;
  • righe 55–57: il controller ha terminato il suo lavoro. Viene inviata una risposta di successo [$statusCode, $status, $content, $headers] al controller principale;

Rivediamo cosa fa il controller principale con le risposte dei controller secondari:


// erreurs ?
if ($erreurs) {
  // on prépare la réponse sans l'envoyer  
  $statusCode = Response::HTTP_BAD_REQUEST;
  $content = ["réponse" => $erreurs];
  $headers = [];
} else {
  // ---------------------------
  // on exécute l'action à l'aide de son contrôleur
  $controller = __NAMESPACE__ . $config["actions"][$action];
  $logger->write("contrôleur : $controller\n");
  list($statusCode, $état, $content, $headers) = (new $controller())->execute($config, $request, $session);
}
 
// --------------------- on envoie la réponse
// cas de l'erreur fatale HTTP_INTERNAL_SERVER_ERROR
// on envoie un mail à l'administrateur si on peut
if ($statusCode === Response::HTTP_INTERNAL_SERVER_ERROR && $config['adminMail'] != NULL) {
  $infosMail = $config['adminMail'];
  $infosMail['message'] = json_encode($content, JSON_UNESCAPED_UNICODE);
  $sendAdminMail = new SendAdminMail($infosMail, $logger);
  $sendAdminMail->send();
}
// la réponse dépend du type de la session
if ($session->has("type")) {
  // le type de session est dans la session
  $type = $session->get("type");
} else {
  // si pas de type dans session, alors par défaut ce sera une réponse en jSON
  $type = "json";
}
// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
 
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
  • riga 12: il controller principale recupera il risultato dal controller secondario;
  • righe 35-36: dopo alcuni controlli, invia la risposta istanziando una delle classi [JsonResponse, XmlResponse, HtmlResponse] a seconda del tipo (json, xml, html) della sessione corrente;

Successivamente, eseguiremo dei test [Postman] nell'ambito di una sessione di simulazione utilizzando il tipo [json]. La funzionalità della classe [JsonResponse] è stata illustrata nella sezione collegata.

23.11.2. Test [Postman]

Image

Sopra:

  • in [2], tre nuovi test;
  • in [3-7], l'azione [init-session] con il parametro [type] mancante;
  • in [8-11], la risposta JSON del server;

Image

Sopra:

  • in [1-7], l'azione [init-session] con un parametro [type] errato;
  • in [8-11], la risposta JSON del server;

Image

Sopra:

  • in [1-8], l'azione [init-session] con il tipo JSON;
  • in [9-12], la risposta JSON del server;

23.11.3. L'azione [authenticate-user]

L'azione [authenticate-user] viene eseguita dal seguente controller [AuthentifierUtilisateurController]:


<?php
 
namespace Application;
 
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
class AuthentifierUtilisateurController implements InterfaceController {
 
  // $config is the application configuration
  // traitement d'une requête Request
  // session and can modify it
  // $infos is additional information specific to each controller
  // renders an array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {
 
    // you must have a POST and a single GET parameter
    $method = strtolower($request->getMethod());
    $erreur = $method !== "post" || $request->query->count() != 1;
    if ($erreur) {
      $état = 201;
      $message = "méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user,password]";
      // return the result to the main controller
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // retrieve POST parameters
    $erreurs = [];
    // user
    $état = 210;
    if (!$request->request->has("user")) {
      $état += 2;
      $erreurs[] = "paramètre [user] manquant";
    } else {
      $user = $request->request->get("user");
    }
    // password
    if (!$request->request->has("password")) {
      $état += 4;
      $erreurs[] = "paramètre [password] manquant";
    } else {
      $password = trim($request->request->get("password"));
    }
    // mistake?
    if ($erreurs) {
      // return the result to the main controller
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
    }
    // verification of user credentials
    // does the user exist?
    $users = $config["users"];
    $i = 0;
    $trouvé = FALSE;
    while (!$trouvé && $i < count($users)) {
      $trouvé = ($user === $users[$i]["login"] && $users[$i]["passwd"] === $password);
      $i++;
    }
    // found?
    if (!$trouvé) {
      // error message
      $message = "Echec de l'authentification [$user, $password]";
      $état = 221;
      // return the result to the main controller
      return [Response::HTTP_UNAUTHORIZED, $état, ["réponse" => $message], []];
    } else {
      // we note in the session that we have authenticated the user
      $session->set("user", TRUE);
      // message of success
      $message = "Authentification réussie [$user, $password]";
      $état = 200;
      // return the result to the main controller
      return [Response::HTTP_OK, $état, ["réponse" => $message], []];
    }
  }
 
}

Commenti

  • Ci aspettiamo una richiesta [POST main.php?action=authentifier-utilisateur] con due parametri [user, password];
  • righe 24–25: verifichiamo di avere una richiesta POST con un unico parametro nell'URL;
  • righe 26–31: se c'è un errore, lo registriamo e restituiamo un risultato [$statusCode, $status, $content, $headers] al controller principale;
  • righe 36–39: verifichiamo la presenza del parametro [user] nei valori inviati. Se non è presente, registriamo l'errore;
  • righe 43–45: verifichiamo la presenza del parametro [password] nei valori inviati. Se non è presente, registriamo l'errore;
  • righe 50–53: se manca uno qualsiasi dei valori inviati, viene restituito un risultato [$statusCode, $status, $content, $headers] al controller principale;
  • righe 56–62: verifichiamo che la coppia [$user,$password] recuperata sia presente nell'array [$config[‘users’]] nel file di configurazione;
  • righe 64–69: se così non fosse, l’errore viene registrato. Il codice di stato HTTP viene impostato su [Response::HTTP_UNAUTHORIZED] e il risultato [$statusCode, $status, $content, $headers] viene restituito al controller principale;
  • riga 72: l'autenticazione ha avuto esito positivo. Ciò viene annotato nella sessione impostando la chiave [user]. La presenza di questa chiave indica che l'autenticazione ha avuto esito positivo;
  • Righe 73–77: un risultato di successo [$statusCode, $status, $content, $headers] viene restituito al controller principale;

23.11.4. Test [Postman]

Eseguiamo i test [Postman] sul controller [AuthentifierUtilisateurController] in modalità JSON;

Image

Sopra:

  • in [1-6], l'azione [authenticate-user] con un GET [2], mentre è richiesto un POST;
  • in [7-10], la risposta JSON del server;

Sostituiamo il GET con un POST [2] senza includere alcun parametro nel corpo della risposta [7]:

Image

Sopra:

  • in [1-7], il POST senza parametri inviato in [7];
  • in [8-11], la risposta JSON del server;

Aggiungiamo ora un parametro [password] al corpo della richiesta [4]:

Image

Sopra:

  • in [1-6], una richiesta POST [2] con un parametro [password] inviato [4-6]. I parametri inviati devono essere aggiunti al corpo della richiesta [4]. Esistono diversi modi per inviare valori al server. Scegliamo il metodo [x-www-form-urlencoded] [5];
  • in [8-10], la risposta JSON dal server;

Ora definiamo il parametro [user] senza il parametro [password]:

Image

Sopra:

  • in [1-7], una richiesta POST senza il parametro [password] [4-7];
  • in [8-11], la risposta JSON del server;

Ora impostiamo i due parametri [user, password] ma con valori che causano il fallimento dell'autenticazione:

Image

Sopra:

  • in [1-9], una richiesta POST con parametri [user, password] errati;
  • in [10-13], la risposta JSON del server. Si noti il codice di stato [401 Non autorizzato] [10] nella risposta;

Ora una richiesta POST con credenziali valide:

Image

Sopra:

  • in [1-9], la richiesta POST [2] con credenziali valide [6-9];
  • in [10-13], la risposta JSON del server. Si noti il codice di stato HTTP [200 OK] in [10];

23.11.5. L'azione [calculate-tax]

L'azione [calculer-impot] è gestita dal seguente controller [CalculerImpotController]:


<?php
 
namespace Application;
 
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// layer alias [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
 
class CalculerImpotController implements InterfaceController {
 
  // $config is the application configuration
  // traitement d'une requête Request
  // session and can modify it
  // $infos is additional information specific to each controller
  // renders an array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {
 
    // you must have one GET parameter and three POST parameters
    $method = strtolower($request->getMethod());
    $erreur = $method !== "post" || $request->query->count() != 1;
    if ($erreur) {
      // we note the error
      $message = "il faut utiliser la méthode [post] avec [action] dans l'URL et les paramètres postés [marié, enfants, salaire]";
      $état = 301;
      // return result to main controller
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // retrieve POST parameters
    $erreurs = [];
    $état = 310;
    // marital status
    if (!$request->request->has("marié")) {
      $état += 2;
      $erreurs[] = "paramètre [marié] manquant";
    } else {
      $marié = trim(strtolower($request->request->get("marié")));
      $erreur = $marié !== "oui" && $marié !== "non";
      if ($erreur) {
        $état += 4;
        $erreurs[] = "valeur [$marié] invalide pour le paramètre [marié]";
      }
    }
    // the number of children
    if (!$request->request->has("enfants")) {
      $état += 8;
      $erreurs[] = "paramètre [enfants] manquant";
    } else {
      $enfants = trim($request->request->get("enfants"));
      $erreur = !preg_match("/^\d+$/", $enfants);
      if ($erreur) {
        $état += 9;
        $erreurs[] = "valeur [$enfants] invalide pour le paramètre [enfants]";
      }
    }
    // we recover the annual salary
    if (!$request->request->has("salaire")) {
      $erreurs[] = "paramètre [salaire] manquant";
      $état += 16;
    } else {
      $salaire = trim($request->request->get("salaire"));
      $erreur = !preg_match("/^\d+$/", $salaire);
      if ($erreur) {
        $état += 17;
        $erreurs[] = "valeur [$salaire] invalide pour le paramètre [salaire]";
      }
    }
    // mistake?
    if ($erreurs) {
      // return result to main controller
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $erreurs], []];
    }
 
    // we have everything you need to work
    // Redis
    \Predis\Autoloader::register();
    try {
      // customer [predis]
      $redis = new \Predis\Client();
      // connect to the server to see if it's there
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // it didn't go well
      // return result with error to main controller
      $état = 350;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }
 
    // we have valid parameters
    // creation of the [dao] layer
    if (!$redis->get("taxAdminData")) {
      try {
        // retrieve tax data from the database
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // put the recovered data into redis
        $redis->set("taxAdminData", $dao->getTaxAdminData());
      } catch (\RuntimeException $ex) {
        // it didn't go well
        // return result with error to main controller
        $état = 340;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // tax data are taken from the [application] scope memory
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
      // istanciation of the [dao] layer
      $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
    }
    // creation of the [business] layer
    $métier = new ServerMetier($dao);
 
    // we have everything we need to work - tax calculation
    $résultat = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
    // we add the simulation just run to the session
    $simulation = new Simulation();
    $résultat = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $résultat;
    $simulation->setFromArrayOfAttributes($résultat);
    // is there a list of in-session simulations?
    if (!$session->has("simulations")) {
      $simulations = [];
    } else {
      $simulations = $session->get("simulations");
    }
    // add simulation to simulation list
    $simulations[] = $simulation;
    // simulations are put back into session
    $session->set("simulations", $simulations);
    // return result to main controller
    $état = 300;
    return [Response::HTTP_OK, $état, ["réponse" => $résultat], []];
  }
 
}

Commenti

  • La richiesta prevista è [POST main.php?action=calculate-tax] con tre parametri inviati [married, children, salary]:
    • [married] deve essere [yes] o [no];
    • [children, salary] devono essere numeri interi positivi o zero;
  • righe 26–27: verifichiamo di avere una richiesta POST con un singolo parametro nell'URL;
  • righe 28–34: se così non fosse, viene inviato un risultato di errore al controller principale;
  • riga 36: accumuleremo i messaggi di errore nell'array [$errors];
  • righe 39–41: verifichiamo la presenza del parametro [married]. Se non è presente, l'errore viene registrato;
  • righe 43–49: verifichiamo che [married] abbia un valore tra [yes, no]. Se così non fosse, l'errore viene registrato;
  • righe 51–54: verifichiamo la presenza del parametro [children]. Se non è presente, viene registrato un errore;
  • righe 55–61: si verifica che il valore del parametro [children] sia un numero positivo o zero. Se così non fosse, viene registrato un errore;
  • righe 63–66: verifichiamo la presenza del parametro [salary]. Se non è presente, viene registrato un errore;
  • righe 67–72: Verifichiamo che il valore del parametro [salary] sia un numero positivo o zero. Se così non fosse, viene registrato un errore;
  • righe 75–78: se l'array [$errors] non è vuoto, significa che si sono verificati degli errori. Includiamo l'array degli errori nella risposta e restituiamo il risultato al controller principale;
  • riga 80: abbiamo parametri validi. Possiamo calcolare l'imposta. Per farlo, dobbiamo costruire i livelli [dao] e [business] che sanno come eseguire questo calcolo;
  • righe 82–94: creiamo un client [Redis];
  • righe 88–94: se non siamo riusciti a connetterci al server [Redis], inviamo un codice [500 Internal Server Error] al client;
  • riga 98: verifichiamo se il server [Redis] possiede la chiave [taxAdminData]. Questa chiave rappresenta i dati dell'amministrazione fiscale. Se la chiave non è presente, i dati fiscali devono essere recuperati dal database;
  • riga 101: costruzione del livello [dao] quando i dati fiscali devono essere recuperati dal database. La classe [ServerDaoWithRedis] è stata descritta nella sezione collegata;
  • riga 103: i dati recuperati dal database vengono memorizzati in [Redis] con la chiave [taxAdminData];
  • righe 104–110: se la query al database ha dato esito negativo, l'errore restituito dal livello [dao] viene registrato e incluso nel risultato rinviato al controller principale;
  • riga 109: il messaggio di errore restituito dal livello [PDO] è codificato in [iso-8859-1]. È codificato in [utf-8];
  • righe 111–117: se la chiave [taxAdminData] esiste nell'archivio [Redis], i dati fiscali vengono passati direttamente al costruttore del livello [DAO];
  • riga 119: viene creato il livello [business]. La classe [ServerMetier] è stata descritta nella sezione dei link;
  • righe 124–126: con l'importo dell'imposta calcolato, viene creato un oggetto [Simulation]. La classe [Simulation] incapsula i dati di una simulazione ed è stata descritta nella sezione dei link;
  • righe 128–132: la simulazione appena costruita deve essere aggiunta all'elenco delle simulazioni già calcolate. Questo elenco è in sessione a meno che non sia stata ancora eseguita alcuna simulazione;
  • righe 133–136: la simulazione viene aggiunta all'elenco delle simulazioni e l'elenco viene restituito alla sessione;
  • righe 137–139: il risultato viene restituito al controller principale;

23.11.6. Test [Postman]

Eseguiamo i test [Postman] sul controller [CalculerImpotController] in modalità JSON;

Image

Sopra:

  • in [1-7], effettuiamo una richiesta [GET] anziché una richiesta [POST];
  • In [8-11], la risposta JSON del server;

Ora, utilizziamo un metodo [POST], con o senza parametri inviati, nonché con parametri inviati non validi:

Image

Sopra:

  • effettuiamo una richiesta [POST] [2] con parametri inviati non validi [6-11] [married, children, salary]. È possibile omettere uno di questi parametri deselezionando la relativa casella in [16]. Ciò consentirà di testare diversi scenari. Nello screenshot sopra, tutti e tre i parametri sono presenti e tutti non validi;
  • in [12-15], la risposta JSON del server;

Ora deselezioniamo due dei tre parametri inviati:

Image

Sopra,

  • in [5-8], viene inviato solo il parametro [salary], che inoltre non è valido;
  • in [9-11], il risultato JSON dal server;

Ora eseguiamo un calcolo delle imposte con parametri validi:

Image

Sopra:

  • in [11-18], una richiesta con parametri validi [6-8];
  • in [12-14], la risposta JSON del server;

23.11.7. L'azione [lister-simulations]

L'azione [lister-simulations] è gestita dal seguente controller secondario [ListerSimulationsController]:


<?php
 
namespace Application;
 
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
class ListerSimulationsController {
 
  // $config is the application configuration
  // traitement d'une requête Request
  // useful session and can modify it
  // $infos is additional information specific to each controller
  // renders an array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {
 
    // you must have a single parameter GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      $état = 501;
      $message = "GET requis, avec l'unique paramètre [action] dans l'URL";
      // return an error result to the main controller
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // retrieve the list of simulations in the session
    if (!$session->has("simulations")) {
      $simulations = [];
    } else {
      $simulations = $session->get("simulations");
    }
    // a successful result is returned to the main controller
    $état = 500;
    return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
  }
 
}

Commenti

  • richiesta [GET main.php?action=list-simulations];
  • righe 24-25: verifichiamo che si tratti di una richiesta GET con un singolo parametro;
  • righe 26–31: se non è così, viene restituito un risultato di errore al controller principale;
  • righe 33-37: recuperiamo l'elenco delle simulazioni dalla sessione se presente (riga 36), altrimenti l'elenco è vuoto (riga 34);
  • righe 39-40: restituiamo l'elenco delle simulazioni al controller principale;

23.11.8. Test [Postman]

Creeremo due test, uno per un errore e uno per un esito positivo.

Image

Sopra:

  • in [1-8], effettuiamo una richiesta [GET] con un parametro aggiuntivo [param1] nell'URL [3, 7-8];
  • In [9-12], la risposta JSON del server;

Ora inviamo una richiesta valida:

Image

Sopra:

  • in [1-5], una richiesta valida;

Il risultato della richiesta è il seguente:

Image

  • in [3-6], la risposta JSON del server. Prima di questo test, il test [Postman] [calculate-tax-300] era stato eseguito più volte per creare simulazioni nella sessione web del server;

23.11.9. L'azione [delete-simulation]

L'azione [delete-simulation] è gestita dal seguente controller secondario [DeleteSessionController]:


<?php
 
namespace Application;
 
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
class SupprimerSimulationController {
 
  /// $config is the application configuration
  // traitement d'une requête Request
  // useful session and can modify it
  // $infos is additional information specific to each controller
  // renders an array [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {
 
    // you must have two GET parameters
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 2;
    $état = 600;
    if ($erreur) {
      $état += 2;
      $message = "GET requis, avec les paramètres [action, numéro]";
    }
    // parameter [number] must exist
    if (!$erreur) {
      $état += 4;
      $erreur = !$request->query->has("numéro");
      if ($erreur) {
        $message = "paramètre [numéro] manquant";
      }
    }
    // parameter [number] must be valid
    if (!$erreur) {
      $état += 8;
      $numéro = $request->query->get("numéro");
      $erreur = !preg_match("/^\d+$/", $numéro);
      if ($erreur) {
        $message = "paramètre [$numéro] invalide";
      }
    }
    // parameter [number] must be in the range [0,n-1]
    // if n is the number of simulations
    if (!$erreur) {
      $numéro = (int) $numéro;
      $erreur = !$session->has("simulations");
      if (!$erreur) {
        $simulations = $session->get("simulations");
        $erreur = $numéro < 0 || $numéro >= count($simulations);
      }
      if ($erreur) {
        $état += 16;
        $message = "la simulation n° [$numéro] n'existe pas";
      }
    }
    // mistake?
    if ($erreur) {
      // return the result to the main controller
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
    // delete the $numéro simulation
    unset($simulations[$numéro]);
    $simulations = array_values($simulations);
    // put the simulations back in the session
    $session->set("simulations", $simulations);
    // we return the list of simulations to the customer
    $état = 600;
    return [Response::HTTP_OK, $état, ["réponse" => $simulations], []];
  }

}

Commenti

  • richiesta [GET main.php?action=delete-simulation&number=x];
  • righe 24–30: verifichiamo di avere una richiesta GET con due parametri;
  • righe 32–38: verifichiamo che il parametro [number] sia presente nei parametri dell'URL;
  • righe 40-47: verifichiamo che il valore del parametro [number] sia sintatticamente corretto;
  • righe 50–61: verifichiamo che la simulazione #[number] esista effettivamente. Ci sono due casi di errore:
    • l'elenco delle simulazioni non è presente nella sessione (riga 52);
    • il numero di simulazione [number] da eliminare non esiste nell'elenco delle simulazioni;
  • righe 63–66: in caso di errore, viene restituito un risultato di errore al controller principale;
  • riga 68: la simulazione #[numero] viene eliminata;
  • riga 69: l'operazione [unset] non modifica gli indici [0, n-1] dell'elenco. Per aggiornarli, recuperiamo i valori dall'array [$simulations] per rimuovere la simulazione mancante;
  • riga 71: il nuovo array di simulazioni viene reinserito nella sessione;
  • righe 73-74: il nuovo elenco di simulazioni viene restituito al controller principale;

23.11.10. [Postman] Test

Eseguiremo test di successo e di fallimento:

Image

Sopra:

  • in [1-6], una richiesta GET senza il parametro [number];
  • in [7-10], la risposta JSON del server;

Ora una richiesta con un numero sintatticamente errato:

Image

Sopra:

  • in [1-5], una richiesta GET con un parametro [number] non valido [3, 5];
  • in [6-9], la risposta JSON del server;

Ora una richiesta con un numero di simulazione inesistente:

Image

Sopra:

  • in [1-5], una richiesta con un numero di simulazione pari a 100 che non esiste nell'elenco delle simulazioni;
  • in [6-9], la risposta JSON del server;

Ora, rimuoveremo la simulazione n. 0 dall'elenco, ovvero la prima simulazione. Per prima cosa, richiediamo nuovamente questo elenco utilizzando la richiesta [lister-simulations-500]:

Image

  • in [1], ci sono attualmente 2 simulazioni;

Eliminiamo la prima simulazione (numero 0):

Image

Sopra:

  • in [1-5], eliminiamo la simulazione n. 0 [5];
  • in [6-9], la risposta JSON del server. Possiamo vedere che la simulazione n. 0 è stata rimossa;

Ripetiamo questo passaggio:

Image

Sopra:

  • In [1], non ci sono più simulazioni disponibili nella sessione web del server;

23.11.11. L'azione [end-session]

L'azione [end-session] è gestita dal seguente controller secondario [FinSessionController]:


<?php
 
namespace Application;
 
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
class FinSessionController implements InterfaceController {
 
  // $config is the application configuration
  // traitement d'une requête Request
  // session and can modify it
  // $infos is additional information specific to each controller
  // renders an array [$statusCode, $état, $content, $headers]
 
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {
 
    // you must have a single parameter GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    // mistake?
    if ($erreur) {
      $état = 401;
      // result to main controller
      $message = "GET requis avec le seul paramètre [action] dans l'URL";
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }
 
    // memorize the session type
    $type = $session->get("type");
    // the current session is invalidated
    $session->invalidate();
    // put the type back in the new session
    $session->set("type", $type);
    // reply sent
    $état = 400;
    // result to main controller
    $content = ["réponse" => "session supprimée"];
    return [Response::HTTP_OK, $état, $content, []];
  }
 
}

Commenti

  • richiesta [GET main.php?action=end-session];
  • righe 25–33: verifichiamo che l'azione sia un GET con il singolo parametro [end-action];
  • riga 38: invalidiamo la sessione corrente. Questo cancella i dati in essa memorizzati e viene avviata una nuova sessione;
  • riga 36: prima di terminare la sessione, memorizziamo il suo tipo [json, xml, html];
  • riga 40: il tipo della sessione precedente viene impostato nella nuova sessione. Infine, procediamo con una nuova sessione contenente la singola chiave [type];
  • righe 44–45: il risultato viene restituito al controller principale;

23.11.12. Test [Postman]

Eseguiremo un test di errore e un test di successo:

Image

Sopra:

  • in [1-5], richiediamo la fine della sessione [5] con un POST [2] invece del previsto GET;
  • In [6-9], la risposta JSON del server;

Ora, un esempio di test riuscito. Innanzitutto, diamo un'occhiata al cookie di sessione scambiato tra il client [Postman] e il server durante l'ultimo test eseguito:

Image

Sopra:

  • in [3], il cookie di sessione inviato dal client [Postman] al server;

Ora diamo un'occhiata alle intestazioni HTTP inviate dal server nella sua risposta:

Image

Sopra:

  • in [3-4], il cookie di sessione non è presente nella risposta del server. Questo è normale. Il server lo invia una sola volta: all'inizio di una nuova sessione web;

Ora eseguiamo un'azione [logout] valida:

Image

Sopra:

  • in [1-3], un'azione [end-session] valida;
  • in [4-7], la risposta JSON del server;

Diamo un'occhiata alle intestazioni HTTP inviate nella risposta del server:

Image

  • in [3], il server invia l'intestazione [Set-Cookie], indicando che sta iniziando una nuova sessione web;

23.12. Tipi di risposta del server

23.12.1. Introduzione

Rivediamo l'architettura complessiva dell'applicazione:

Image

Presenteremo i possibili tipi di risposta [3a]. Questi sono raggruppati nella cartella [Responses] del progetto:

Image

Abbiamo già presentato la classe [JsonResponse] nella sezione collegata. Essa implementa l'interfaccia [InterfaceResponse] ed estende la classe [ParentResponse]. Lo stesso vale per le altre due classi, [XmlResponse] e [HtmlResponse].

Rivediamo la definizione dell'interfaccia [InterfaceResponse]:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
interface InterfaceResponse {
 
  // Request $request : requête en cours de traitement
  // Session $session: the web application session
  // array $config: application configuration
  // int statusCode: HTTP response status code
  // array $content: server response
  // array $headers: HTTP headers to be added to the response
  // Logger $logger: the logger for writing logs
  
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void;
}
  • righe 19–27: l'interfaccia [InterfaceResponse] ha un unico metodo [send] per inviare la risposta al client;
  • righe 11–17: il significato dei vari parametri del metodo [send];
  • righe 23–25: i parametri [$statusCode, $content, $headers] sono la risposta standard proveniente dai controller secondari dell’applicazione. Tuttavia, la risposta potrebbe richiedere informazioni aggiuntive. Pertanto, la forniamo con i primi tre parametri (righe 20–22), che le danno accesso a tutte le informazioni relative alla richiesta, alla sessione e alla configurazione;
  • riga 26: la risposta richiede il [Logger] perché registrerà la risposta inviata al client;

Esaminiamo ora il codice della classe [ParentResponse], la classe padre dei tre tipi di risposta che astrae ciò che hanno in comune: l'invio effettivo di una risposta di testo al client:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Response;
 
class ParentResponse {
 
  // int $statusCode: HTTP response status code
  // string $content: the body of the response to be sent
  // depending on the case, this is a jSON, XML, HTML string
  // array $headers: HTTP headers to be added to the response
 
  public function sendResponse(
    int $statusCode,
    string $content,
    array $headers): void {
 
    // preparing the server's text response
    $response = new Response();
    $response->setCharset("utf-8");
    // status code
    $response->setStatusCode($statusCode);
    // headers
    foreach ($headers as $text => $value) {
      $response->headers->set($text, $value);
    }
    // we send the answer
    $response->setContent($content);
    $response->send();
  }
}

Commenti

  • righe 10–13: significato dei tre parametri del metodo [send];
  • riga 17: si noti che il corpo della risposta è di tipo [string] e quindi pronto per essere inviato (riga 30);
  • riga 22: la risposta conterrà caratteri UTF-8;
  • riga 24: codice di stato HTTP della risposta;
  • righe 26–28: aggiunta delle intestazioni HTTP fornite dal codice chiamante;
  • righe 30–31: invio della risposta al client;

Infine, rivediamo il codice del controller principale che richiede l'invio della risposta al client:


// on ajoute les clés [action, état] à la réponse du contrôleur
$content = ["action" => $action, "état" => $état] + $content;
// on instancie l'objet [Response] chargée d'envoyer la réponse au client
$response = __NAMESPACE__ . $config["types"][$type]["response"];
(new $response())->send($request, $session, $config, $statusCode, $content, $headers, $logger);
 
// la réponse a été envoyée - on libère les ressources
$logger->close();
exit;
  • riga 4: impostiamo il nome della classe [Response] da istanziare;
  • riga 5: la istanziamo e inviamo la risposta al client utilizzando il metodo [send($request, $session, $config, $statusCode, $content, $headers, $logger)]. Poiché implementano la stessa interfaccia [InterfaceResponse], i metodi [send] dei diversi tipi di risposta hanno tutti la stessa firma;

23.12.2. La classe [JsonResponse]

È già stata presentata nella sezione collegata. Tuttavia, ne riproduciamo qui il codice per evidenziare meglio la coerenza delle tre classi di risposta:

La classe [JsonResponse] implementa l'interfaccia [InterfaceResponse] come segue:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
class JsonResponse extends ParentResponse implements InterfaceResponse {
 
  // Request $request : requête en cours de traitement
  // Session $session: the web application session
  // array $config: application configuration
  // int statusCode: HTTP response status code
  // array $content: server response
  // array $headers: HTTP headers to be added to the response
  // Logger $logger: the logger for writing logs
 
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void {
 
    // symfony serializer preparation
    $serializer = new Serializer(
      [
      // required for object serialization
      new ObjectNormalizer()],
      // encoder jSON
      // for options, make OU between the different options
      [new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))]
    );
    // serialization jSON
    $json = $serializer->serialize($content, 'json');
    // headers
    $headers = array_merge($headers, ["content-type" => "application/json"]);
    // sending reply
    parent::sendResponse($statusCode, $json, $headers);
    // log
    if ($logger !== NULL) {
      $logger->write("réponse=$json\n");
    }
  }
 
}

Commenti

  • riga 13: la classe implementa l'interfaccia [InterfaceResponse];
  • riga 13: la classe estende la classe [ParentResponse]. Tutti i tipi [Response] estendono questa classe. È questa classe padre che invia la risposta al client (riga 46). Poiché questo codice era comune a tutti i tipi [Response], è stato estrapolato in una classe padre;
  • righe 33–40: istanziazione del serializzatore [Symfony], che convertirà la risposta del server [$content] in una stringa JSON (riga 42);
  • righe 34–36: il primo parametro del costruttore [Serializer] è un array. Al suo interno inseriamo un'istanza della classe [ObjectNormalizer] necessaria per la serializzazione degli oggetti. In questa applicazione, ciò avviene con un elenco di simulazioni in cui ogni simulazione è un'istanza della classe [Simulation];
  • riga 39: anche il secondo parametro del costruttore [Serializer] è un array: vi inseriamo tutti gli encoder utilizzati in una serializzazione (XML, JSON, CSV, ecc.);
  • riga 39: qui ci sarà un solo codificatore, di tipo [JsonEncoder]. Il costruttore senza parametri avrebbe potuto essere sufficiente. Qui, abbiamo passato un parametro [JsonEncode] al costruttore, esclusivamente per passare le opzioni di codifica JSON;
  • riga 39: il parametro del costruttore [JsonEncode] è un array di opzioni. Qui usiamo l'opzione [JSON_UNESCAPED_UNICODE] per richiedere che i caratteri UTF-8 nella stringa JSON vengano renderizzati in modo nativo anziché "escapati";
  • riga 42: il corpo della risposta HTTP viene serializzato in JSON utilizzando il serializzatore precedente;
  • riga 44: aggiungiamo l'intestazione HTTP che comunica al client che stiamo inviando JSON;
  • riga 46: alla classe padre viene chiesto di inviare la risposta al client;
  • righe 48–50: registriamo la risposta JSON;

23.12.3. La classe [XmlResponse]

La classe [XmlResponse] implementa l'interfaccia [InterfaceResponse] come segue:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
 
class XmlResponse extends ParentResponse implements InterfaceResponse {
 
  // Request $request : requête en cours de traitement
  // Session $session: the web application session
  // array $config: application configuration
  // int statusCode: HTTP response status code
  // array $content: server response
  // array $headers: HTTP headers to be added to the response
  // Logger $logger: the logger for writing logs
 
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void {
 
    // symfony serializer preparation
    $serializer = new Serializer(
      // required for object serialization
      [new ObjectNormalizer()],
      [
      // serialization XML
      new XmlEncoder(
        [
        XmlEncoder::ROOT_NODE_NAME => 'root',
        XmlEncoder::ENCODING => 'utf-8'
        ]
      ),
      // serialization jSON
      new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
      ]
    );
    // serialization XML
    $xml = $serializer->serialize($content, 'xml');
    // headers
    $headers = array_merge($headers, ["content-type" => "application/xml"]);
    // sending reply
    parent::sendResponse($statusCode, $xml, $headers);
    // log
    if ($logger !== NULL) {
      // log in jSON
      $log = $serializer->serialize($content, 'json');
      $logger->write("réponse=$log\n");
    }
  }
 
}

Commenti

  • righe 34–48: istanziamento di un serializzatore Symfony. Il costruttore accetta due parametri di tipo array;
  • riga 36: il primo array contiene un'istanza di tipo [ObjectNormalizer] utilizzata nella serializzazione degli oggetti;
  • righe 37–47: il secondo array contiene gli encoder utilizzati per la serializzazione. È possibile configurare vari tipi di serializzazione con lo stesso serializzatore;
  • righe 38–44: l'encoder XML;
  • riga 41: viene impostata la radice del codice XML generato. Avrà la forma <root>[altri tag XML]</root>;
  • riga 42: la codifica utilizzerà caratteri UTF-8;
  • riga 46: il codificatore JSON. Questo verrà utilizzato per registrare la risposta nel file [logs.txt], che è in formato JSON;
  • riga 50: il corpo della risposta inviata al client viene serializzato in XML;
  • riga 52: aggiungiamo alle intestazioni ricevute come parametri (riga 30) l'intestazione HTTP che comunica al client che stiamo inviando un documento XML;
  • riga 54: la classe padre invia effettivamente la risposta al client;
  • Righe 56–60: log JSON della risposta;

23.12.4. Test [Postman]

Abbiamo già eseguito tutti i possibili test di errore in JSON. Non c'è altro da fare in XML. Mostriamo due esempi di risposte XML:

Image

Sopra:

  • in [1-3], la richiesta di avvio della sessione XML;
  • in [4-7], la risposta XML del server;

D'ora in poi, tutte le risposte del server saranno in formato XML. Possiamo riutilizzare tutte le richieste già utilizzate in [Postman] senza modificarle e otterremo una risposta XML per ciascuna di esse. Eseguiamo un'autenticazione riuscita, ad esempio:

Image

Sopra:

  • in [1-3], una richiesta di autenticazione valida;
  • in [4-7], la risposta XML del server;

23.12.5. [HtmlResponse]

Quando il tipo di sessione è [html], viene istanziato un oggetto di tipo [HtmlResponse] per inviare la risposta al client. Questo invierà al client un flusso HTML che dipende dal codice di stato restituito dal controller secondario che ha elaborato l'azione. Questa mappatura [status=>view] è definita nel file di configurazione [config.json] come segue:


"vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
"vue-erreurs": "vue-erreurs.php"

Questa configurazione si legge come segue: [‘nome della vista’ => ‘stati associati a questa vista’]

  • riga 2: se il controller secondario ha restituito uno stato dall'array [700, 221, 400], allora deve essere visualizzata la vista [vue-authentification.php];
  • riga 3: se il controller secondario ha restituito un array [200, 300, 341, 350, 800], allora visualizza la vista [tax-calculation-view.php];
  • riga 4: se il controller secondario ha restituito un array [500, 600], visualizza la vista [view-simulation-list.php];
  • riga 6: se il controller secondario ha restituito un valore non presente in nessuno degli array precedenti, visualizza la vista [vue-erreurs.php];

Le viste si trovano nella cartella [Views] del progetto:

Image

Il codice della classe [HtmlResponse] è il seguente:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
 
class HtmlResponse extends ParentResponse implements InterfaceResponse {
 
  // Request $request : requête en cours de traitement
  // Session $session: the web application session
  // array $config: application configuration
  // int statusCode: HTTP response status code
  // array $content: server response
  // array $headers: HTTP headers to be added to the response
  // Logger $logger: the logger for writing logs
 
  public function send(
    Request $request = NULL,
    Session $session = NULL,
    array $config,
    int $statusCode,
    array $content,
    array $headers,
    Logger $logger = NULL): void {
 
    // symfony serializer preparation
    $serializer = new Serializer(
      [
      // for object serialization
      new ObjectNormalizer()],
      [
      // for jSON serialization of the response log
      new JsonEncoder(new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_UNICODE]))
      ]
    );
    // the HTML response depends on the status code returned by the controller
    $état = $content["état"];
    // a view corresponds to a state - look for it in the application configuration
    // view list
    $vues = array_keys($config["vues"]);
    $trouvé = false;
    $i = 0;
    // browse the list of views
    while (!$trouvé && $i < count($vues)) {
      // states associated with view n° i
      $états = $config["vues"][$vues[$i]];
      // is the state you're looking for in the states associated with view n° I?
      if (in_array($état, $états)) {
        // the view displayed will be view n° i
        $vueRéponse = $vues[$i];
        $trouvé = true;
      }
      // next view
      $i++;
    }
    // found?
    if (!$trouvé) {
      // if no view exists for the current state of the application
      // render error view
      $vueRéponse = $config["vue-erreurs"];
    }
    // retrieve the HTML view to be displayed in a character string
    ob_start();
    require __DIR__ . "/../Views/$vueRéponse";
    $html = ob_get_clean();
    // we indicate in the headers that we're going to send HTML
    $headers = array_merge($headers, ["content-type" => "text/html"]);
    // the parent class handles the actual sending of the response
    parent::sendResponse($statusCode, $html, $headers);
    // log in jSON of the response without the HTML
    if ($logger !== NULL) {
      // log in jSON of the response from the secondary controller that processed the action
      $log = $serializer->serialize($content, 'json');
      $logger->write("réponse=$log\n");
    }
  }
 
}

Commenti

  • righe 32–41: istanziamo un serializzatore Symfony. Ciò è necessario per il log JSON della risposta dal controller che ha gestito l'azione (righe 72–82);
  • righe 42–57: cerchiamo nella configurazione dell’applicazione la vista da visualizzare. Ciò dipende dal codice di stato restituito dal controller che ha gestito l’azione. Questo codice si trova in [$content[‘status’]] (riga 43);
  • righe 42–61: viene cercata la vista corrispondente a questo stato;
  • righe 62–67: se non viene trovata alcuna vista, l'applicazione HTML si trova in uno stato anomalo. Spiegheremo questo concetto di stati anomali più in dettaglio in seguito. In questo caso, viene visualizzata una vista di errore;
  • righe 68–70: il codice PHP della vista selezionata viene interpretato e il risultato viene memorizzato nella variabile [$html] (riga 71);
  • Questo codice richiede alcune spiegazioni. Immaginiamo che la vista selezionata sia [vue-authentification.php], che visualizza un modulo di autenticazione web:
    • riga 69: la funzione [ob_start] avvia ciò che la documentazione chiama un buffer di output. Tutto ciò che viene scritto da print, require e operazioni simili — che normalmente verrebbe inviato immediatamente al client — viene inserito in un buffer di output (ob=output buffer) senza essere inviato al client;
    • riga 70: viene caricata la vista [authentication-view.php]; si tratta di una vista HTML dinamica contenente codice PHP. A questo punto accadono due cose:
      • il codice PHP nella vista [vue-authentification.php] viene caricato e interpretato. Il risultato è una vista che chiameremo [vue-authentification.html], che contiene solo codice HTML — e possibilmente CSS e JavaScript — ma non più PHP;
      • questo codice HTML viene normalmente inviato al client. Questo è in realtà il caso di qualsiasi testo incontrato dall'interprete PHP che non sia codice PHP. A causa del buffering dell'output, questo codice HTML viene inserito nel buffer di output senza essere inviato al client;
    • Riga 71: La funzione [ob_get_clean] fa due cose:
      • inserisce il contenuto del buffer di output nella variabile [$html], ovvero la pagina [vue-authentification.html] che vi era stata inserita;
      • svuota il buffer di output. Per quanto riguarda il buffer, è come se non fosse successo nulla. Inoltre, il client non ha ancora ricevuto nulla;
  • Riga 70: Stiamo attualmente eseguendo la classe [HtmlResponse], che si trova nella cartella [Responses]. Per trovare la vista, dobbiamo quindi salire di un livello [..] e poi navigare fino alla cartella [Views]. [__DIR__] è il percorso assoluto della cartella contenente lo script attualmente in esecuzione; nel nostro esempio, la cartella [C:/myprograms/laragon-lite/www/php7/scripts-web/impots/13/Responses];
  • riga 73: aggiungiamo alle intestazioni HTTP ricevute come parametri (riga 29) l'intestazione che comunica al client che gli invieremo HTML;
  • riga 75: chiediamo alla classe padre di inviare effettivamente la risposta al client;
  • righe 77–81: registriamo la risposta [$content] fornita dal controller secondario che ha elaborato l'azione corrente in JSON;

23.12.6. Test [Postman]

Per testare davvero la modalità HTML della sessione, dovremmo esaminare tutte le viste. Lo faremo più tardi. Eseguiremo il seguente test:

Diamo un'occhiata all'elenco delle viste nel file di configurazione:


"vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
    "vue-erreurs": "vue-erreurs.php"

Possiamo identificare il contesto che genera alcuni dei codici di stato sopra riportati esaminando i test [Postman] eseguiti:

Image

Possiamo vedere che il codice di stato [700] corrisponde a un'azione [init-session] riuscita [2]. Sopra abbiamo una risposta JSON, ma potrebbe anche essere XML o HTML. È proprio quest'ultimo caso che verrà testato. Secondo il file di configurazione, la vista [vue-authentification.php] costituisce la risposta HTML. Verifichiamolo.

Image

Sopra:

  • in [1-3], inizializziamo una sessione HTML. Ci aspettiamo quindi una risposta HTML;
  • in [4-8], la risposta HTML dal server;
  • la scheda [8] fornisce un'anteprima del codice HTML ricevuto;

Image

  • in [8-9], un'anteprima della vista HTML;

23.13. L'applicazione web HTML

23.13.1. Panoramica delle viste

L'applicazione web HTML utilizzerà quattro viste:

La vista di autenticazione:

Image

La vista di calcolo delle imposte:

Image

La vista dell'elenco delle simulazioni:

Image

La vista degli errori imprevisti:

Image

Descriveremo queste viste una per una.

23.13.2. La vista di autenticazione

23.13.2.1. Panoramica della vista

La schermata di autenticazione è la seguente:

Image

La vista è composta da due elementi che chiameremo frammenti:

  • il frammento [1] è generato da uno script [v-banner.php];
  • il frammento [2] è generato da uno script [v-authentication.php];

La vista di autenticazione è generata dalla seguente pagina [vue-authentification.php]:


<?php
// page test data
// encapsulate paged data in $page

?>
 
<!doctype html>
<html lang="fr">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
        <title>Application impots</title>
    </head>
    <body>
        <div class="container">
            <!-- bandeau sur 1 ligne et 12 colonnes -->
            <?php require "v-bandeau.php"; ?>
            <!-- formulaire d'authentification sur 9 colonnes -->
            <div class="row">
                <div class="col-md-9">
                    <?php require "v-authentification.php" ?>
                </div>
            </div>  
            <?php
            // if error - displays an error alert
            if ($modèle->error) {
              print <<<EOT
            <div class="row">                
                <div class="col-md-9">
                    <div class="alert alert-danger" role="alert">
                      Les erreurs suivantes se sont produites :
                      <ul>$modèle->erreurs</ul>
                    </div>
                </div>
            </div>
EOT;
            }
            ?>
        </div>
    </body>
</html>

Commenti

  • riga 7: un documento HTML inizia con questa riga;
  • righe 8–44: la pagina HTML è racchiusa tra i tag <html> e </html>;
  • righe 9–16: l'intestazione (head) del documento HTML;
  • riga 11: il tag <meta charset> indica che il documento è codificato in UTF-8;
  • riga 12: il tag <meta name='viewport'> imposta la visualizzazione iniziale del viewport: su tutta la larghezza dello schermo che lo visualizza (width) alla sua dimensione iniziale (initial-scale) senza ridimensionarlo per adattarlo a uno schermo più piccolo (shrink-to-fit);
  • riga 14: il tag <link rel='stylesheet'> specifica il file CSS che regola l'aspetto del viewport. Qui stiamo usando il framework CSS Bootstrap 4.1.3 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
  • riga 15: il tag title imposta il titolo della pagina:

Image

  • righe 17–43: il corpo della pagina web è racchiuso tra i tag `body` e `/body`;
  • righe 18–42: il tag <div> delimita una sezione della pagina visualizzata. Gli attributi [class] utilizzati nella vista fanno tutti riferimento al framework CSS Bootstrap. Il tag <div class=’container’> delimita un contenitore Bootstrap;
  • Riga 20: includiamo lo script [v-banner.php]. Questo script genera il banner della pagina [1]. Lo descriveremo tra poco;
  • Righe 22–26: il tag <div class=’row’> definisce una riga Bootstrap. Queste righe sono composte da 12 colonne;
  • riga 23: il tag <div class=’col-md-9’> definisce una sezione a 9 colonne;
  • riga 24: includiamo lo script [v-authentification.php] che visualizza il modulo di autenticazione della pagina [2]. Lo descriveremo tra poco;
  • riga 27: il tag <?php inserisce codice PHP nella pagina HTML. Questo codice viene eseguito prima che la pagina HTML venga renderizzata e può modificarla;
  • riga 29: tutti i dati dinamici nella vista visualizzata saranno incapsulati in un oggetto [$model] di tipo [stdClass]. Si tratta di una scelta arbitraria. Avremmo potuto scegliere invece un array associativo per ottenere lo stesso risultato;
  • riga 29: l'autenticazione fallisce se l'utente inserisce credenziali errate. In questo caso, la vista di autenticazione viene visualizzata nuovamente con un messaggio di errore. L'attributo [$model→error] indica se visualizzare questo messaggio di errore;
  • Righe 30–39: questa sintassi visualizza tutto il testo posto tra i simboli PHP <<<EOT (riga 30 — è possibile utilizzare qualsiasi testo al posto di EOT=End Of Text) e il simbolo EOT alla riga 39 (deve essere identico al simbolo utilizzato alla riga 30). Il simbolo deve essere scritto nella prima colonna della riga 39. Le variabili PHP situate nel testo tra i due simboli EOT vengono interpretate;
  • righe 33–36: definiscono un'area con sfondo rosa (class="alert alert-danger") (riga 33);

Image

  • riga 34: testo;
  • riga 35: il tag HTML <ul> (elenco non ordinato) visualizza un elenco puntato. Ogni voce dell'elenco deve avere la sintassi <li>voce</li>;

Prendiamo nota degli elementi dinamici da definire in questo codice:

  • [$model→error]: per visualizzare un messaggio di errore;
  • [$template→errors]: un elenco (in senso HTML) di messaggi di errore;

23.13.2.2. Il frammento [v-bandeau.php]

Il frammento [v-bandeau.php] visualizza il banner superiore di tutte le viste nell'applicazione web:

Image

Il codice del frammento [v-banner.php] è il seguente:


<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
    <div class="row">
        <div class="col-md-4">
            <img src="<?= $logo ?>" alt="Cerisier en fleurs" />
        </div>
        <div class="col-md-8">
            <h1>
                Calculez votre impôt
            </h1>
        </div>
    </div>
</div>

Commenti

  • righe 2–13: Il banner è racchiuso in una sezione Jumbotron di Bootstrap [<div class="jumbotron">]. Questa classe Bootstrap applica uno stile specifico al contenuto visualizzato per farlo risaltare;
  • righe 3–12: una riga Bootstrap;
  • righe 4-6: un'immagine [img] è posizionata nelle prime quattro colonne della riga;
  • riga 5: la sintassi [<?= $logo ?>] è equivalente alla sintassi [<?php print $logo ?>]. In altre parole, il valore dell'attributo [src] sarà il valore della variabile PHP [$logo];
  • righe 7–11: le restanti 8 colonne della riga (ricordate che ce ne sono 12 in totale) saranno utilizzate per visualizzare del testo (riga 9) con caratteri di grandi dimensioni (<h1>, righe 8–10);

Elementi dinamici:

  • [$logo]: URL dell'immagine visualizzata nel banner;

23.13.2.3. Il frammento [v-authentification.php]

Il frammento [v-authentication.php] visualizza il modulo di autenticazione dell'applicazione web:

Image

Il codice del frammento [v-authentication.php] è il seguente:


<!-- form HTML - post its values with the [authenticate-user] action -->
<form method="post" action="main.php?action=authentifier-utilisateur">
 
    <!-- title -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>
 
    <!-- bootstrap form -->
    <fieldset class="form-group">
        <!-- 1st line -->
        <div class="form-group row">
            <!-- wording -->
            <label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
            <div class="col-md-4">
                <!-- text input field -->
                <input type="text" class="form-control" id="user" name="user"
                       placeholder="Nom d'utilisateur" value="<?= $modèle->login ?>">
            </div>
        </div>
        <!-- 2nd line -->
        <div class="form-group row">
            <!-- wording -->
            <label for="password" class="col-md-3 col-form-label">Mot de passe</label>
            <!-- text input field -->
            <div class="col-md-4">
                <input type="password" class="form-control" id="password" name="password"
                       placeholder="Mot de passe">
            </div>
        </div>
        <!-- submit] button on a 3rd line-->
        <div class="form-group row">
            <div class="col-md-2">
                <button type="submit" class="btn btn-primary">Valider</button>
            </div>
        </div>
    </fieldset>

</form>

Commenti

  • Righe 2–39: Il tag <form> definisce un modulo HTML. Questo modulo presenta generalmente le seguenti caratteristiche:
    • definisce campi di immissione (tag <input> alle righe 17 e 27);
    • ha un pulsante [submit] (riga 34) che invia i valori inseriti all'URL specificato nell'attributo [action] del tag [form] (riga 2). Il metodo HTTP utilizzato per effettuare una richiesta a questo URL è specificato nell'attributo [method] del tag [form] (riga 2);
    • in questo caso, quando l'utente clicca sul pulsante [Submit] (riga 34), il browser invierà tramite POST (riga 2) i valori inseriti nel modulo all'URL [main.php?action=authentifier-utilisateur] (riga 2);
    • i valori inviati sono quelli inseriti dall'utente nei campi di immissione alle righe 17 e 27. Verranno inviati nel formato [user=xx&password=yy]. I nomi dei parametri [user, password] corrispondono agli attributi [name] dei campi di immissione alle righe 17 e 27;
  • Righe 5–7: Una sezione Bootstrap per visualizzare un titolo su sfondo blu:

Image

  • righe 10–37: un modulo Bootstrap. Tutti gli elementi del modulo saranno quindi stilizzati in modo specifico;
  • righe 12–20: definiscono la prima riga del modulo:

Image

  • la riga 14 definisce l'etichetta [1] su tre colonne. L'attributo [for] del tag [label] collega l'etichetta all'attributo [id] del campo di immissione alla riga 17;
  • righe 15–19: inseriscono il campo di immissione in un layout a quattro colonne;
  • riga 17: il tag HTML [input] definisce un campo di immissione. Ha diversi attributi:
    • [type='text']: si tratta di un campo di immissione testo. È possibile digitare qualsiasi cosa al suo interno;
    • [class='form-control']: stile Bootstrap per il campo di immissione;
    • [id='user']: identificatore del campo di immissione. Questo identificatore viene generalmente utilizzato dal codice CSS e JavaScript;
    • [name='user']: il nome del campo di immissione. Il valore inserito dall'utente verrà inviato dal browser con questo nome [user=xx];
    • [placeholder='prompt']: il testo visualizzato nel campo di immissione quando l'utente non ha ancora digitato nulla;

Image

  • [value='value']: il testo 'value' verrà visualizzato nel campo di immissione non appena appare, prima che l'utente inserisca qualsiasi altra cosa. Questo meccanismo viene utilizzato in caso di errore per visualizzare l'immissione che ha causato l'errore. In questo caso, questo valore sarà il valore della variabile PHP [$model->login];
  • righe 21–30: codice simile per il campo di immissione della password;
  • riga 27: [type='password'] crea un campo di immissione testo (è possibile digitare qualsiasi cosa) ma i caratteri inseriti sono nascosti:

Image

  • righe 32–36: una terza riga per il pulsante [Invia];
  • riga 34: poiché ha l'attributo [type="submit"], cliccando su questo pulsante si fa in modo che il browser invii i valori inseriti al server, come spiegato in precedenza. L'attributo CSS [class="btn btn-primary"] visualizza un pulsante blu:

Image

C'è un'ultima cosa da spiegare. Riga 2: l'attributo [action="main.php?action=authentifier-utilisateur"] definisce un URL incompleto (non inizia con http://machine:port/chemin). Nel nostro esempio, tutti gli URL dell'applicazione hanno il formato [http://localhost/php7/scripts-web/impots/version-12/main.php?action=xx]. La vista di autenticazione sarà accessibile tramite vari URL:

  • [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html];
  • [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur]

Questi URL puntano a un documento [main.php] situato all'indirizzo [http://localhost/php7/scripts-web/impots/version-12]. Ciò vale per tutti gli URL di questa applicazione. Il parametro [action="main.php?action=authentifier-utilisateur"] sarà preceduto da questo percorso quando i valori inseriti verranno inviati. Questi valori saranno quindi inviati all'URL [http://localhost/php7/scripts-web/impots/version-12/main.php?action=authentifier-utilisateur].

23.13.2.4. Test visivi

È possibile testare le viste ben prima di integrarle nell'applicazione. L'obiettivo in questo caso è testarne l'aspetto visivo. Raccoglieremo tutte le viste di test nella cartella [Tests] del progetto:

Image

Per testare la vista [vue-authentification.php], dobbiamo creare il modello di dati che verrà visualizzato:


<?php
// page test data
//
// calculate the view model
$modèle = getModelForThisView();
 
function getModelForThisView(): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();
  // user code
  $modèle->login = "albert";
  // error list
  $modèle->error = TRUE;
  $erreurs = ["erreur1", "erreur2"];
  // build a HTML list of errors
  $content = "";
  foreach ($erreurs as $erreur) {
    $content .= "<li>$erreur</li>";
  }
  $modèle->erreurs = $content;
  // banner image
  $modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
  // we render the model
  return $modèle;
}
?>
 
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        <!-- Required meta tags -->

    </head>
    <body>
        ….
    </body>
</html>

Commenti

  • righe 1–5: La vista di autenticazione presenta parti dinamiche controllate dall'oggetto [$model]. Questo oggetto è chiamato modello di vista. Secondo una delle due definizioni fornite per l'acronimo MVC, questo rappresenta la M in MVC;
  • riga 5: il modello di vista viene calcolato dalla funzione [getModelForThisView];
  • riga 9: il modello di vista sarà incapsulato in un tipo [stdClass];
  • righe 10–22: vengono definiti valori di prova per gli elementi dinamici della vista di autenticazione;

È possibile eseguire test visivi da NetBeans:

Image

Continuiamo questi test visivi finché non siamo soddisfatti del risultato.

23.13.2.5. Calcolo del modello di vista

Una volta determinato l'aspetto visivo della vista, possiamo procedere al calcolo del modello di vista in condizioni reali. Esaminiamo i codici di stato che portano a questa vista. Si trovano nel file di configurazione:


"vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
"vue-erreurs": "vue-erreurs.php"

Quindi, i codici di stato [700, 221, 400] sono quelli che attivano la visualizzazione della vista di autenticazione. Per comprendere il significato di questi codici, possiamo fare riferimento ai test [Postman] eseguiti sull'applicazione JSON:

  • [init-session-json-700]: 700 è il codice di stato che segue un'azione [init-session] riuscita: il modulo di autenticazione viene quindi visualizzato vuoto;
  • [authenticate-user-221]: 221 è il codice di stato che segue un'azione [authenticate-user] non riuscita (credenziali non riconosciute): viene quindi visualizzato il modulo di autenticazione per consentire la correzione delle credenziali;
  • [end-session-400]: 400 è il codice di stato che segue un'azione [end-session] riuscita: viene quindi visualizzato il modulo di autenticazione vuoto;

Ora che sappiamo quando deve essere visualizzato il modulo di autenticazione, possiamo calcolare il suo template in [authentication-view.php]:

Image

Il codice per il calcolo del modello di visualizzazione [vue-authentification.php] è il seguente:


<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: controller response
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
 
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // encapsulate paged data in $modèle
  $modèle = new stdClass();
  // application status
  $état = $content["état"];
  // the model depends on the state
  switch ($état) {
    case 700:
    case 400:
      // case of empty form display
      $modèle->login = "";
      // no error to display
      $modèle->error = FALSE;
      break;
    case 221:
      // false authentication
      // the user initially entered is redisplayed
      $modèle->login = $request->request->get("user");
      // there is an error to display
      $modèle->error = TRUE;
      // list HTML of error msg - here only one
      $modèle->erreurs = "<li>Echec de l'authentification</li>";
  }
  // result
  return $modèle;
}
?>
 
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Commenti

  • righe 3–6: vengono dichiarate le variabili ereditate dalla classe [HtmlResponse]; questa classe utilizza un [require] per visualizzare la vista [vue-authentification.php];
  • righe 9-10: le classi Symfony utilizzate nel codice della vista;
  • righe 15-40: la funzione [getModelForThisView] è responsabile del calcolo del modello di vista;
  • riga 19: viene recuperato il codice di stato restituito dal controller che ha elaborato l'azione corrente;
  • righe 21–37: il modello dipende da questo codice di stato;
  • righe 22–28: caso in cui deve essere visualizzato un modulo di autenticazione vuoto;
  • righe 29–37: caso di autenticazione fallita: viene visualizzato il nome utente inserito dall'utente, insieme a un messaggio di errore. L'utente può quindi effettuare un altro tentativo di autenticazione;

È stato scritto un template specifico per il banner [v-bandeau.php]:


<?php
  // logo
  $scheme = $request->server->get('REQUEST_SCHEME'); // http
  $host = $request->server->get('SERVER_NAME'); // localhost
  $port = $request->server->get('SERVER_PORT'); // 80
  $uri = $request->server->get('REQUEST_URI'); // /php7/scripts-web/impots/version-12/main.php?action=xxx
  $champs = [];
  preg_match("/(.+)\/.+?$/", $uri, $champs);
  $root = $champs[1]; // /php7/scripts-web/impots/version-12
  $modèle->logo = "$scheme://$host:$port$root/Views/logo.jpg"; // http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg
?>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
    <div class="row">
        <div class="col-md-4">
            <img src="<?= $modèle->logo ?>" alt="Cerisier en fleurs" />
        </div>
        <div class="col-md-8">
            <h1>
                Calculez votre impôt
            </h1>
        </div>
    </div>
</div>

Commenti

  • La riga 16 utilizza la variabile [$template→logo], che è l'URL del logo del banner. Anziché calcolare questa variabile quattro volte per le quattro viste dell'applicazione, questo calcolo viene integrato nel frammento [v-banner.php];
  • Le righe 1–11 mostrano come costruire l'URL [http://localhost:80/php7/scripts-web/impots/version-12/Views/logo.jpg] utilizzando le informazioni presenti nell'ambiente server [$request→server];

23.13.2.6. Test [Postman]

Abbiamo già creato delle richieste che restituiscono i codici di stato [700, 221, 400], che visualizzano la vista di autenticazione. Esaminiamole:

  • [init-session-html-700]: 700 è il codice di stato che segue un'azione [init-session] riuscita: viene quindi visualizzato il modulo di autenticazione vuoto;
  • [authenticate-user-221]: 221 è il codice di stato che segue un'azione [authenticate-user] non riuscita (credenziali non riconosciute): viene quindi visualizzato il modulo di autenticazione in modo che le credenziali possano essere corrette;
  • [end-session-400]: 400 è il codice di stato che segue un'azione [end-session] riuscita: viene quindi visualizzato il modulo di autenticazione vuoto;

Basta riutilizzarli e verificare se visualizzano correttamente la vista di autenticazione. Qui mostreremo solo due test:

  • [init-session-html-700]: avvio di una sessione HTML;

Image

  • [authenticate-user-221]: autenticazione dell'utente [x, x];

Image

Sopra:

  • la richiesta ha inviato la stringa [user=x&password=x];
  • in [4], viene visualizzato un messaggio di errore;
  • in [3], è stato visualizzato nuovamente l'utente errato;

23.13.2.7. Conclusione

Siamo riusciti a testare la vista [vue-authentification.php] senza aver scritto le altre viste. Ciò è stato possibile perché:

  • tutti i controller sono stati scritti;
  • [Postman] ci permette di inviare richieste al server senza bisogno delle viste. Quando si scrivono i controller, bisogna essere consapevoli che chiunque può farlo. Bisogna quindi essere pronti a gestire richieste che nessuna vista consentirebbe. Queste vengono create manualmente in [Postman]. Non si dovrebbe mai dare per scontato a priori che “questa richiesta sia impossibile”. Bisogna verificare;

23.13.3. La vista di calcolo delle imposte

23.13.3.1. Panoramica della vista

La vista di calcolo delle imposte è la seguente:

Image

La vista è composta da tre parti:

  • 1: Il banner in alto è generato dal frammento [v-bandeau.php] già presentato;
  • 2: il modulo di calcolo delle imposte generato dal frammento [v-calcul-impot.php];
  • 3: un menu con due link, generato dal frammento [v-menu.php];

La vista di calcolo delle imposte è generata dal seguente script [vue-calcul-impot.php]:

Image


<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: the response of the controller that processed the action
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
 
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();

  // we render the model
  return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
        <title>Application impots</title>
    </head>
    <body>
        <div class="container">
            <!-- bandeau -->
            <?php require "v-bandeau.php"?>
            <!-- ligne à deux colonnes -->
            <div class="row">
                <!-- le menu -->
                <div class="col-md-3">
                    <?php require "v-menu.php" ?>
                </div>
                <!-- le formulaire de calcul -->
                <div class="col-md-9">
                    <?php require "v-calcul-impot.php" ?>
                </div>
            </div>  
            <!-- cas du succès -->
            <?php
            if ($modèle->success) {
              // a success alert is displayed
              print <<<EOT1
            <div class="row">
                <div class="col-md-3">
 
                </div>
                <div class="col-md-9">
                    <div class="alert alert-success" role="alert">
                        $modèle->impôt</br>
                        $modèle->décôte</br>\n
                        $modèle->réduction</br>\n
                        $modèle->surcôte</br>\n
                        $modèle->taux</br>\n
                    </div>
                </div>
            </div>
EOT1;
            }
            ?>
            <?php
            if ($modèle->error) {
              // 9-column error list
              print <<<EOT2
                <div class="row">
                  <div class="col-md-3">
 
                  </div>
                  <div class="col-md-9">
                      <div class="alert alert-danger" role="alert">
                        L'erreur suivante s'est produite :
                        <ul>$modèle->erreurs</ul>
                      </div>
                  </div>
                </div>
EOT2;
            }
            ?>
        </div>
    </body>
</html>

Commenti

  • Commentiamo solo le nuove funzionalità che non sono state ancora riscontrate;
  • riga 37: inclusione del banner superiore della vista nella prima riga Bootstrap della vista;
  • righe 41–43: inclusione del menu, che occuperà tre colonne della seconda riga Bootstrap della vista;
  • righe 45–47: inserimento del modulo di calcolo delle imposte, che occuperà nove colonne della seconda riga Bootstrap della vista;
  • righe 51–69: se il calcolo delle imposte ha esito positivo [$model→success=TRUE], il risultato del calcolo viene visualizzato in un riquadro verde (righe 59–65). Questo riquadro si trova nella terza riga Bootstrap della vista (riga 54) e occupa nove colonne (riga 58) a destra di tre colonne vuote (righe 55–57). Questo riquadro si troverà quindi immediatamente sotto il modulo di calcolo delle imposte;
  • righe 71–87: se il calcolo dell'imposta fallisce [$model→error=TRUE], viene visualizzato un messaggio di errore in un riquadro rosa (righe 80–83). Questo frame si trova nella terza riga Bootstrap della vista (riga 75) e occupa nove colonne (riga 79) a destra di tre colonne vuote (righe 76–78). Questo frame si troverà quindi immediatamente sotto il modulo di calcolo delle imposte;

23.13.3.2. Il frammento [v-calcul-impot.php]

Il frammento [v-calcul-impot.php] visualizza il modulo di accesso all'applicazione web:

Image

Il codice del frammento [v-calcul-impot.php] è il seguente:


<!-- form HTML posted -->
<form method="post" action="main.php?action=calculer-impot">
    <!-- 12-column message on blue background -->
    <div class="col-md-12">
        <div class="alert alert-primary" role="alert">
            <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </div>
    </div>
    <!-- form elements -->
    <fieldset class="form-group">
        <!-- first row of 9 columns -->
        <div class="row">
            <!-- 4-column wording -->
            <legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
            <!-- 5-column radio buttons-->
            <div class="col-md-5">
                <div class="form-check">
                    <input class="form-check-input" type="radio" name="marié" id="gridRadios1" value="oui" <?= $modèle->checkedOui ?>>
                    <label class="form-check-label" for="gridRadios1">
                        Oui
                    </label>
                </div>
                <div class="form-check">
                    <input class="form-check-input" type="radio" name="marié" id="gridRadios2" value="non" <?= $modèle->checkedNon ?>>
                    <label class="form-check-label" for="gridRadios2">
                        Non
                    </label>
                </div>
            </div>
        </div>
        <!-- second row of 9 columns -->
        <div class="form-group row">
            <!-- 4-column wording -->
            <label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
            <!-- 5-column numerical entry field for number of children -->
            <div class="col-md-5">
                <input type="number" min="0" step="1" class="form-control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="<?= $modèle->enfants ?>">
            </div>
        </div>
        <!-- third row of 9 columns -->
        <div class="form-group row">
            <!-- 4-column wording -->
            <label for="salaire" class="col-md-4 col-form-label">Salaire annuel</label>
            <!-- 5-column numeric input field for wages -->
            <div class="col-md-5">
                <input type="number" min="0" step="1" class="form-control" id="salaire" name="salaire" placeholder="Salaire annuel" aria-describedby="salaireHelp" value="<?= $modèle->salaire ?>">
                <small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
            </div>
        </div>
        <!-- fourth row, [submit] button on 5 columns -->
        <div class="form-group row">
            <div class="col-md-5">
                <button type="submit" class="btn btn-primary">Valider</button>
            </div>
        </div>
    </fieldset>
 
</form>

Commenti

  • Riga 2: Il modulo HTML verrà inviato (attributo [method]) all'URL [main.php?action=calculer-impot] (attributo [action]). I valori inviati saranno quelli dei campi di input:
    • il valore del pulsante di opzione selezionato nel modulo:
      • [marié=oui] se è selezionato il pulsante di opzione [Oui] (righe 16–22). [marié] è il valore dell'attributo [name] nella riga 18, [oui] è il valore dell'attributo [value] nella riga 18;
      • [married=no] se è selezionato il pulsante di opzione [No] (righe 23–28). [married] è il valore dell'attributo [name] alla riga 24, e [no] è il valore dell'attributo [value] alla riga 24;
    • il valore del campo di immissione numerico alla riga 37 nella forma [children=xx], dove [children] è il valore dell'attributo [name] alla riga 37 e [xx] è il valore inserito dall'utente tramite la tastiera;
    • il valore del campo di immissione numerico alla riga 46 nella forma [salary=xx], dove [salary] è il valore dell'attributo [name] alla riga 46 e [xx] è il valore inserito dall'utente tramite la tastiera;

Infine, il valore inviato sarà nella forma [married=xx&children=yy&salary=zz].

  • I valori inseriti verranno inviati quando l'utente cliccherà sul pulsante [Invia] alla riga 53;
  • Righe 16–30: I due pulsanti di opzione:

Image

I due pulsanti di opzione fanno parte dello stesso gruppo di pulsanti di opzione perché hanno lo stesso attributo [name] (righe 18, 24). Il browser garantisce che all'interno di un gruppo di pulsanti di opzione ne sia selezionato solo uno alla volta. Pertanto, cliccandone uno si deseleziona quello che era stato selezionato in precedenza;

  • si tratta di pulsanti di opzione grazie all'attributo [type="radio"] (righe 18, 24);
  • quando il modulo viene visualizzato (prima dell'inserimento dei dati), uno dei pulsanti di opzione deve essere selezionato: per farlo, basta aggiungere l'attributo [checked=’checked’] al tag <input type="radio"> corrispondente. Ciò si ottiene utilizzando variabili dinamiche:
    • [<?= $model->checkedYes ?>] alla riga 18;
    • [<?= $model->checkedNo ?>] alla riga 24;

Queste variabili faranno parte del modello di visualizzazione.

  • Riga 37: un campo di immissione numerico [type="number"] con un valore minimo di 0 [min="0"]. Nei browser moderni, ciò significa che l'utente può inserire solo un numero >=0. In questi stessi browser moderni, l'immissione può essere effettuata utilizzando un cursore che può essere cliccato verso l'alto o verso il basso. L'attributo [step="1"] alla riga 37 indica che il cursore funzionerà con incrementi di 1. Di conseguenza, il cursore accetterà solo valori interi compresi tra 0 e n con incrementi di 1. Per l'inserimento manuale, ciò significa che i numeri con decimali non saranno accettati;

Image

  • riga 37: su alcune schermate, il campo di inserimento dei figli deve essere precompilato con l'ultimo valore inserito in quel campo. Per farlo, usiamo l'attributo [value], che imposta il valore da visualizzare nel campo di inserimento. Questo valore sarà dinamico e generato dalla variabile [$model→children];
  • riga 46: per l'inserimento dello stipendio valgono le stesse spiegazioni fornite per i figli;
  • riga 53: il pulsante [submit] che attiva l'invio (POST) dei valori inseriti all'URL [main.php?action=calculer-impot];

Image

23.13.3.3. Il frammento [v-menu.php]

Questo frammento visualizza un menu a sinistra del modulo di calcolo delle imposte:

Image

Il codice di questo frammento è il seguente:


<!-- bootstrap menu -->
<nav class="nav flex-column">
    <?php
    // affichage d'une liste de liens HTML
    foreach($modèle->optionsMenu as $texte=>$url){
      print <<<EOT3
      <a class="nav-link" href="$url">$texte</a>
EOT3;
    }
    ?>
</nav>

Commenti

  • righe 2–11: il tag HTML [nav] racchiude una sezione del documento HTML contenente collegamenti di navigazione ad altri documenti;
  • riga 7: il tag HTML [a] introduce un link di navigazione:
    • [$url]: è l'URL a cui l'utente viene indirizzato quando clicca sul link [$text]. Il browser esegue quindi un'operazione [GET $url]. Se [$url] è un URL relativo, gli viene anteposto il percorso principale dell'URL attualmente visualizzato nella barra degli indirizzi del browser. Pertanto, per creare il collegamento [1] quando l'URL corrente del browser è della forma [http://chemin/main.php?paramètres], creiamo il collegamento:
<a href=’main.php?action=liste-simulation’>Liste des simulations</a>
  • Riga 5: Il modello del frammento [$modèle→optionsMenu] sarà un array della forma:
[‘ Liste des simulations’=>’main.php?action=liste-simulations’,
‘ Fin de session’=>’main.php?action=fin-session’]
  • righe 2, 7: le classi CSS [nav, flex-column, nav-link] sono classi Bootstrap che definiscono l’aspetto del menu;

23.13.3.4. Test visivo

Raccogliamo questi vari elementi nella cartella [Tests] e creiamo un modello di test per la vista [view-tax-calculation.php]:

Image

Il modello di dati per la vista [view-tax-calculation] sarà il seguente:


<?php
// page test data
//
// calculate the view model
$modèle = getModelForThisView();
 
function getModelForThisView(): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();
  // form
  $modèle->checkedOui = "";
  $modèle->checkedNon = 'checked="checked"';
  $modèle->enfants = 2;
  $modèle->salaire = 300000;
  // message of success
  $modèle->success = TRUE;
  $modèle->impôt = "Montant de l'impôt : 1000 euros";
  $modèle->décôte = "Décôte : 15 euros";
  $modèle->réduction = "Réduction : 20 euros";
  $modèle->surcôte = "Surcôte : 0 euros";
  $modèle->taux = "Taux d'imposition : 14 %";
  // error message
  $modèle->error = TRUE;
  $erreurs = ["erreur1", "erreur2"];
  // build a HTML list of errors
  $content = "";
  foreach ($erreurs as $erreur) {
    $content .= "<li>$erreur</li>";
  }
  $modèle->erreurs = $content;
  // menu
  $modèle->optionsMenu = [
    'Liste des simulations' => 'main.php?action=liste-simulations',
    'Fin de session' => 'main.php?action=fin-session'];
  // banner image
  $modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
  // we render the model
  return $modèle;
}
 
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Commenti

  • Righe 7–39: Inizializziamo tutte le parti dinamiche della vista [vue-calcul-impot.php] e dei componenti [v-calcul-impot.php] e [v-menu.php];

Testiamo la vista [vue-calcul-impot.php]:

Image

Otteniamo il seguente risultato:

Image

Lavoriamo su questa vista finché non siamo soddisfatti del risultato visivo. A quel punto possiamo procedere all'integrazione della vista nell'applicazione web attualmente in fase di sviluppo.

23.13.3.5. Calcolo del modello di vista

Image

Una volta determinato l'aspetto visivo della vista, possiamo procedere al calcolo del modello di vista in condizioni reali. Esaminiamo i codici di stato che portano a questa vista. Si trovano nel file di configurazione:


"vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
"vue-erreurs": "vue-erreurs.php"

Questi codici di stato [200, 300, 341, 350, 800] sono quelli che attivano la visualizzazione della pagina di autenticazione. Per comprendere il significato di questi codici, possiamo fare riferimento ai test [Postman] eseguiti sull'applicazione JSON:

  • [authenticate-user-200]: 200 è il codice di stato che segue un'azione [authenticate-user] riuscita; viene quindi visualizzato il modulo di calcolo delle imposte vuoto;
  • [calculate-tax-300]: 300 è il codice di stato che segue un'azione [calculate-tax] riuscita. Viene quindi visualizzato il modulo di calcolo con i dati inseriti e l'importo dell'imposta. L'utente può quindi eseguire un altro calcolo;
  • [end-session-400]: 400 è il codice di stato che segue un'azione [end-session] riuscita: viene quindi visualizzato il modulo di autenticazione vuoto;
  • Il codice di stato [341] viene restituito per un calcolo dell'imposta valido, ma la mancanza di una connessione al DBMS causa un errore;
  • il codice di stato [350] viene restituito per un calcolo dell'imposta valido, ma la mancanza di una connessione al server [Redis] causa un errore;
  • il codice di stato [800] verrà presentato in seguito. Non lo abbiamo ancora incontrato;
  • Abbiamo ipotizzato che l'utente stia utilizzando un browser moderno. Pertanto, con il modulo in esame, non è possibile inserire numeri negativi, stringhe di caratteri non numerici o numeri decimali nei campi di immissione [figli, stipendio]. Con browser più vecchi, ciò sarebbe possibile. Tratteremo questi errori come errori imprevisti e visualizzeremo la vista [vue-erreurs];

Ora che sappiamo quando deve essere visualizzato il modulo di calcolo delle imposte, possiamo calcolarne il template in [tax-calculation-view.php]:


<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: the response of the controller that processed the action
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
 
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();
  // application status
  $état = $content["état"];
  // the model depends on the state
  switch ($état) {
    case 200 :
    case 800:
      // initial display of an empty form
      $modèle->success = FALSE; $modèle->errror = FALSE;
      $modèle->checkedNon = 'checked="checked"';
      $modèle->checkedOui = "";
      $modèle->enfants = "";
      $modèle->salaire = "";
      break;
    case 300:
      // successful calculation - result display
      $modèle->success = TRUE;
      $modèle->error = FALSE;
      $modèle->impôt = "Montant de l'impôt : {$content["réponse"]["impôt"]} euros";
      $modèle->décôte = "Décôte : {$content["réponse"]["décôte"]} euros";
      $modèle->réduction = "Réduction : {$content["réponse"]["réduction"]} euros";
      $modèle->surcôte = "Surcôte : {$content["réponse"]["surcôte"]} euros";
      $modèle->taux = "Taux d'imposition : " . ($content["réponse"]["taux"] * 100) . " %";
      // form restored with values entered
      $modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
      $modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
      $modèle->enfants = $request->request->get("enfants");
      $modèle->salaire = $request->request->get("salaire");
      break;
    case 341:
    // database HS
    case 350:
      // redis server HS
      // form restored with values entered
      $modèle->checkedOui = $request->request->get("marié") === "oui" ? 'checked="checked"' : "";
      $modèle->checkedNon = $request->request->get("marié") === "oui" ? "" : 'checked="checked"';
      $modèle->enfants = $request->request->get("enfants");
      $modèle->salaire = $request->request->get("salaire");
      // error
      $modèle->success = FALSE;
      $modèle->error = TRUE;
      $modèle->erreurs = "<li>{$content["réponse"]}</li>";
      break;
  }
  //menu
  $modèle->optionsMenu = [
    "Liste des simulations" => "main.php?action=lister-simulations",
    "Fin de session" => "main.php?action=fin-session"];
  // we render the model
  return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
        <title>Application impots</title>
    </head>
    <body>
        
    </body>
</html>

Commenti

  • righe 22–30: visualizzazione di un modulo vuoto;
  • righe 31–45: calcolo dell'imposta riuscito. I valori inseriti e l'importo dell'imposta vengono visualizzati nuovamente;
  • righe 46–59: caso in cui il calcolo dell'imposta fallisce a causa dell'indisponibilità di uno dei server [Redis] o [MySQL];
  • righe 62–64: calcolo delle due opzioni di menu;

23.13.3.6. Test [Postman]

Il test [calculate-tax-300] restituisce il codice di stato 300, indicando un calcolo dell'imposta riuscito:

Image

  • in [3], i valori che hanno portato al risultato [2];

Proviamo un caso di errore: errore [350] dovuto all'indisponibilità del server [Redis]:

Image

23.13.4. La vista elenco della simulazione

23.13.4.1. Panoramica della vista

La vista che mostra l'elenco delle simulazioni è la seguente:

Image

La vista generata dallo script [vue-liste-simulations] è composta da tre parti:

  • 1: il banner in alto è generato dal frammento [v-bandeau.php] già presentato;
  • 2: la tabella delle simulazioni generata dal frammento [v-simulation-list.php];
  • 3: un menu con due link, generato dal frammento [v-menu.php];

La vista delle simulazioni è generata dal seguente script [simulation-list-view.php]:

Image


<?php
 
// calculate the view model
$modèle = getModelForThisView();
 
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();
  
  // we render the model
  return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
        <title>Application impots</title>
    </head>
    <body>
        <div class="container">
            <!-- bandeau -->
            <?php require "v-bandeau.php"; ?>
            <!-- ligne à deux colonnes -->
            <div class="row">
                <!-- menu sur trois colonnes-->
                <div class="col-md-3">
                    <?php require "v-menu.php" ?>
                </div>
                <!-- liste des simulations sur 9 colonnes-->
                <div class="col-md-9">
                    <?php require "v-liste-simulations.php" ?>
                </div>
            </div>  
        </div>
    </body>
</html>

Commenti

  • riga 28: inserimento del banner dell'applicazione [1];
  • riga 33: inserimento del menu [2]. Verrà visualizzato in tre colonne sotto il banner;
  • riga 37: inserimento della tabella di simulazione [3]. Verrà visualizzata in nove colonne sotto il banner e a destra del menu;

Abbiamo già commentato due dei tre frammenti di questa vista:

  • [v-banner.php]: nella sezione link;
  • [v-menu.php]: nella sezione "link";

Il frammento [v-liste-simulations.php] è il seguente:


<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>
<!-- simulation table -->
<table class="table table-sm table-hover table-striped">
    <!-- headers of the six table columns -->
    <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Marié</th>
            <th scope="col">Nombre d'enfants</th>
            <th scope="col">Salaire annuel</th>
            <th scope="col">Montant impôt</th>
            <th scope="col">Surcôte</th>
            <th scope="col">Décôte</th>
            <th scope="col">Réduction</th>
            <th scope="col">Taux</th>
            <th scope="col"></th>
        </tr>
    </thead>
    <!-- table body (data displayed) -->
    <tbody>
        <?php
        $i = 0;
        // on affiche chaque simulation en parcourant le tableau des simulations
        foreach ($modèle->simulations as $simulation) {
          // affichage d'une ligne du tableau avec 6 colonnes - balise <tr>
          // colonne 1 : entête ligne (n° simulation) - balise <th scope='row'>
          // colonne 2 : valeur paramètre [marié] - balise <td>
          // colonne 3 : valeur paramètre [enfants] - balise <td>
          // colonne 4 : valeur paramètre [salaire] - balise <td>
          // colonne 5 : valeur paramètre [impôt] (de l'impôt) - balise <td>
          // colonne 6 : valeur paramètre [surcôte] - balise <td>
          // colonne 7 : valeur paramètre [décôte] - balise <td>
          // colonne 8 : valeur paramètre [réduction] - balise <td>
          // colonne 9 : valeur paramètre [taux] (de l'impôt) - balise <td>
          // colonne 10 : lien de suppression de la simulation - balise <td>
          print <<<EOT
        <tr>
          <th scope="row">$i</th>
          <td>{$simulation["marié"]}</td>
          <td>{$simulation["enfants"]}</td>
          <td>{$simulation["salaire"]}</td>
          <td>{$simulation["impôt"]}</td>
          <td>{$simulation["surcôte"]}</td>
          <td>{$simulation["décôte"]}</td>
          <td>{$simulation["réduction"]}</td>
          <td>{$simulation["taux"]}</td>
          <td><a href="main.php?action=supprimer-simulation&numéro=$i">Supprimer</a></td>
        </tr>
EOT;
          $i++;
        }
        ?>
        </tr>
    </tbody>
</table>

Commenti

  • Una tabella HTML viene creata utilizzando il tag <table> (righe 6 e 58);
  • Le intestazioni delle colonne della tabella sono racchiuse tra un tag <thead> (intestazione della tabella, righe 8, 21). Il tag <tr> (riga della tabella, righe 9 e 20) definisce una riga. Righe 10–15: il tag <th> (intestazione della tabella) definisce un'intestazione di colonna. Ce ne sono quindi dieci. [scope="col"] indica che l'intestazione si applica alla colonna. [scope="row"] indica che l'intestazione si applica alla riga;
  • righe 23–57: il tag <tbody> racchiude i dati visualizzati dalla tabella;
  • Righe 40–51: il tag <tr> racchiude una riga della tabella;
  • riga 41: il tag <th scope=’row’> definisce l’intestazione della riga;
  • righe 42–50: ogni tag td definisce una colonna della riga;
  • riga 27: l'elenco delle simulazioni si trova nel modello [$model→simulations], che è un array associativo;
  • riga 50: un link per eliminare la simulazione. L'URL utilizza il numero visualizzato nella prima colonna della tabella (riga 41);

23.13.4.2. Test visivo

Raccogliamo questi vari elementi nella cartella [Tests] e creiamo un modello di test per la vista [view-simulation-list.php]:

Image

Il modello di dati per la vista [simulation-list-view] sarà il seguente:


<?php
// calculate the view model
$modèle = getModelForThisView();
 
function getModelForThisView(): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();
  // put the simulations in the format expected by the page
  $modèle->simulations = [
    [
      "marié" => "oui",
      "enfants" => 2,
      "salaire" => 60000,
      "impôt" => 448,
      "décôte" => 100,
      "réduction" => 20,
      "surcôte" => 0,
      "taux" => 0.14
    ],
    [
      "marié" => "non",
      "enfants" => 2,
      "salaire" => 200000,
      "impôt" => 25600,
      "décôte" => 0,
      "réduction" => 0,
      "surcôte" => 8400,
      "taux" => 0.45
    ]
  ];
  // menu options
  $modèle->optionsMenu = [
    "Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
    "Fin de session" => "main.php?action=fin-session"];
  // banner image
  $modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
  // we render the model
  return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Commenti

  • righe 9–30: la tabella delle simulazioni visualizzata dalla tabella HTML;
  • righe 32–34: la tabella delle opzioni di menu;

Visualizziamo questa vista:

Image

Otteniamo il seguente risultato:

Image

Lavoriamo su questa vista finché non siamo soddisfatti del risultato visivo. A quel punto possiamo procedere all'integrazione della vista nell'applicazione web attualmente in fase di sviluppo.

23.13.4.3. Calcolo del modello di vista

Image

Una volta determinato l'aspetto visivo della vista, possiamo procedere al calcolo del modello di vista in condizioni reali. Esaminiamo i codici di stato che portano a questa vista. Si trovano nel file di configurazione:


"vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
"vue-erreurs": "vue-erreurs.php"

Sono quindi i codici di stato [500, 600] a visualizzare la vista di simulazione. Per capire il significato di questi codici, possiamo fare riferimento ai test [Postman] eseguiti sull'applicazione JSON:

  • [list-simulations-500]: 500 è il codice di stato che segue un'azione [list-simulations] riuscita: viene quindi visualizzato l'elenco delle simulazioni eseguite dall'utente;
  • [delete-simulation-600]: 600 è il codice di stato che segue un'azione [delete-simulation] riuscita. Viene quindi visualizzato il nuovo elenco di simulazioni ottenuto dopo questa eliminazione;

Ora che sappiamo quando deve essere visualizzato l'elenco delle simulazioni, possiamo calcolarne il template in [view-simulation-list.php]:


<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: controller response
// no errors possible
// array $content: controller response
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
 
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();
  // put the simulations in the format expected by the page
  // they are found in the response of the controller that executed the action
  // as an array of objects of type [Simulation]
  $objetsSimulation = $content["réponse"];
  // each [Simulation] object will be transformed into an associative array
  $modèle->simulations = [];
  foreach ($objetsSimulation as $objetSimulation) {
    $modèle->simulations[] = [
      "marié" => $objetSimulation->getMarié(),
      "enfants" => $objetSimulation->getEnfants(),
      "salaire" => $objetSimulation->getSalaire(),
      "impôt" => $objetSimulation->getImpôt(),
      "surcôte" => $objetSimulation->getSurcôte(),
      "décôte" => $objetSimulation->getdécôte(),
      "réduction" => $objetSimulation->getRéduction(),
      "taux" => $objetSimulation->getTaux()
    ];
  }
  // menu options
  $modèle->optionsMenu = [
    "Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
    "Fin de session" => "main.php?action=fin-session"];
  // we render the model
  return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
       
    </body>
</html>

Commenti

  • righe 26–36: calcolo del template [$template→simulations] utilizzato dal frammento [v-list-simulations.php];
  • righe 39-41: calcolo del template [$template→optionsMenu] utilizzato dal frammento [v-menu.php];

23.13.4.4. [Postman] Test

Il test [list-simulations-500] restituisce il codice di stato 500. Corrisponde a una richiesta di visualizzazione delle simulazioni:

Image

Il test [delete-simulation-600] restituisce un codice di stato 600. Corrisponde alla cancellazione riuscita della simulazione n. 0. Il risultato restituito è un elenco di simulazioni in cui manca una simulazione:

Image

23.13.5. Visualizzazione degli errori imprevisti

In questo contesto, per errore imprevisto si intende un errore che non avrebbe dovuto verificarsi durante il normale utilizzo dell'applicazione web.

Prendiamo ad esempio il test [calculate-tax-3xx] di [Postman] definito come segue:

Image

  • in [1-3], una richiesta POST con l'azione [calculer-impot];
  • in [4-6]: qui possiamo definire ciò che vogliamo per i tre parametri POST:
    • [4]: manca il parametro [marié];
    • [5-6]: i parametri [children, salary] sono presenti ma non validi;
  • in [9], questi tre errori vengono segnalati con il codice di stato 338;

Tuttavia, nel modulo HTML dell'applicazione web, questa situazione non può verificarsi:

  • tutti i parametri sono presenti;
  • il parametro [married], che prende il suo valore dagli attributi [value] di due pulsanti di opzione, deve avere uno dei valori [yes] o [no];
  • con un browser moderno, gli attributi <input type='number' min='0' step='1' …> assicurano che i valori inseriti per children e salary siano necessariamente numeri interi >=0;

Tuttavia, nulla impedisce a un utente di utilizzare [Postman] per inviare il test [calcul-impot-3xx] sopra riportato al nostro server. Abbiamo visto che la nostra applicazione web sa come rispondere correttamente a questa richiesta. Definiremo “errore imprevisto” un errore che non dovrebbe verificarsi nel contesto dell’applicazione HTML. Se si verifica, è probabile che qualcuno stia tentando di “hackerare” l’applicazione. A scopo didattico, abbiamo deciso di visualizzare una pagina di errore per questi casi. In realtà, potremmo semplicemente visualizzare nuovamente l'ultima pagina inviata al client. Per farlo, basta memorizzare l'ultima risposta HTML inviata nella sessione. In caso di errore imprevisto, restituiamo questa risposta. In questo modo, l'utente avrà l'impressione che il server non stia rispondendo ai suoi errori poiché la pagina visualizzata non cambia.

23.13.5.1. Visualizza presentazione

La vista che visualizza gli errori imprevisti è la seguente:

Image

La pagina generata dallo script [vue-erreurs.php] è composta da tre parti:

  • 1: Il banner in alto è generato dal frammento [v-banner.php] già presentato;
  • 2: gli errori imprevisti;
  • 3: un menu con tre link, generato dal frammento [v-menu.php];

La vista per gli errori imprevisti è generata dal seguente script [vue-erreurs.php]:

Image


<?php
// calculate the view model
$modèle = getModelForThisView();
 
function getModelForThisView(): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();

  // we return the model
  return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
        <title>Application impots</title>
    </head>
    <body>
        <div class="container">
            <!-- bandeau sur 12 colonnes -->
            <?php require "v-bandeau.php"; ?>
            <!-- ligne à deux colonnes -->
            <div class="row">
                <!-- menu sur 3 colonnes-->
                <div class="col-md-3">
                    <?php require "v-menu.php" ?>
                </div>
                <!-- liste des erreurs -->
                <div class="col-md-9">
                    <?php
                    print <<<EOT
                      <div class="alert alert-danger" role="alert">
                        Les erreurs inattendues suivantes se sont produites :
                        <ul>$modèle->erreurs</ul>
                      </div>
EOT;
                    ?>
                </div>
            </div>
        </div>
    </body>
</html>

Commenti

  • riga 27: inserimento del banner dell'applicazione [1];
  • riga 32: inserimento del menu [2]. Verrà visualizzato su tre colonne sotto il banner;
  • righe 34–44: visualizzazione dell'area degli errori su nove colonne;
  • righe 37–44: l'operazione [print] che visualizza errori imprevisti;
  • riga 38: questa visualizzazione apparirà in un contenitore Bootstrap con sfondo rosa;
  • riga 39: testo introduttivo;
  • riga 40: il tag <ul> racchiude un elenco puntato. Questo elenco puntato è fornito dal modello [$model->errors];

Abbiamo già commentato i due frammenti di questa vista:

  • [v-bandeau.php]: nella sezione dei link;
  • [v-menu.php]: nella sezione dei link;

23.13.5.2. Test visivi

Raccogliamo questi vari elementi nella cartella [Tests] e creiamo un modello di test per la vista [vue-erreurs.php]:

Image

Il modello di dati per la vista [vue-erreurs.php] sarà il seguente:


<?php
// calculate the view model
$modèle = getModelForThisView();
 
function getModelForThisView(): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();
 
  // the table of unexpected errors
  $erreurs = ["erreur1", "erreur2"];
  // build the HTML list of errors
  $modèle->erreurs = "";
  foreach ($erreurs as $erreur) {
    $modèle->erreurs .= "<li>$erreur</li>";
  }
  // menu options
  $modèle->optionsMenu = [
    "Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
    "Liste des simulations" => "main.php?action=lister-simulations",
    "Fin de session" => "main.php?action=fin-session",];
  // banner image
  $modèle->logo = "http://localhost/php7/scripts-web/impots/version-12/Tests/logo.jpg";
  // we return the model
  return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Commenti

  • righe 9–15: creazione dell'elenco degli errori HTML;
  • righe 17–20: l'array delle opzioni di menu;

Visualizziamo questa vista:

Image

Otteniamo il seguente risultato:

Image

Lavoriamo su questa vista finché non siamo soddisfatti del risultato visivo. A quel punto possiamo procedere all'integrazione della vista nell'applicazione web attualmente in fase di sviluppo.

23.13.5.3. Calcolo del modello di visualizzazione

Image

Una volta determinato l'aspetto visivo della vista, possiamo procedere al calcolo del modello di vista in condizioni reali. Esaminiamo i codici di stato che portano a questa vista. Sono disponibili nel file di configurazione:


"vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
"vue-erreurs": "vue-erreurs.php"

Pertanto, sono i codici di stato non elencati nelle righe [2–4] a far apparire la pagina degli errori imprevisti.

Il codice per il calcolo del modello di visualizzazione [vue-erreurs.php] è il seguente:


<?php
// we inherit the following variables
// Request $request : la requête en cours
// Session $session: the application session
// array $config: application configuration
// array $content: controller response
//
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
// calculate the view model
$modèle = getModelForThisView($request, $session, $config, $content);
 
function getModelForThisView(Request $request, Session $session, array $config, array $content): object {
  // encapsulate paged data in $modèle
  $modèle = new \stdClass();
 
  // recover errors from the controller response
  $réponse = $content["réponse"];
  if (!is_array($réponse)) {
    // a single error message
    $erreurs = [$réponse];
  } else {
    // several error messages
    $erreurs = $réponse;
  }
  // build the HTML list of errors
  $modèle->erreurs = "";
  foreach ($erreurs as $erreur) {
    $modèle->erreurs .= "<li>$erreur</li>";
  }
  // menu options
  $modèle->optionsMenu = [
    "Calcul de l'impôt" => "main.php?action=afficher-calcul-impot",
    "Liste des simulations" => "main.php?action=lister-simulations",
    "Fin de session" => "main.php?action=fin-session",];
 
  // we return the model
  return $modèle;
}
?>
<!-- document HTML -->
<!doctype html>
<html lang="fr">
    <head>
        
    </head>
    <body>
        
    </body>
</html>

Commenti

  • righe 19–32: calcolo del template [$template→errors] utilizzato dalla vista [view-errors.php];
  • righe 34-37: calcolo del template [$template→optionsMenu] utilizzato dal frammento [v-menu.php];

23.13.5.4. Test [Postman]

Il test [calculate-tax-3xx] restituisce il codice di stato 338, che non è un codice di stato previsto. La risposta HTML è la seguente:

Image

23.13.6. Implementazione delle azioni del menu dell'applicazione

Qui discuteremo l'implementazione delle azioni del menu. Rivediamo il significato dei link che abbiamo incontrato

Visualizza
Link
Destinazione
Ruolo
Calcolo delle imposte
[Elenco delle simulazioni]
[main.php?action=list-simulations]
Richiedi l'elenco delle simulazioni
  
[Chiudi sessione]
Elenco delle simulazioni
[Calcolo delle imposte]
[main.php?action=display-tax-calculation]
Visualizza la schermata del calcolo delle imposte
  
[Esci]
Errori imprevisti
[Calcolo delle imposte]
[main.php?action=display-tax-calculation]
Visualizza la schermata del calcolo delle imposte
  
[Elenco delle simulazioni]
  
[Chiudi sessione]

È importante notare che cliccando su un link si innesca una richiesta GET verso la destinazione del link. Le azioni [lister-simulations, fin-session] sono state implementate utilizzando un'operazione GET, il che ci permette di utilizzarle come destinazioni dei link. Quando l'azione viene eseguita tramite una richiesta POST, l'uso di un link non è più possibile a meno che non sia combinato con JavaScript.

Dalle azioni sopra riportate, sembra che l'azione [display-tax-calculation] non sia stata ancora implementata. Si tratta di un'operazione di navigazione tra due viste: il server JSON o XML non ha motivo di implementarla perché non ha il concetto di vista. È il server HTML che introduce questo concetto.

Dobbiamo quindi implementare l'azione [display-tax-calculation]. Questo ci permetterà di rivedere la procedura per l'implementazione di un'azione all'interno del server.

Per prima cosa, dobbiamo aggiungere un nuovo controller secondario. Lo chiameremo [AfficherCalculImpotController]:

Image

Questo controller deve essere aggiunto al file di configurazione [config.json]:


{
    "databaseFilename": "database.json",
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-12",
    "relativeDependencies": [
 

 
        "/Controllers/InterfaceController.php",
        "/Controllers/InitSessionController.php",
        "/Controllers/ListerSimulationsController.php",
        "/Controllers/AuthentifierUtilisateurController.php",
        "/Controllers/CalculerImpotController.php",
        "/Controllers/SupprimerSimulationController.php",
        "/Controllers/FinSessionController.php",
        "/Controllers/AfficherCalculImpotController.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php",
        "C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
    ],

    "actions":
            {
                "init-session": "\\InitSessionController",
                "authentifier-utilisateur": "\\AuthentifierUtilisateurController",
                "calculer-impot": "\\CalculerImpotController",
                "lister-simulations": "\\ListerSimulationsController",
                "supprimer-simulation": "\\SupprimerSimulationController",
                "fin-session": "\\FinSessionController",
                "afficher-calcul-impot": "\\AfficherCalculImpotController"
            },

    "vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
    "vue-erreurs": "vue-erreurs.php"
}
  • riga 15: il nuovo controller;
  • riga 30: la nuova azione e il suo controller;
  • riga 35: il nuovo controller restituirà il codice di stato 800. Quando si cambia vista, non ci possono essere errori;

Il controller [AfficherCalculImpotController.php] avrà questo aspetto:


<?php
 
namespace Application;
 
// symfony dependencies
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Response;
 
class AfficherCalculImpotController implements InterfaceController {
 
  // $config is the application configuration
  // traitement d'une requête Request
  // session and can modify it
  // $infos is additional information specific to each controller
  // renders an array [$statusCode, $état, $content, $headers]
  
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {
 
    // view change - just a status code to set
    return [Response::HTTP_OK, 800, ["réponse" => ""], []];
  }
 
}

Commenti

  • riga 10: come gli altri controller secondari, il nuovo controller implementa l'interfaccia [InterfaceController];
  • Le modifiche alla vista sono facili da implementare: basta restituire il codice di stato associato alla vista di destinazione, in questo caso il codice 800 come visto sopra;

23.13.7. Test nel mondo reale

Il codice è stato scritto e ogni azione è stata testata con [Postman]. Dobbiamo ancora testare il flusso della vista in uno scenario reale. Abbiamo bisogno di un modo per inizializzare la sessione HTML. Sappiamo che dobbiamo inviare i parametri [action=init-session&type=html] al server. Per evitare di doverli digitare nella barra degli indirizzi del browser, aggiungeremo lo script [index.php] alla nostra applicazione:

Image

Lo script [index.php] sarà il seguente:


<?php
 
// redirect to [main.php] in [html] mode
header('Location: main.php?action=init-session&type=html');
  • Riga 4: [header] è una funzione PHP che aggiunge un'intestazione HTTP alla risposta. L'intestazione HTTP [Location: main.php?action=init-session&type=html] indica al browser client di reindirizzarsi all'URL di destinazione specificato in [Location]. Lo script [index.php] viene richiesto con l'URL [http://localhost/php7/scripts-web/impots/version-12/index.php]. Quando il browser del client riceve il reindirizzamento all'URL relativo [main.php?action=init-session&type=html], richiederà l'URL assoluto [http://localhost/php7/scripts-web/impots/version-12/main.php?action=init-session&type=html] e la sessione HTML avrà inizio;

L'URL di avvio può essere semplificato in [http://localhost/php7/scripts-web/impots/version-12/]. Se nell'URL non viene specificata alcuna pagina, vengono utilizzate per impostazione predefinita le pagine [index.html, index.php]. In questo caso, verrà quindi utilizzato lo script [index.php];

Cominciamo: presenteremo ora alcune sequenze di visualizzazione.

Nel nostro browser, attiviamo gli strumenti di sviluppo (F12 in Firefox) e richiediamo l'URL di avvio [https://localhost/php7/scripts-web/impots/version-12/]:

Image

  • Al punto [4], la prima risposta del server è un reindirizzamento 302:
  • Al punto [5], viene effettuata una nuova richiesta all'URL [http://localhost/php7/scripts-web/impots/13/main.php?action=init-session&type=html];

Diamo un'occhiata più da vicino al reindirizzamento 302:

Image

  • in [8], il codice HTTP [302] è un codice di reindirizzamento: al browser client viene comunicato che l'URL richiesto è stato spostato. Il nuovo URL è specificato in [9]. Il browser seguirà questo reindirizzamento con una nuova richiesta GET:

Image

  • in [12-13], la nuova richiesta effettuata dal browser;

Compiliamo il modulo che abbiamo ricevuto;

Image

Quindi eseguiamo alcune simulazioni:

Image

Image

Richiediamo l'elenco delle simulazioni:

Image

Elimina la prima simulazione:

Image

Chiudi la sessione:

Image

Il lettore è invitato a eseguire ulteriori test.

23.14. Client di servizi web JSON

23.14.1. Architettura client/server

Image

Ci concentreremo ora sul client JSON [A] del servizio web [B]. Il client [A], come il servizio web [B], ha una struttura a livelli:

Image

Questa architettura si riflette nella seguente organizzazione del codice:

Image

La maggior parte delle classi è già stata presentata e spiegata:

BaseEntity
link al paragrafo.
TaxPayerData
link al paragrafo.
Simulazione
link al paragrafo.
Eccenzioni fiscali
link al paragrafo.
TraitDao
Link al paragrafo.
Utilità
link al paragrafo.

23.14.2. Il livello [dao]

Image

23.14.2.1. Interfaccia

L'interfaccia per il livello [dao] sarà la seguente [InterfaceClientDao.php]:


<?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): Simulation;
 
  // recording results
  public function saveResults(string $resultsFilename, array $simulations): void;
 
  // authentication
  public function authentifierUtilisateur(String $user, string $password): void;
 
  // list of simulations
  public function listerSimulations(): array;
 
  // delete a simulation
  public function supprimerSimulation(int $numéro): array;
 
  // start of session
  public function initSession(string $type = 'json'): void;
 
  // end of session
  public function finSession(): void;
}

Commenti

  • riga 9: il metodo [getTaxPayersData] consente di utilizzare il file JSON contenente i dati dei contribuenti. Questo metodo è implementato dal trait [TraitDao], già discusso in precedenza (vedi il paragrafo collegato);
  • riga 15: il metodo [saveResults] salva i risultati di più calcoli fiscali in un file JSON. Anche in questo caso, il metodo è implementato dal trait [TraitDao] già discusso (paragrafo collegato);
  • Righe 12, 18, 21, 27, 30: è stato creato un metodo per ogni azione accettata dal servizio web;

23.14.2.2. Implementazione

L'interfaccia [InterfaceClientDao] è implementata dalla seguente classe [ClientDao]:


<?php
 
namespace Application;
 
// dependencies
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\CurlResponse;
 
class ClientDao implements InterfaceClientDao {
  // using a Trait
  use TraitDao;
  // attributes
  private $urlServer;
  private $sessionCookie;
  private $verbose;
 
  // manufacturer
  public function __construct(string $urlServer, bool $verbose = TRUE) {
    $this->urlServer = $urlServer;
    $this->verbose = $verbose;
  }

}

Commenti

  • righe 18–21: il costruttore riceve due parametri:
    • l'URL [$urlServer] del servizio web JSON;
    • un valore booleano [$verbose] che, se impostato su TRUE, indica che la classe deve visualizzare le risposte del server sulla console;
  • riga 14: il cookie di sessione. Il suo ruolo è stato descritto nella versione 09 del client (paragrafo del link);
  • riga 11: la classe utilizza il trait [TraitDao], che implementa due metodi dell'interfaccia:
    • [getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array];
    • [function calculateTax(string $married, int $children, int $salary): Simulation];

23.14.2.2.1. Metodo [initSession]

Il metodo [initSession] è implementato come segue:


public function initSession(string $type = 'json'): void {
    // create a HTTP customer
    $httpClient = HttpClient::create();
    // make the request to the server without authentication
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "action" => "init-session",
          "type" => $type
        ],
        "verify_peer" => false
    ]);
    // the answer is retrieved
    $this->getResponse($response);
    // 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];
        }
      }
    }
  }

Poiché l'azione [init-session] deve essere la prima azione richiesta al servizio web, il metodo [initSession] sarà il primo metodo del livello [dao] a essere chiamato.

Commenti

  • riga 1: il tipo di sessione desiderato viene passato come parametro. Se non viene fornito alcun parametro, verrà avviata una sessione JSON;
  • righe 5–11: viene effettuata una richiesta GET al servizio web;
  • righe 7–8: i due parametri GET;
  • riga 10: nel caso di comunicazione sicura (HTTPS), il certificato di sicurezza inviato dal servizio web non verrà verificato;
  • riga 13: il metodo [getResponse] recupera la risposta del server. La restituisce sotto forma di array. In questo caso, il risultato del metodo non viene utilizzato. Il metodo [getResponse] genera un'eccezione se il codice di stato HTTP della risposta del servizio web non è 200 OK;
  • Righe 14–25: poiché il metodo [initSession] è il primo metodo del livello [dao] ad essere eseguito, recuperiamo il cookie di sessione in modo che i metodi successivi possano rispedirlo al servizio web. Questo codice era già stato commentato nella versione 09;

23.14.2.2.2. Il metodo [getResponse]

Il metodo [getResponse] è responsabile dell'elaborazione della risposta del servizio web:


private function getResponse(CurlResponse $response) {
    // the answer is retrieved
    $json = $response->getContent(false);
    // logs
    if ($this->verbose) {
      print "$json\n";
    }
    // retrieve response status
    $statusCode = $response->getStatusCode();
    // mistake?
    if ($statusCode !== 200) {
      // we have an error
      throw new ExceptionImpots($json);
    }
    // we give our answer
    $array = json_decode($json, true);
    return $array["réponse"];
  }

Commenti

  • riga 1: il metodo è privato;
  • riga 1: il parametro del metodo è la risposta del servizio web di tipo [Symfony\Component\HttpClient\Response\CurlResponse], il tipo di risposta Symfony, quando [HttpClient] è implementato da [CurlClient], ovvero dalla libreria [curl];
  • riga 3: recuperiamo la risposta JSON dal server. Si noti che il parametro [false] serve a impedire a Symfony di generare un'eccezione quando lo stato della risposta HTTP del server rientra nell'intervallo [3xx, 4xx, 5xx];
  • righe 5–7: se siamo in modalità [$verbose], visualizziamo la risposta del server sulla console;
  • righe 9–14: se lo stato della risposta HTTP del server non è 200, viene generata un'eccezione con la risposta JSON del server come messaggio di errore;
  • riga 16: la stringa JSON viene decodificata in un array;
  • riga 17: le informazioni utili si trovano in [$array["response"]];

23.14.2.2.3. Il metodo [authenticateUser]

Il metodo [authenticateUser] è il seguente:


public function authentifierUtilisateur(string $user, string $password): void {
    // create a HTTP customer
    $httpClient = HttpClient::create();
    // make a request to the server with authentication
    $response = $httpClient->request('POST', $this->urlServer,
      ["query" => [
          "action" => "authentifier-utilisateur"
        ],
        "body" => [
          "user" => $user,
          "password" => $password
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // the answer is retrieved
    $this->getResponse($response);
  }

Commenti

  • riga 5: la richiesta del client è un POST;
  • righe 6–8: parametri nell'URL;
  • righe 9–12: parametri POST;
  • riga 14: il cookie di sessione;
  • Riga 17: leggiamo la risposta. Sappiamo che se c'è un errore (un codice di stato HTTP diverso da 200), il metodo [getResponse] genera a sua volta un'eccezione;

23.14.2.2.4. Il metodo [calculateTax]

public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
    // create a HTTP customer
    $httpClient = HttpClient::create();
    // make the request to the server without authentication but with the session cookie
    $response = $httpClient->request('POST', $this->urlServer,
      ["query" => [
          "action" => "calculer-impot"],
        "body" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // the answer is retrieved
    $array = $this->getResponse($response);
    return (new Simulation())->setFromArrayOfAttributes($array);
  }

Commenti

  • righe 6–7: il singolo parametro URL;
  • righe 8–12: i tre parametri POST (riga 5);
  • riga 17: la risposta viene elaborata;
  • riga 18: se arriviamo a questo punto, significa che il metodo [getResponse] non ha generato un'eccezione. Restituiamo un oggetto [Simulation] inizializzato con l'array restituito da [getResponse];

23.14.2.2.5. Il metodo [listerSimulations]

public function listerSimulations(): array {
    // create a HTTP customer
    $httpClient = HttpClient::create();
    // make the request to the server without authentication but with the session cookie
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "action" => "lister-simulations"
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // the answer is retrieved
    return $this->getSimulations($response);
  }

Commenti

  • riga 5: metodo GET;
  • righe 6–8: l'unico parametro GET;
  • riga 13: il recupero delle simulazioni è gestito dal metodo privato [getSimulations];

23.14.2.2.6. Il metodo [getSimulations]

private function getSimulations(CurlResponse $response): array {
    // we retrieve the JSON response
    $array = $this->getResponse($response);
    // we have an array of associative objects
    // we'll turn it into an array of Simulation objects
    $simulations = [];
    foreach ($array as $simulation) {
      $simulations [] = (new Simulation())->setFromArrayOfAttributes($simulation);
    }
    // we render the Simulation object list
    return $simulations;
}

Commenti

  • riga 3: recuperiamo l'array dalla risposta. Si tratta di un array di array, ciascuno dei quali possiede tutti gli attributi di un oggetto [Simulation];
  • riga 6: se arriviamo a questo punto, significa che il metodo [getResponse] non ha generato un'eccezione;
  • righe 6–9: utilizziamo la risposta per creare un array di oggetti [Simulation];
  • riga 11: restituiamo questo array;

23.14.2.2.7. Il metodo [DeleteSimulation]

public function supprimerSimulation(int $numéro): array {
    // create a HTTP customer
    $httpClient = HttpClient::create();
    // make the request to the server without authentication but with the session cookie
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "action" => "supprimer-simulation",
          "numéro" => $numéro
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // the answer is retrieved
    return $this->getSimulations($response);
  }

Commenti

  • riga 5: viene effettuata una richiesta GET;
  • Righe 6–9: i due parametri URL;
  • riga 14: dopo una cancellazione, il server restituisce il nuovo array di simulazioni. Restituiamo questo array;

23.14.2.2.8. Il metodo [endSession]

Una sessione con il servizio web termina normalmente chiamando il metodo [finSession]:


public function finSession(): void {
    // create a HTTP customer
    $httpClient = HttpClient::create();
    // make the request to the server without authentication but with the session cookie
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "action" => "fin-session"
        ],
        "verify_peer" => false,
        "headers" => ["Cookie" => $this->sessionCookie]
    ]);
    // the answer is retrieved
    $this->getResponse($response);
  }

Commenti

  • riga 5: inviamo una richiesta GET;
  • righe 6–8: il singolo parametro URL;
  • riga 13: leggiamo la risposta. Verrà generata un'eccezione se il codice di stato HTTP della risposta non è 200;

23.14.3. Il livello [business]

Image

23.14.3.1. L'interfaccia

L'interfaccia per il livello [business] è la seguente [InterfaceClientMetier.php]:


<?php
 
// namespace
namespace Application;
 
interface InterfaceClientMetier {
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation;
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFilename, string $errorsFileName): void;
 
  // authentication
  public function authentifierUtilisateur(String $user, string $password): void;
 
  // list of simulations
  public function listerSimulations(): array;
 
  // recording results
  public function saveResults(string $resultsFilename, array $simulations): void;
 
  // delete a simulation
  public function supprimerSimulation(int $numéro): array;
 
  // start of session
  public function initSession(string $type = 'json'): void;
 
  // end of session
  public function finSession(): void;
}

Commenti

  • Solo il metodo [executeBatchImports] alla riga 12 è specifico del livello [business]. Tutti gli altri appartengono al livello [DAO], che li implementa;

23.14.3.2. La classe [ClientMetier]

La classe che implementa il livello [business] è la seguente:


<?php
 
namespace Application;
 
class ClientMetier implements InterfaceClientMetier {
  // attribute
  private $clientDao;
 
  // manufacturer
  public function __construct(InterfaceClientDao $clientDao) {
    $this->clientDao = $clientDao;
  }
 
  // tAX CALCULATION
  public function calculerImpot(string $marié, int $enfants, int $salaire): Simulation {
    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
    $simulations = [];
    // we exploit them
    foreach ($taxPayersData as $taxPayerData) {
      // tax calculation     
      $simulations [] = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
    }
    // recording results
    if ($resultsFileName !== NULL) {
      $this->clientDao->saveResults($resultsFileName, $simulations);
    }
  }
 
  public function authentifierUtilisateur(String $user, string $password): void {
    $this->clientDao->authentifierUtilisateur($user, $password);
  }
 
  public function listerSimulations(): array {
    return $this->clientDao->listerSimulations();
  }
 
  public function saveResults(string $resultsFilename, array $simulations): void {
    $this->clientDao->saveResults($resultsFilename, $simulations);
  }
 
  public function supprimerSimulation(int $numéro): array {
    return $this->clientDao->supprimerSimulation($numéro);
  }
 
  public function finSession(): void {
    $this->clientDao->finSession();
  }
 
  public function initSession(string $type = 'json'): void {
    $this->clientDao->initSession($type);
  }
 
}

Commenti

  • righe 10–12: Per essere costruito, il livello [business] necessita di un riferimento al livello [DAO];
  • righe 20–38: solo il metodo [executeBatchImports] è specifico del livello [business]. L'implementazione degli altri metodi delega il lavoro a metodi con lo stesso nome nel livello [DAO];
  • riga 23: chiamiamo il livello [dao] per recuperare i dati dei contribuenti in un array di oggetti [TaxPayerData];
  • riga 25: le varie simulazioni calcolate vengono accumulate nell'array [$simulations];
  • righe 27–33: calcoliamo l'imposta per ciascun contribuente nell'array [$taxPayersData];
  • righe 35–37: i risultati ottenuti nell'array [$simulations] vengono salvati in un file JSON;

Nota: il livello [business] non svolge praticamente alcuna funzione. Potremmo decidere di eliminarlo e consolidare tutto nel livello [DAO].

23.14.4. Lo script principale

Image

Lo script principale è configurato dal seguente file [config.json]:


{
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-12",
    "dependencies": [
        "/Entities/BaseEntity.php",
        "/Entities/TaxPayerData.php",
        "/Entities/Simulation.php",
        "/Entities/ExceptionImpots.php",
        "/Utilities/Utilitaires.php",
        "/Model/InterfaceClientDao.php",        
        "/Model/TraitDao.php",
        "/Model/ClientDao.php",
        "/Model/InterfaceClientMetier.php",
        "/Model/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-12/main.php"
}

Lo script principale [main.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.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"]);
// creation of the [business] layer
$clientMetier = new ClientMetier($clientDao);
 
// tax calculation in batch mode
try {
  // session initialization
  $clientMetier->initSession('json');
  // authentication
  $clientMetier->authentifierUtilisateur($config["user"]["login"], $config["user"]["passwd"]);
  // tax calculation without saving results
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, NULL, ERRORS_FILENAME);
  // list of simulations
  $clientMetier->listerSimulations();
  // deleting a simulation
  $simulations = $clientMetier->supprimerSimulation(1);
  // saving results
  $clientMetier->saveResults(RESULTS_FILENAME, $simulations);
  // end of session
  $clientMetier->finSession();
  // action without being authenticated - must crash
  $clientMetier->listerSimulations();
} catch (ExceptionImpots $ex) {
  // error is displayed
  print "Une erreur s'est produite : " . $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit();

Commenti

  • righe 12-16: elaborazione del file di configurazione [config.json];
  • righe 18–26: caricamento di tutte le dipendenze;
  • righe 28–34: definizione di costanti e alias;
  • righe 36–39: creazione dei livelli [dao] e [business];
  • riga 44: inizializzazione di una sessione JSON;
  • riga 46: autenticazione con il server;
  • riga 48: calcolo dell'imposta per una serie di contribuenti. I risultati non vengono salvati (2° parametro impostato su NULL);
  • riga 50: recupero dei risultati di tutti questi calcoli;
  • riga 52: eliminazione della simulazione n. 1 (la seconda nell'elenco);
  • riga 54: salvataggio delle simulazioni rimanenti;
  • riga 56: la sessione è terminata. Ciò significa che il cookie di sessione viene eliminato;
  • riga 58: richiediamo l'elenco delle simulazioni. Poiché il cookie di sessione è stato cancellato, è necessario eseguire nuovamente l'autenticazione. Dovremmo quindi ottenere un'eccezione che indica che non siamo autenticati;

Il file [taxpayersdata.json] è il seguente:


[
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 55555
    },
    {
        "marié": "ouix",
        "enfants": "2x",
        "salaire": "55555x"
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": 50000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 50000
    },
    {
        "marié": "non",
        "enfants": 2,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 5,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 30000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 200000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 20000
    }
]

Ci sono 12 contribuenti, di cui 1 non corretto. Questo porta a un totale di 11 simulazioni. Una di queste verrà eliminata. Ne dovrebbero rimanere 10.

Dopo aver eseguito lo script principale, il file JSON [results.json] appare così:


[
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": "55555",
        "impôt": 2814,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.14
    },
    {
        "marié": "oui",
        "enfants": "3",
        "salaire": "50000",
        "impôt": 0,
        "surcôte": 0,
        "décôte": 720,
        "réduction": 0,
        "taux": 0.14
    },
    {
        "marié": "non",
        "enfants": "2",
        "salaire": "100000",
        "impôt": 19884,
        "surcôte": 4480,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.41
    },
    {
        "marié": "non",
        "enfants": "3",
        "salaire": "100000",
        "impôt": 16782,
        "surcôte": 7176,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.41
    },
    {
        "marié": "oui",
        "enfants": "3",
        "salaire": "100000",
        "impôt": 9200,
        "surcôte": 2180,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.3
    },
    {
        "marié": "oui",
        "enfants": "5",
        "salaire": "100000",
        "impôt": 4230,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.14
    },
    {
        "marié": "non",
        "enfants": "0",
        "salaire": "100000",
        "impôt": 22986,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.41
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": "30000",
        "impôt": 0,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0
    },
    {
        "marié": "non",
        "enfants": "0",
        "salaire": "200000",
        "impôt": 64210,
        "surcôte": 7498,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.45
    },
    {
        "marié": "oui",
        "enfants": "3",
        "salaire": "20000",
        "impôt": 0,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0
    }
]

Ci sono effettivamente 10 simulazioni.

Il file JSON [errors.json] ha il seguente contenuto:


{
    "numéro": 1,
    "erreurs": [
        {
            "marié": "ouix"
        },
        {
            "enfants": "2x"
        },
        {
            "salaire": "55555x"
        }
    ]
}

L'output della console è il seguente (in modalità verbosa, le risposte JSON del server vengono visualizzate sulla console):


{"action":"init-session","état":700,"réponse":"session démarrée avec type [json]"}
{"action":"authentifier-utilisateur","état":200,"réponse":"Authentification réussie [admin, admin]"}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}}
{"action":"calculer-impot","état":300,"réponse":{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}}
{"action":"lister-simulations","état":500,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"50000","impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"supprimer-simulation","état":600,"réponse":[{"marié":"oui","enfants":"2","salaire":"55555","impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"50000","impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"2","salaire":"100000","impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"non","enfants":"3","salaire":"100000","impôt":16782,"surcôte":7176,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"100000","impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3,"arrayOfAttributes":null},{"marié":"oui","enfants":"5","salaire":"100000","impôt":4230,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"100000","impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41,"arrayOfAttributes":null},{"marié":"oui","enfants":"2","salaire":"30000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null},{"marié":"non","enfants":"0","salaire":"200000","impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45,"arrayOfAttributes":null},{"marié":"oui","enfants":"3","salaire":"20000","impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0,"arrayOfAttributes":null}]}
{"action":"fin-session","état":400,"réponse":"session supprimée"}
{"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Une erreur s'est produite : {"action":"lister-simulations","état":103,"réponse":["pas de session en cours. Commencer par action [init-session]"]}
Terminé

23.14.5. Test [Codeception]

Come per i client precedenti, il client versione 12 può essere testato utilizzando [Codeception]:

Image

Il codice della classe di test del livello [business] del client è simile a quello delle classi di test dei client precedenti:


<?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-12");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config.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";
}
// symfony dependencies
use Symfony\Component\HttpClient\HttpClient;
 
// test class
class ClientDaoTest extends \Codeception\Test\Unit {
  // dao layer
  private $clientDao;
 
  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"]);
    // creation of the [business] layer
    $this->métier = new ClientMetier($clientDao);
    // session initialization
    $this->métier->initSession("json");
    // authentication
    $this->métier->authentifierUtilisateur("admin", "admin");
  }
 
  // tests
  public function test1() {
    $simulation = $this->métier->calculerImpot("oui", 2, 55555);
    $this->assertEqualsWithDelta(2815, $simulation->getImpôt(), 1);
    $this->assertEqualsWithDelta(0, $simulation->getSurcôte(), 1);
    $this->assertEqualsWithDelta(0, $simulation->getDécôte(), 1);
    $this->assertEqualsWithDelta(0, $simulation->getRéduction(), 1);
    $this->assertEquals(0.14, $simulation->getTaux());
  }
 
  public function test2() {
    ….
  }
 

  public function test11() {

  }
 
}

Commenti

  • righe 34–46: si noti che il costruttore della classe di test viene eseguito prima di ogni test;
  • righe 38–41: costruzione dei livelli [dao] e [business];
  • righe 42–45: i metodi di test [test1…, test11] testano il metodo [calculateTax]. Per rendere ciò possibile, è necessario prima inizializzare una sessione JSON ed eseguire l'autenticazione;

I risultati del test sono i seguenti:

Image

Dovrebbero essere eseguiti molti altri test:

  • testare i vari metodi del livello [dao];
  • testare gli stati restituiti dal server web. Questi stati sono importanti perché il loro valore determina quale pagina HTML visualizzare;