Skip to content

3. A generic controller

3.1. Introduction

In the previous method, it was understood that we had to write the controller named main.php. With a little experience, one realizes that this controller often performs the same tasks, and it is therefore tempting to write a generic controller that can be used in most web applications. The code for this controller could be as follows:

<?php
     // generic controller

  // reading config
  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 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      

  //--------------------------------------------------------------------
    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. The application configuration file

The application is configured in a script that must be named config.php. The application settings are stored in a dictionary named $dConfig, which is used by the controller, action scripts, models, and basic views.

3.3. Libraries to be included in the controller

The libraries to be included in the controller code are placed in the array $dConfig['includes']. The controller includes them with the following code snippet:

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

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

3.4. Session Management

The generic controller automatically manages a session. It saves and retrieves session content via the $dSession dictionary. This dictionary may contain objects that must be serialized in order to be retrieved correctly later. The key associated with this dictionary is 'session'. To retrieve a session, use the following code:

<?php

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

If an action wants to store information in the session, it will add keys and values to the $dSession dictionary. Since all actions share the same session, there is a risk of session key conflicts if the application is developed independently by multiple people. This is a challenge. A repository listing the session keys must be developed, a repository shared by all. We will see that each action ends with a call to the following finSession function:

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

An action may decide not to continue a session. To do so, it simply needs to not pass a value to the $dSession parameter of the finSession function, in which case the session is deleted (session_destroy). If the dictionary $dSession exists, it is saved in the session, which is then recorded (session_write_close). The current action can therefore store elements in the session by adding elements to the $dSession dictionary. Note that the controller automatically stores the parameters of the current request in the session. This allows them to be retrieved if needed to process the next request.

3.5. Sending the response to the client

The ultimate purpose of the finSession function is to send a response to the user. We mentioned that a response can have different page templates. These are configured in $dConfig['vuesReponse']. In a two-template application, we might have:

<?php

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

The current action specifies the desired template in $dReponse['vuereponse']. The controller displays this template using the following instruction:

<?php

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

Once this response is sent to the client, the controller stops (exit).

3.6. Execution of actions

The controller waits for requests containing the parameter action=XX. If this parameter does not exist in the request and the request is in the form GET, the action takes the value 'init'. This is the case for the very first request made to the controller, which is in the form http://machine:port/path/main.php.

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

By default, each action is associated with a script responsible for handling that action. For example:

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

Two actions are predefined:

enchainementInvalide
cases where the current action cannot follow the previous action
actionInvalide
when the requested action does not exist in the action dictionary

Application-specific actions are denoted in the form method:action, where method is the get or post method of the request and action is the requested action, here: init, calculateTax, returnForm, clearForm. Note that the action is retrieved, regardless of whether the method used to send the parameters is GET or POST, using the following sequence:

<?php

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

In fact, even if a form is submitted via POST, you can still write:

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

The form elements will be posted (method='post'). However, the requested url will be main.php?action=calculerimpot. The parameters of this URL will be retrieved from the $_GET dictionary, while the other form elements will be retrieved from the $_POST dictionary.

Using the action dictionary, the controller executes the requested action as follows:

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

If the requested action is not in the action dictionary, the script corresponding to an invalid action will be executed. Once the action script is loaded into the controller, it runs. Note that it has access to the controller’s variables ($dConfig, $dSession) as well as the superglobal dictionaries of PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). The script contains application logic and calls to business classes. In all cases, the action must

  • populate the $dSession dictionary if elements need to be saved in the current session
  • specify in $dReponse['vuereponse'] the name of the response template to be displayed
  • end with a call to finSession($dConfig, $dReponse, $dSession). If the session must be destroyed, the action will simply end with a call to finSession($dConfig, $dReponse).

For consistency, the action may place all the information needed for the views into the $dReponse dictionary. However, this is not required. Only the value $dReponse['vuereponse'] is essential. Note that every action script ends with a call to the finSession function, which itself ends with an exit operation. Therefore, there is no return from an action script.

3.7. The sequence of actions

A web application can be viewed as a finite-state machine. The application’s various states correspond to the views presented to the user. The user can navigate to a different view by clicking a link or a button. The web application has changed states. We have seen that an action is initiated by a request of the form http://machine:port/path/main.php?action=XX. This URL must come from a link contained in the view presented to the user. We want to prevent a user from directly typing URL http://machine:port/path/main.php?action=XX, thereby bypassing the path the application has defined for it. This also applies if the client is a program.

A navigation sequence will be valid if the requested URL is a URL that can be requested from the last view presented to the user. The list of these is easy to determine. It consists

  • the URLs contained in the view, either as links or as action targets of the submit type
  • URL that a user is authorized to type directly into their browser when the view is displayed.

The list of application states is not necessarily the same as the list of views. Consider, for example, the following basic view errors.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>

This basic view will be integrated into a composition of basic views that will form the response. On this view, there is a link that can be positioned dynamically. The errors.php view can then be displayed with n different links depending on the circumstances. This will result in n different states for the application. In state #i, the errors.php view will be displayed with the lieni link. In this state, only the use of lieni is permitted.

The list of an application’s states and the possible actions in each state will be recorded in the dictionary $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'));

The application above has two named states: e-form and e-errors. We add a state called no-state, which corresponds to the initial startup of the application when it had no state. In a state E, the list of authorized actions is found in the array $dConfig['etats'][E]['actionsautorisees']. It specifies the method (get/post) authorized for the action and the action’s name. In the example above, there are four possible actions: get:init, post:calculateTax, get:returnForm, and post:clearForm.

Using the dictionary $dConfig['etats'], the controller can determine whether the current action $sAction is permitted in the application’s current state. This is built by each action and stored in the session in $dSession['etat']. The controller code to check whether the current action is allowed or not is as follows:

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

The logic is as follows: an action $sAction is allowed if it is in the list $dConfig['etats'][$etat]['actionsautorisees'] or if this list does not exist , in which case any action is permitted. $etat is the application state at the end of the previous client request/server response cycle. This state was stored in the session and is retrieved from there. If it is discovered that the requested action is illegal, the script $dConfig['actions']['enchainementInvalide']['url'] is executed. This script will be responsible for sending an appropriate response to the client.

During the development phase, the $dConfig['etats'] dictionary may not be populated. In this case, any status allows any action. The dictionary can be finalized once the application has been fully debugged. It will protect the application from unauthorized actions.

3.8. Debugging

The controller offers two debugging functions:

  • the trace function displays a message in the HTML stream
  • the dump function displays the contents of a dictionary in the same stream

Any action script can use these two functions. Since the action script code is included in the controller code, the trace and dump functions will be visible to the scripts.

3.9. Conclusion

The generic controller is designed to allow the developer to focus on the actions and views of their application. It handles the following for them:

  • session management (restoration, saving)
  • validation of requested actions
  • execution of the script associated with the action
  • sending the client a response appropriate to the result of the action’s execution