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, we realize that this controller often does the same things, 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

  // read config
  include 'config.php';

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

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

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

    // Is the sequence of actions normal?
  if( ! enchainementOK($dConfig,$dSession,$sAction)){  
    // invalid sequence
    $sAction = 'invalidSequence';
  }//if

    // Process the action
  $scriptAction = $dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['invalidAction']['url'];
  include $scriptAction;

  // send the response (view) to the client
  $sState = $dSession['state']['main'];
  $scriptView = $dConfig['states'][$sState]['view'];
  include $scriptView;

  // end of script - we shouldn't get here unless there's a bug
  trace("Configuration error.");
  trace("Action=[$sAction]");
  trace("scriptAction=[$scriptAction]");
  trace("Status=[$sStatus]");
  trace("scriptView=[$scriptView]");
  trace ("Verify that the scripts exist and that the [$scriptVue] script ends with a call to endSession.");
  exit(0);

  // ---------------------------------------------------------------
  function endSession(&$dConfig,&$dResponse,&$dSession){
    // $dConfig: configuration dictionary
      // $dSession: dictionary containing session information
        // $dResponse: dictionary of response page arguments

    // session check
    if(isset($dSession)){
      // store the request parameters in the session
      $dSession['request'] = 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();
    }

        // display the response
        include $dConfig['responseViews'][$dResponse['responseView']]['url'];

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

  //--------------------------------------------------------------------
    function chainOK(&$dConfig,&$dSession,$sAction){
      // checks if the current action is allowed based on the previous state
    $state = $dSession['state']['main'];
    if(! isset($state)) $state='noState';

    // action check
    $allowedActions = $dConfig['states'][$state]['allowedActions'];
    $authorized = ! isset($allowedActions) || in_array($sAction, $allowedActions);
        return $authorized;    
  }

  //--------------------------------------------------------------------
  function dump($dInfo){
      // displays a dictionary of information
    while(list($key, $value) = each($dInfo)){
        echo "[$key,$value]<br>\n";
    }//while
  }//end

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

?>

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 called $dConfig, which is used by the controller, action scripts, models, and basic views.

3.3. Libraries to include in the controller

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

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

  // include 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'. Therefore, retrieving a session is done with the following code:

<?php

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

If an action needs 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. We need to develop a repository listing the session keys, a repository shared by everyone. We will see that each action ends with a call to the following finSession function:

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

    // session check
    if(isset($dSession)){
      // store the request parameters in the session
      $dSession['request'] = 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();
    }

        // display the response
        include $dConfig['responseViews'][$dResponse['responseView']]['url'];

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

An action may decide not to continue a session. To do so, it simply needs to pass no value to the $dSession parameter of the endSession function, in which case the session is destroyed (session_destroy). If the $dSession dictionary exists, it is saved in the session, which is then written (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['responseViews']['template1']=array('url'=>'m-template1.php');
  $dConfig['responseViews']['template2']=array('url'=>'m-template2.php');

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

<?php

        // display the response
        include $dConfig['responseViews'][$dResponse['responseView']]['url'];

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

3.6. Execution of actions

The controller waits for requests with an action=XX parameter. If this parameter does not exist in the request and the request is a 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/chemin/main.php.

<?php
..
  // retrieve the action to be performed
  $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:calculateTax']=array('url'=>'a-calculateTax.php');
  $dConfig['actions']['get:returnForm'] = array('url' => 'a-returnForm.php');
  $dConfig['actions']['post:clearform']=array('url'=>'a-init.php');
  $dConfig['actions']['invalidSequence']=array('url'=>'a-invalidSequence.php');
  $dConfig['actions']['invalidAction'] = array('url' => 'a-invalidAction.php');          

Two actions are predefined:

invalidSequence
cases where the current action cannot follow the previous action
invalidAction
cases where the requested action does not exist in the action dictionary

Application-specific actions are written 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 parameters are sent via the GET or POST method, using the sequence:

<?php

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

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

<form method='post' action='main.php?action=calculateTax'>
..
</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 array, while the other form elements will be retrieved from the $_POST array.

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

<?php
...
    // processing the action
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url']: 
    $dConfig['actions']['invalidAction']['url'];
  include $scriptAction;

If the requested action is not in the actions 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 variables ($dConfig, $dSession) as well as PHP’s superglobal dictionaries ($_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 any elements need to be saved in the current session
  • specify in $dResponse['vuereponse'] the name of the response template to display
  • end with a call to `finSession($dConfig, $dReponse, $dSession)`. If the session is to be destroyed, the action will simply end with a call to `finSession($dConfig, $dReponse)`.

For consistency, the action may place all the information needed by the views into the $dReponse dictionary. But 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 are associated with the views presented to the user. The user can navigate to another view via a link or a button. The web application has changed state. We have seen that an action is initiated by a request of the form http://machine:port/chemin/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 typing the URL http://machine:port/chemin/main.php?action=XX directly, thereby bypassing the path the application has planned for them. This is also true if the client is a program.

A navigation path is valid if the requested URL is one that can be reached from the last view presented to the user. The list of such URLs is easy to determine. It consists of

  • the URLs contained in the view, either as links or as targets for submit-type actions
  • URLs 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 simple view, **erreurs.php**:

The following errors have occurred:
<ul>
    <?php
        for($i=0;$i<count($dResponse["errors"]);$i++){
            echo "<li class='error'>".$dResponse["errors"][$i]."</li>\n";
        }//for
    ?>
</ul>
<div class="info"><?php echo $dReponse["info"] ?></div>
<a href="<?php echo $dResponse["href"] ?>"><?php echo $dResponse["link"] ?></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 link lieni. 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 stored in the $dConfig['etats'] dictionary:

<?php
...  
// configuration of application states
  $dConfig['states']['e-form']=array(
       'allowedActions'=>array('post:calculateTax','get:init','post:clearForm'),
    'view'=>'e-form2.php');
  $dConfig['states']['e-errors'] = array(
      'allowedActions'=>array('get:returnForm','get:init'),
      'view'=>'e-errors2.php');
  $dConfig['states']['no-state']=array('allowed-actions'=>array('get:init'));

The application above has two named states: e-form and e-errors. We add a state called stateless, which corresponds to the initial startup of the application when it had no state. In a state E, the list of allowed actions is found in the array $dConfig['states'][E]['allowedActions']. It specifies the allowed method (get/post) 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 $dConfig['etats'] dictionary, the controller can determine whether the current $sAction is allowed in the application’s current state. This state is built by each action and stored in the session in $dSession['etat']. The controller code to check whether the current action is allowed is as follows:

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

    // Process the action
  $scriptAction = $dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['invalidAction']['url'];
  include $scriptAction;
..........
  //--------------------------------------------------------------------
    function chainOK(&$dConfig,&$dSession,$sAction){
      // Checks whether the current action is permitted based on the previous state
    $state = $dSession['state']['main'];
    if(!isset($state)) $state='noState';

    // action check
    $allowedActions = $dConfig['states'][$state]['allowedActions'];
    $authorized = ! isset($allowedActions) || in_array($sAction, $allowedActions);
        return $authorized;    
  }

The logic is as follows: an action $sAction is allowed if it is in the list $dConfig['states'][$state]['allowedActions'], or if this list does not exist, in which case any action is allowed. $state is the application's 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 the requested action is found to be invalid, the script $dConfig['actions']['invalidSequence']['url'] is executed. This script will handle sending an appropriate response to the client.

During the development phase, the $dConfig['etats'] dictionary may be left empty. In this case, any state 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 output
  • 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’s code is included in the controller’s 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