Skip to content

3. Ein generischer Controller

3.1. Einführung

Bei der vorherigen Methode ging es darum, dass wir den Controller mit dem Namen main.php schreiben mussten. Mit etwas Erfahrung stellen wir fest, dass dieser Controller oft dieselben Aufgaben erfüllt, und es ist daher verlockend, einen generischen Controller zu schreiben, der in den meisten Webanwendungen verwendet werden kann. Der Code für diesen Controller könnte wie folgt aussehen:

<?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. Die Konfigurationsdatei der Anwendung

Die Anwendung wird in einem Skript konfiguriert, das den Namen config.php tragen muss. Die Anwendungseinstellungen werden in einem Dictionary namens $dConfig gespeichert, das vom Controller, den Aktionsskripten, den Modellen und den Basisansichten verwendet wird.

3.3. In den Controller einzubindende Bibliotheken

Die Bibliotheken, die in den Controller-Code eingebunden werden sollen, werden im Array $dConfig['includes'] abgelegt. Der Controller bindet sie mit dem folgenden Code-Schnipsel ein:

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

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

3.4. Sitzungsverwaltung

Der generische Controller verwaltet automatisch eine Sitzung. Er speichert und ruft Sitzungsinhalte über das $dSession-Wörterbuch ab. Dieses Wörterbuch kann Objekte enthalten, die serialisiert werden müssen, um später korrekt abgerufen werden zu können. Der mit diesem Wörterbuch verbundene Schlüssel lautet „session“. Daher erfolgt das Abrufen einer Sitzung mit dem folgenden Code:

<?php

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

Wenn eine Aktion Informationen in der Sitzung speichern muss, fügt sie Schlüssel und Werte zum $dSession-Wörterbuch hinzu. Da alle Aktionen dieselbe Sitzung nutzen, besteht die Gefahr von Konflikten bei den Sitzungsschlüsseln, wenn die Anwendung von mehreren Personen unabhängig voneinander entwickelt wird. Dies stellt eine Herausforderung dar. Wir müssen ein Repository entwickeln, das die Sitzungsschlüssel auflistet – ein Repository, das von allen gemeinsam genutzt wird. Wir werden sehen, dass jede Aktion mit einem Aufruf der folgenden finSession-Funktion endet:

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

Eine Aktion kann entscheiden, eine Sitzung nicht fortzusetzen. Dazu muss sie einfach keinen Wert an den Parameter $dSession der Funktion endSession übergeben; in diesem Fall wird die Sitzung gelöscht (session_destroy). Wenn das $dSession-Dictionary existiert, wird es in der Sitzung gespeichert, die anschließend geschrieben wird (session_write_close). Die aktuelle Aktion kann daher Elemente in der Sitzung speichern, indem sie Elemente zum $dSession-Dictionary hinzufügt. Beachten Sie, dass der Controller die Parameter der aktuellen Anfrage automatisch in der Sitzung speichert. Dadurch können sie bei Bedarf abgerufen werden, um die nächste Anfrage zu verarbeiten.

3.5. Senden der Antwort an den Client

Der eigentliche Zweck der Funktion finSession** besteht darin, eine Antwort an den Benutzer zu senden. Wir haben erwähnt, dass eine Antwort verschiedene Seitenvorlagen haben kann. Diese werden in $dConfig[&#x27;vuesResponse&#x27;]** konfiguriert. In einer Anwendung mit zwei Vorlagen könnten wir Folgendes haben:

<?php

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

Die aktuelle Aktion gibt die gewünschte Vorlage in $dResponse['responseView'] an. Der Controller zeigt diese mit folgender Anweisung an:

<?php

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

Sobald diese Antwort an den Client gesendet wurde, wird der Controller beendet (exit).

3.6. Ausführung von Aktionen

Der Controller wartet auf Anfragen mit dem Parameter action=XX. Wenn dieser Parameter in der Anfrage nicht vorhanden ist und es sich um eine GET-Anfrage handelt, nimmt die Aktion den Wert 'init' an. Dies ist bei der allerersten Anfrage an den Controller der Fall, die die Form http://machine:port/chemin/main.php hat.

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

Standardmäßig ist jede Aktion mit einem Skript verknüpft, das für die Ausführung dieser Aktion zuständig ist. Zum Beispiel:

<?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');          

Zwei Aktionen sind vordefiniert:

invalidSequence
Fälle, in denen die aktuelle Aktion nicht auf die vorherige Aktion folgen kann
Ungültige Aktion
Fälle, in denen die angeforderte Aktion nicht im Aktionswörterbuch vorhanden ist

Anwendungsspezifische Aktionen werden in der Form Methode:Aktion geschrieben, wobei Methode die GET- oder POST-Methode der Anfrage ist und Aktion die angeforderte Aktion, hier: init, calculateTax, returnForm, clearForm. Beachten Sie, dass die Aktion unabhängig davon, ob die Parameter über die GET- oder POST-Methode gesendet werden, anhand der folgenden Reihenfolge abgerufen wird:

<?php

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

Selbst wenn ein Formular per POST übermittelt wird, können wir dennoch schreiben:

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

Die Formularelemente werden gesendet (method='post'). Die angeforderte URL lautet jedoch main.php?action=calculerimpot. Die Parameter dieser URL werden aus dem Array $_GET abgerufen, während die anderen Formularelemente aus dem Array $_POST abgerufen werden.

Mithilfe des Aktionswörterbuchs führt der Controller die angeforderte Aktion wie folgt aus:

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

Wenn die angeforderte Aktion nicht im Aktionswörterbuch enthalten ist, wird das Skript ausgeführt, das einer ungültigen Aktion entspricht. Sobald das Aktionsskript in den Controller geladen wurde, wird es ausgeführt. Beachten Sie, dass es Zugriff auf die Controller-Variablen ($dConfig, $dSession) sowie auf die superglobalen Wörterbücher von PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION) hat. Das Skript enthält Anwendungslogik und Aufrufe an Geschäftsklassen. In allen Fällen muss die Aktion

  • das $dSession-Wörterbuch füllen, falls Elemente in der aktuellen Sitzung gespeichert werden müssen
  • in $dResponse['vuereponse'] den Namen der anzuzeigenden Antwortvorlage angeben
  • mit einem Aufruf von `finSession($dConfig, $dResponse, $dSession)` enden. Soll die Sitzung beendet werden, endet die Aktion einfach mit einem Aufruf von `finSession($dConfig, $dResponse)`.

Aus Gründen der Konsistenz kann die Aktion alle von den Ansichten benötigten Informationen in das $dResponse-Wörterbuch einfügen. Dies ist jedoch nicht erforderlich. Nur der Wert $dResponse['vuereponse'] ist unverzichtbar. Beachten Sie, dass jedes Aktionsskript mit einem Aufruf der Funktion finSession endet, die ihrerseits mit einer Beendigungsoperation endet. Daher gibt es keine Rückkehr aus einem Aktionsskript.

3.7. Die Abfolge von Aktionen

Eine Webanwendung kann als endliche Zustandsmaschine betrachtet werden. Die verschiedenen Zustände der Anwendung sind mit den dem Benutzer angezeigten Ansichten verknüpft. Der Benutzer kann über einen Link oder eine Schaltfläche zu einer anderen Ansicht navigieren. Die Webanwendung hat ihren Zustand geändert. Wir haben gesehen, dass eine Aktion durch eine Anfrage der Form http://machine:port/chemin/main.php?action=XX ausgelöst wird. Diese URL muss von einem Link stammen, der in der dem Benutzer angezeigten Ansicht enthalten ist. Wir möchten verhindern, dass ein Benutzer die URL http://machine:port/chemin/main.php?action=XX direkt eingibt und dadurch den von der Anwendung für ihn vorgesehenen Pfad umgeht. Dies gilt auch, wenn der Client ein Programm ist.

Ein Navigationspfad ist gültig, wenn die angeforderte URL von der zuletzt dem Benutzer angezeigten Ansicht aus erreichbar ist. Die Liste solcher URLs lässt sich leicht ermitteln. Sie besteht aus

  • den URLs, die in der Ansicht enthalten sind, entweder als Links oder als Ziele für Aktionen vom Typ „Submit“
  • URLs, die ein Benutzer direkt in seinen Browser eingeben darf, wenn die Ansicht angezeigt wird.

Die Liste der Anwendungszustände entspricht nicht unbedingt der Liste der Ansichten. Betrachten Sie beispielsweise die folgende einfache Ansicht, **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>

Diese Basisansicht wird in eine Zusammensetzung von Basisansichten integriert, die die Antwort bilden. In dieser Ansicht gibt es einen Link, der dynamisch positioniert werden kann. Die Ansicht errors.php kann dann je nach Situation mit n verschiedenen Links angezeigt werden. Dies führt zu n verschiedenen Zuständen für die Anwendung. Im Zustand #i wird die Ansicht errors.php mit dem Link lieni angezeigt. In diesem Zustand ist nur die Verwendung von lieni zulässig.

Die Liste der Zustände einer Anwendung und die möglichen Aktionen in jedem Zustand werden im Wörterbuch $dConfig['etats'] gespeichert:

<?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'));

Die obige Anwendung verfügt über zwei benannte Zustände: e-form und e-errors. Wir fügen einen Zustand namens stateless hinzu, der dem ersten Start der Anwendung entspricht, als sie noch keinen Zustand hatte. In einem Zustand E befindet sich die Liste der zulässigen Aktionen im Array $dConfig['states'][E]['allowedActions']. Sie gibt die zulässige Methode (get/post) für die Aktion und den Namen der Aktion an. Im obigen Beispiel gibt es vier mögliche Aktionen: get:init, post:calculateTax, get:returnForm und post:clearForm.

Mithilfe des Wörterbuchs $dConfig['etats'] kann der Controller feststellen, ob die aktuelle $sAction im aktuellen Zustand der Anwendung zulässig ist. Dieser Zustand wird von jeder Aktion aufgebaut und in der Sitzung unter $dSession['etat'] gespeichert. Der Controller-Code zur Überprüfung, ob die aktuelle Aktion zulässig ist, lautet wie folgt:

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

Die Logik ist wie folgt: Eine Aktion $sAction ist zulässig, wenn sie in der Liste $dConfig['states'][$state]['allowedActions'] enthalten ist, oder, falls diese Liste nicht existiert, in diesem Fall ist jede Aktion zulässig. $state ist der Zustand der Anwendung am Ende des vorherigen Zyklus aus Client-Anfrage und Server-Antwort. Dieser Zustand wurde in der Sitzung gespeichert und wird von dort abgerufen. Wird die angeforderte Aktion als ungültig erkannt, wird das Skript $dConfig['actions']['invalidSequence']['url'] ausgeführt. Dieses Skript sorgt dafür, dass eine entsprechende Antwort an den Client gesendet wird.

Während der Entwicklungsphase kann das $dConfig['etats')-Wörterbuch leer gelassen werden. In diesem Fall erlaubt jeder Zustand jede Aktion. Das Wörterbuch kann fertiggestellt werden, sobald die Anwendung vollständig debuggt wurde. Es schützt die Anwendung vor unbefugten Aktionen.

3.8. Debugging

Der Controller bietet zwei Debugging-Funktionen:

  • Die Trace-Funktion zeigt eine Meldung in der HTML-Ausgabe an
  • Die Dump-Funktion zeigt den Inhalt eines Wörterbuchs im selben Stream an

Jedes Action-Skript kann diese beiden Funktionen nutzen. Da der Code des Action-Skripts in den Code des Controllers eingebunden ist, sind die Trace- und Dump-Funktionen für die Skripte sichtbar.

3.9. Fazit

Der generische Controller ist so konzipiert, dass sich der Entwickler auf die Aktionen und Ansichten seiner Anwendung konzentrieren kann. Er übernimmt für ihn folgende Aufgaben:

  • Sitzungsverwaltung (Wiederherstellung, Speicherung)
  • Validierung der angeforderten Aktionen
  • Ausführung des mit der Aktion verbundenen Skripts
  • Senden einer dem Ergebnis der Aktionsausführung entsprechenden Antwort an den Client