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'] 中指定了所需的模板。控制器使用以下指令显示该模板:
一旦此响应发送给客户端,控制器即停止(退出)。
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');
预定义了两个操作:
指当前操作无法紧跟上一个操作的情况 | |
请求的操作在操作字典中不存在的情况 |
应用程序特有的操作采用 method:action 的形式编写,其中 method 指请求的 GET 或 POST 方法,action 指请求的操作,例如:init、calculateTax、returnForm、clearForm。请注意,无论参数是通过 GET 还是 POST 方法发送的,操作的获取顺序均为:
<?php
…
// retrieve the action to be taken
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
事实上,即使表单是通过 POST 提交的,我们仍然可以这样写:
表单元素将通过 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-form 和 e-errors。我们添加了一个名为 stateless 的状态,它对应于应用程序初始启动时没有状态的情况。 在状态 E 中,允许的操作列表位于数组 $dConfig['states'][E]['allowedActions'] 中。该数组指定了操作的允许方法(get/post)以及操作名称。在上例中,共有四个可能的操作:get:init、post:calculateTax、get:returnForm 和 post: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 函数在同一输出流中显示字典的内容
任何操作脚本均可使用这两个函数。由于操作脚本的代码被包含在控制器代码中,因此这些脚本将能够访问 trace 和 dump 函数。
3.9. 结论
通用控制器旨在让开发人员能够专注于应用程序的操作和视图。它为开发人员处理以下事项:
- 会话管理(恢复、保存)
- 请求操作的验证
- 执行与操作关联的脚本
- 向客户端发送与操作执行结果相符的响应