Skip to content

3. Un controller generico

3.1. Introduzione

Nel metodo precedente, era chiaro che dovevamo scrivere il controller denominato main.php. Con un po' di esperienza, ci rendiamo conto che questo controller spesso fa le stesse cose, ed è quindi allettante scrivere un controller generico che possa essere utilizzato nella maggior parte delle applicazioni web. Il codice per questo controller potrebbe essere il seguente:

<?php
     // generic controller

   // configurable reading
  include 'config.php';

   // including libraries
  for($i=0;$i<count($dConfig['includes']);$i++){
      include($dConfig['includes'][$i]);
  }//for  

   // start or resume session
  session_start();
  $dSession=$_SESSION["session"];
  if($dSession) $dSession=unserialize($dSession);

   // retrieve the action to be taken
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
  $sAction=strtolower($_SERVER['REQUEST_METHOD']).":$sAction";

     // is the sequence of actions normal?
  if( ! enchainementOK($dConfig,$dSession,$sAction)){  
     // abnormal sequence
    $sAction='enchainementinvalide';
  }//if

     // share processing
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['actionInvalide']['url'];
  include $scriptAction;

   // send response(view) to customer
  $sEtat=$dSession['etat']['principal'];
  $scriptVue=$dConfig['etats'][$sEtat]['vue'];
  include $scriptVue;

   // end of script - we shouldn't get there unless there's a bug
  trace ("Erreur de configuration.");
  trace("Action=[$sAction]");
  trace("scriptAction=[$scriptAction]");
  trace("Etat=[$sEtat]");
  trace("scriptVue=[$scriptVue]");
  trace ("Vérifiez que les script existent et que le script [$scriptVue] se termine par l'appel à finSession.");
  exit(0);

  // ---------------------------------------------------------------
  function finSession(&$dConfig,&$dReponse,&$dSession){
     // $dConfig: configuration dictionary
       // $dSession: dictionary containing session info
         // $dReponse: the dictionary of arguments for the response page

     // session registration
    if(isset($dSession)){
      // put the query parameters in the session
      $dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
          strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
        $_SESSION['session']=serialize($dSession);
      session_write_close();
    }else{    
         // no session
      session_destroy();
    }

         // we present the answer
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

     // end of script
    exit(0);
  }//endsession      

  //--------------------------------------------------------------------
    function enchainementOK(&$dConfig,&$dSession,$sAction){
       // checks whether the current action is authorized with respect to the previous state
    $etat=$dSession['etat']['principal'];
    if(! isset($etat)) $etat='sansetat';

     // check action
    $actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
    $autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
        return $autorise;    
  }

  //--------------------------------------------------------------------
  function dump($dInfos){
       // displays an information dictionary
    while(list($clé,$valeur)=each($dInfos)){
        echo "[$clé,$valeur]<br>\n";
    }//while
  }//follow-up

  //--------------------------------------------------------------------
  function trace($msg){
      echo $msg."<br>\n";
  }//follow-up

?>

3.2. Il file di configurazione dell'applicazione

L'applicazione è configurata in uno script che deve essere denominato config.php. Le impostazioni dell'applicazione sono memorizzate in un dizionario chiamato $dConfig, utilizzato dal controller, dagli script di azione, dai modelli e dalle viste di base.

3.3. Librerie da includere nel controller

Le librerie da includere nel codice del controller sono collocate nell'array $dConfig['includes']. Il controller le include con il seguente frammento di codice:

<?php
...
   // configurable reading
  include "config.php";

   // including libraries
  for($i=0;$i<count($dConfig['includes']);$i++){
      include($dConfig['includes'][$i]);
  }//for  

3.4. Gestione delle sessioni

Il controller generico gestisce automaticamente una sessione. Salva e recupera il contenuto della sessione tramite il dizionario $dSession. Questo dizionario può contenere oggetti che devono essere serializzati per poter essere recuperati correttamente in seguito. La chiave associata a questo dizionario è 'session'. Pertanto, il recupero di una sessione viene effettuato con il seguente codice:

<?php

   // start or resume session
  session_start();
  $dSession=$_SESSION["session"];
  if($dSession) $dSession=unserialize($dSession);

Se un'azione deve memorizzare informazioni nella sessione, aggiungerà chiavi e valori al dizionario $dSession. Poiché tutte le azioni condividono la stessa sessione, esiste il rischio di conflitti tra le chiavi di sessione se l'applicazione viene sviluppata in modo indipendente da più persone. Questa è una sfida. Dobbiamo sviluppare un repository che elenchi le chiavi di sessione, un repository condiviso da tutti. Vedremo che ogni azione termina con una chiamata alla seguente funzione finSession:

<?php
... 
 // ---------------------------------------------------------------
  function finSession(&$dConfig,&$dReponse,&$dSession){
     // $dConfig: configuration dictionary
       // $dSession: dictionary containing session information
         // $dReponse: the dictionary of arguments for the response page

     // session registration
    if(isset($dSession)){
      // put the query parameters in the session
      $dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
          strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
        $_SESSION['session']=serialize($dSession);
      session_write_close();
    }else{    
         // no session
      session_destroy();
    }

         // we present the answer
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

     // end of script
    exit(0);
  }//endsession      

Un'azione può decidere di non continuare una sessione. Per farlo, è sufficiente non passare alcun valore al parametro $dSession della funzione endSession; in tal caso, la sessione viene distrutta (session_destroy). Se il dizionario $dSession esiste, viene salvato nella sessione, che viene poi scritta (session_write_close). L'azione corrente può quindi memorizzare elementi nella sessione aggiungendoli al dizionario $dSession. Si noti che il controller memorizza automaticamente i parametri della richiesta corrente nella sessione. Ciò consente di recuperarli se necessario per elaborare la richiesta successiva.

3.5. Invio della risposta al client

Lo scopo finale della funzione finSession è quello di inviare una risposta all'utente. Abbiamo accennato al fatto che una risposta può avere diversi modelli di pagina. Questi sono configurati in $dConfig['vuesReponse']. In un'applicazione a due modelli, potremmo avere:

<?php

  $dConfig['vuesReponse']['modele1']=array('url'=>'m-modele1.php');
  $dConfig['vuesReponse']['modele2']=array('url'=>'m-modele2.php');

L'azione corrente specifica il modello desiderato in $dResponse['responseView']. Il controller lo visualizza utilizzando la seguente istruzione:

<?php

         // we present the answer
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

Una volta inviata questa risposta al client, il controller si arresta (exit).

3.6. Esecuzione delle azioni

Il controller attende richieste con un parametro action=XX. Se questo parametro non esiste nella richiesta e la richiesta è di tipo GET, l'azione assume il valore 'init'. Questo è il caso della primissima richiesta effettuata al controller, che ha la forma http://machine:port/chemin/main.php.

<?php
..
   // retrieve the action to be taken
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';

Per impostazione predefinita, ogni azione è associata a uno script responsabile della gestione di tale azione. Ad esempio:

<?php
... 
  // configuration of application actions
  $dConfig['actions']['get:init']=array('url'=>'a-init.php');  
  $dConfig['actions']['post:calculerimpot']=array('url'=>'a-calculimpot.php');
  $dConfig['actions']['get:retourformulaire']=array('url'=>'a-retourformulaire.php');
  $dConfig['actions']['post:effacerformulaire']=array('url'=>'a-init.php');
  $dConfig['actions']['enchainementinvalide']=array('url'=>'a-enchainementinvalide.php');
  $dConfig['actions']['actionInvalide']=array('url'=>'a-actioninvalide.php');          

Sono predefinite due azioni:

invalidSequence
casi in cui l'azione corrente non può seguire l'azione precedente
azione non valida
casi in cui l'azione richiesta non esiste nel dizionario delle azioni

Le azioni specifiche dell'applicazione sono scritte nella forma metodo:azione, dove metodo è il metodo GET o POST della richiesta e azione è l'azione richiesta, in questo caso: init, calculateTax, returnForm, clearForm. Si noti che l'azione viene recuperata, indipendentemente dal fatto che i parametri siano inviati tramite il metodo GET o POST, utilizzando la sequenza:

<?php

   // retrieve the action to be taken
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init'; 

Infatti, anche se un modulo viene inviato tramite POST, possiamo comunque scrivere:

<form method='post' action='main.php?action=calculerimpot'>
..
</form>

Gli elementi del modulo verranno inviati (method='post'). Tuttavia, l'URL richiesto sarà main.php?action=calculerimpot. I parametri di questo URL verranno recuperati dall'array $_GET, mentre gli altri elementi del modulo verranno recuperati dall'array $_POST.

Utilizzando il dizionario delle azioni, il controller esegue l'azione richiesta come segue:

<?php
...
    // share processing
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['actionInvalide']['url'];
  include $scriptAction;

Se l'azione richiesta non è presente nel dizionario delle azioni, verrà eseguito lo script corrispondente a un'azione non valida. Una volta caricato nel controller, lo script dell'azione viene eseguito. Si noti che ha accesso alle variabili del controller ($dConfig, $dSession) e ai dizionari superglobali di PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). Lo script contiene la logica dell'applicazione e le chiamate alle classi di business. In tutti i casi, l'azione deve

  • compilare il dizionario $dSession se è necessario salvare degli elementi nella sessione corrente
  • specificare in $dResponse['vuereponse'] il nome del modello di risposta da visualizzare
  • terminare con una chiamata a `finSession($dConfig, $dResponse, $dSession)`. Se la sessione deve essere distrutta, l'azione terminerà semplicemente con una chiamata a `finSession($dConfig, $dResponse)`.

Per coerenza, l'azione può inserire tutte le informazioni necessarie alle viste nel dizionario $dReponse. Ma ciò non è obbligatorio. Solo il valore $dReponse['vuereponse'] è essenziale. Si noti che ogni script di azione termina con una chiamata alla funzione finSession, che a sua volta termina con un'operazione di uscita. Pertanto, non c'è ritorno da uno script di azione.

3.7. La sequenza delle azioni

Un'applicazione web può essere vista come una macchina a stati finiti. I vari stati dell'applicazione sono associati alle viste presentate all'utente. L'utente può navigare verso un'altra vista tramite un link o un pulsante. L'applicazione web ha cambiato stato. Abbiamo visto che un'azione viene avviata da una richiesta del tipo http://machine:port/chemin/main.php?action=XX. Questo URL deve provenire da un link contenuto nella vista presentata all'utente. Vogliamo impedire a un utente di digitare direttamente l'URL http://machine:port/chemin/main.php?action=XX, aggirando così il percorso che l'applicazione ha pianificato per lui. Questo vale anche se il client è un programma.

Un percorso di navigazione è valido se l'URL richiesto è raggiungibile dall'ultima vista presentata all'utente. L'elenco di tali URL è facile da determinare. Consiste negli

  • gli URL contenuti nella vista, sia come link che come destinazioni per azioni di tipo submit
  • gli URL che un utente è autorizzato a digitare direttamente nel proprio browser quando la vista è visualizzata.

L'elenco degli stati dell'applicazione non è necessariamente lo stesso dell'elenco delle viste. Si consideri, ad esempio, la seguente semplice vista, **erreurs.php**:

Les erreurs suivantes se sont produites :
<ul>
    <?php
        for($i=0;$i<count($dReponse["erreurs"]);$i++){
            echo "<li class='erreur'>".$dReponse["erreurs"][$i]."</li>\n";
        }//for
    ?>
</ul>
<div class="info"><?php echo $dReponse["info"] ?></div>
<a href="<?php echo $dReponse["href"] ?>"><?php echo $dReponse["lien"] ?></a>

Questa vista di base verrà integrata in una composizione di viste di base che formeranno la risposta. In questa vista, è presente un link che può essere posizionato dinamicamente. La vista errors.php potrà quindi essere visualizzata con n link diversi a seconda delle circostanze. Ciò comporterà n stati diversi per l'applicazione. Nello stato #i, la vista errors.php verrà visualizzata con il link lieni. In questo stato, è consentito solo l'uso di lieni.

L'elenco degli stati di un'applicazione e le azioni possibili in ciascuno stato saranno memorizzati nel dizionario $dConfig['etats']:

<?php
...  
// application status configuration
  $dConfig['etats']['e-formulaire']=array(
       'actionsautorisees'=>array('post:calculerimpot','get:init','post:effacerformulaire'),
    'vue'=>'e-formulaire2.php');
  $dConfig['etats']['e-erreurs']=array(
      'actionsautorisees'=>array('get:retourformulaire','get:init'),
      'vue'=>'e-erreurs2.php');
  $dConfig['etats']['sansetat']=array('actionsautorisees'=>array('get:init'));

L'applicazione sopra riportata presenta due stati denominati: e-form ed e-errors. Aggiungiamo uno stato denominato stateless, che corrisponde all'avvio iniziale dell'applicazione quando non aveva alcuno stato. In uno stato E, l'elenco delle azioni consentite si trova nell'array $dConfig['states'][E]['allowedActions']. Esso specifica il metodo consentito (get/post) per l'azione e il nome dell'azione. Nell'esempio sopra riportato, ci sono quattro azioni possibili: get:init, post:calculateTax, get:returnForm e post:clearForm.

Utilizzando il dizionario $dConfig['etats'], il controller può determinare se l'attuale $sAction è consentita nello stato corrente dell'applicazione. Questo stato viene costruito da ciascuna azione e memorizzato nella sessione in $dSession['etat']. Il codice del controller per verificare se l'azione corrente è consentita è il seguente:

<?php
.....
     // is the sequence of actions normal?
  if( ! enchainementOK($dConfig,$dSession,$sAction)){  
     // abnormal sequence
    $sAction='enchainementinvalide';
  }//if

     // share processing
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['actionInvalide']['url'];
  include $scriptAction;
..........
  //--------------------------------------------------------------------
    function enchainementOK(&$dConfig,&$dSession,$sAction){
       // checks whether the current action is authorized with respect to the previous state
    $etat=$dSession['etat']['principal'];
    if(! isset($etat)) $etat='sansetat';

     // check action
    $actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
    $autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
        return $autorise;    
  }

La logica è la seguente: un'azione $sAction è consentita se è presente nell'elenco $dConfig['states'][$state]['allowedActions'], oppure, se tale elenco non esiste, in tal caso qualsiasi azione è consentita. $state è lo stato dell'applicazione al termine del precedente ciclo richiesta client/risposta server. Questo stato è stato memorizzato nella sessione e viene recuperato da lì. Se l'azione richiesta risulta non valida, viene eseguito lo script $dConfig['actions']['invalidSequence']['url']. Questo script si occuperà di inviare una risposta appropriata al client.

Durante la fase di sviluppo, il dizionario $dConfig['etats'] può essere lasciato vuoto. In questo caso, qualsiasi stato consente qualsiasi azione. Il dizionario può essere completato una volta che l'applicazione è stata completamente sottoposta a debug. Proteggerà l'applicazione da azioni non autorizzate.

3.8. Debug

Il controller offre due funzioni di debug:

  • la funzione trace visualizza un messaggio nell'output HTML
  • la funzione dump visualizza il contenuto di un dizionario nello stesso flusso

Qualsiasi script di azione può utilizzare queste due funzioni. Poiché il codice dello script di azione è incluso nel codice del controller, le funzioni trace e dump saranno visibili agli script.

3.9. Conclusione

Il controller generico è progettato per consentire allo sviluppatore di concentrarsi sulle azioni e sulle viste della propria applicazione. Si occupa per loro conto di quanto segue:

  • gestione della sessione (ripristino, salvataggio)
  • convalida delle azioni richieste
  • esecuzione dello script associato all'azione
  • invio al client di una risposta adeguata al risultato dell'esecuzione dell'azione