Skip to content

3. 通用控制器

3.1. 简介

在前一种方法中,我们已经了解到需要编写名为 main.php 的控制器。随着经验的积累,我们会发现该控制器往往执行相同的功能,因此很想编写一个通用控制器,以便在大多数 Web 应用程序中使用。该控制器的代码可能如下所示:

<?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. 应用程序配置文件

应用程序的配置存储在一个必须命名为 config.php 的脚本中。应用程序设置保存在一个名为 $dConfig 的字典中,该字典被控制器、操作脚本、模型和基本视图所使用。

3.3. 控制器中需包含的库

控制器代码中需包含的库文件存放在 $dConfig['includes'] 数组中。控制器通过以下代码片段包含这些库:

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

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

3.4. 会话管理

通用控制器会自动管理会话。它通过 $dSession 字典保存和检索会话内容。该字典可能包含必须进行序列化的对象,以便日后正确检索。该字典的键名为 'session'。因此,检索会话需使用以下代码:

<?php

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

如果某个操作需要将信息存储在会话中,它会向 $dSession 字典中添加键值对。由于所有操作共享同一个会话,如果应用程序由多人独立开发,则存在会话键冲突的风险。这是一个挑战。我们需要开发一个列出会话键的存储库,供所有人共享。我们将看到,每个操作都会以调用以下 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      

某个操作可能决定不继续当前会话。要实现这一点,只需在 endSession 函数中不向 $dSession 参数传递任何值,此时会话将被销毁(session_destroy)。如果 $dSession 字典存在,它将被保存到会话中,随后会话会被写入(session_write_close)。 因此,当前操作可以通过向 $dSession 字典添加元素来将数据存储在会话中。请注意,控制器会自动将会话参数存储在会话中。这使得在处理下一个请求时,如有需要,可以检索这些参数。

3.5. 向客户端发送响应

finSession 函数的最终目的是向用户发送响应。我们提到响应可以使用不同的页面模板,这些模板在 $dConfig['vuesResponse'] 中进行配置。在包含两个模板的应用程序中,我们可能会有:

<?php

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

当前操作在 $dResponse['responseView'] 中指定了所需的模板。控制器使用以下指令显示该模板:

<?php

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

一旦此响应发送给客户端,控制器即停止(退出)。

3.6. 操作的执行

控制器等待包含 action=XX 参数的请求。如果请求中不存在该参数且请求为 GET 请求,则 action 取值为 'init'。这是对控制器发出的第一个请求的情况,其形式为 http://machine:port/chemin/main.php

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

默认情况下,每个操作都关联有一个负责处理该操作的脚本。例如:

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

预定义了两个操作:

invalidSequence
指当前操作无法紧跟上一个操作的情况
无效操作
请求的操作在操作字典中不存在的情况

应用程序特有的操作采用 method:action 的形式编写,其中 method 指请求的 GETPOST 方法,action 指请求的操作,例如:init、calculateTax、returnForm、clearForm。请注意,无论参数是通过 GET 还是 POST 方法发送的,操作的获取顺序均为:

<?php

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

事实上,即使表单是通过 POST 提交的,我们仍然可以这样写:

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

表单元素将通过 POST 方法提交。然而,请求的 URL 将是 main.php?action=calculerimpot。该 URL 的参数将从 $_GET 数组中获取,而其他表单元素则将从 $_POST 数组中获取。

控制器通过 actions 字典执行请求的操作,具体如下:

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

如果请求的操作不在 actions 字典中,则会执行对应于无效操作的脚本。一旦操作脚本被加载到控制器中,它就会运行。 请注意,它既可以访问控制器变量($dConfig、$dSession),也可以访问 PHP 的超级全局字典($_GET、$_POST、$_SERVER、$_ENV、$_SESSION)。该脚本包含应用程序逻辑以及对业务类的调用。在所有情况下,该操作都必须

  • $dSession 字典中填充数据,若需将任何元素保存至当前会话
  • $dResponse['vuereponse'] 中指定要显示的响应模板名称
  • 最后调用 `finSession($dConfig, $dResponse, $dSession)` 结束。如果需要销毁会话,该操作只需调用 `finSession($dConfig, $dResponse)` 即可结束。

为保持一致性,操作可将视图所需的所有信息放入 $dResponse 字典中。但这并非强制要求。仅 $dResponse['vuereponse'] 的值是必需的。请注意,每个操作脚本都以调用 finSession 函数结束,而该函数本身以退出操作结束。因此,操作脚本没有返回值。

3.7. 操作的顺序

Web 应用程序可以被视为一个有限状态机。应用程序的各种状态与呈现给用户的视图相关联。用户可以通过链接或按钮导航到另一个视图。此时 Web 应用程序的状态发生了变化。我们已经看到,一个操作是由形式为 http://machine:port/chemin/main.php?action=XX 的请求触发的。该 URL 必须来自呈现给用户的视图中包含的链接。 我们需要防止用户直接输入 URL http://machine:port/chemin/main.php?action=XX,从而绕过应用程序为其规划的路径。如果客户端是一个程序,情况也是如此。

如果请求的 URL 是从用户看到的上一个视图可以到达的,则该导航路径是有效的。此类 URL 的列表很容易确定。它由

  • 视图中包含的 URL(无论是作为链接,还是作为提交型操作的目标)
  • 当视图显示时,用户被授权可直接在浏览器中输入的 URL。

应用程序状态的列表未必与视图列表完全一致。例如,考虑以下简单的视图 **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>

此基础视图将被整合到由多个基础视图组成的响应结构中。在此视图中,存在一个可动态定位的链接。根据具体情况,errors.php 视图可通过 n 种不同的链接进行显示。这将导致应用程序呈现 n 种不同的状态。在第 i 种状态下,errors.php 视图将通过链接 lieni 显示。在此状态下,仅允许使用 lieni

应用程序的状态列表以及每个状态下可能的操作将存储在 $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'));

上述应用程序有两个命名状态:e-forme-errors。我们添加了一个名为 stateless 的状态它对应于应用程序初始启动时没有状态的情况。 在状态 E 中,允许的操作列表位于数组 $dConfig['states'][E]['allowedActions'] 中。该数组指定了操作的允许方法(get/post)以及操作名称。在上例中,共有四个可能的操作:get:initpost:calculateTaxget:returnFormpost:clearForm

通过 $dConfig['etats'] 字典,控制器可以判断当前的 $sAction 在应用程序的当前状态下是否被允许。该状态由每个操作构建,并存储在会话的 $dSession['etat'] 中。用于检查当前操作是否被允许的控制器代码如下:

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

逻辑如下:如果操作 $sAction 存在于列表 $dConfig['states'][$state]['allowedActions']则该操作被允许;如果该列表不存在,则允许任何操作。$state 是上一次客户端请求/服务器响应周期结束时应用程序的状态。该状态存储在会话中,并从那里检索。 如果发现请求的操作无效,则执行脚本 $dConfig['actions']['invalidSequence']['url']。该脚本将负责向客户端发送相应的响应。

在开发阶段,$dConfig['etats'] 字典可以留空。此时,任何状态下均可执行任何操作。待应用程序完全调试完毕后,可对该字典进行最终配置。这将保护应用程序免受未经授权的操作。

3.8. 调试

控制器提供两个调试函数:

  • trace 函数会在 HTML 输出中显示一条消息
  • dump 函数在同一输出流中显示字典的内容

任何操作脚本均可使用这两个函数。由于操作脚本的代码被包含在控制器代码中,因此这些脚本将能够访问 tracedump 函数。

3.9. 结论

通用控制器旨在让开发人员能够专注于应用程序的操作和视图。它为开发人员处理以下事项:

  • 会话管理(恢复、保存)
  • 请求操作的验证
  • 执行与操作关联的脚本
  • 向客户端发送与操作执行结果相符的响应