3. 通用控制器
3.1. 简介
在前面的方法中,我们已知需要编写名为 main.php 的控制器。随着经验的积累,我们会发现该控制器经常执行相同的操作,因此很想编写一个可在大多数 Web 应用程序中使用的通用控制器。该控制器的代码可能如下所示:
<?php
// 通用控制器
// 读取配置
include 'config.php';
// 引入库
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
// 启动或恢复会话
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
// 获取待执行的操作
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
$sAction=strtolower($_SERVER['REQUEST_METHOD']).":$sAction";
// 操作序列是否正常?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// 操作序列异常
$sAction='enchainementinvalide';
}//if
// 处理操作
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
// 将响应(视图)发送给客户端
$sEtat=$dSession['etat']['principal'];
$scriptVue=$dConfig['etats'][$sEtat]['vue'];
include $scriptVue;
// 脚本结束 - 除非存在错误,否则不应到达此处
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:配置字典
// $dSession:包含会话信息的字典
// $dReponse:响应页面的参数字典
// 会话记录
if(isset($dSession)){
// 将请求参数存入会话
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// 无会话
session_destroy();
}
// 显示响应
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// 脚本结束
exit(0);
}//结束会话
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// 根据前一状态验证当前操作是否被授权
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// 操作验证
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
//--------------------------------------------------------------------
function dump($dInfos){
// 显示信息词典
while(list($clé,$valeur)=each($dInfos)){
echo "[$clé,$valeur]<br>\n";
}//while
}//跟踪
//--------------------------------------------------------------------
function trace($msg){
echo $msg."<br>\n";
}//跟踪
?>
3.2. 应用程序的配置文件
应用程序的配置位于一个必须命名为 config.php 的脚本中。应用程序的参数存储在一个名为 $dConfig 的字典中,该字典同时被控制器、操作脚本、模型和基本视图所使用。
3.3. 需包含在控制器中的库
需在控制器代码中包含的库被放置在数组 $dConfig['includes'] 中。控制器通过以下代码序列将其包含进来:
<?php
...
// 读取配置
include "config.php";
// 包含库
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
3.4. 会话管理
通用控制器会自动管理会话。它通过字典 $dSession 保存和恢复会话内容。该字典可能包含需要序列化的对象,以便后续正确恢复。该字典的键为 'session'。因此,恢复会话可使用以下代码:
<?php
…
// 启动或恢复会话
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
如果某个操作希望在会话中存储信息,它会向字典 $dSession 中添加键和值。由于所有操作共享同一个会话,如果应用程序由多人独立开发,则存在会话键冲突的风险。这是一个难题。 必须开发一个列出会话键的存储库,并由所有人共享。我们将看到,每个操作都会以调用以下函数 finSession 结束:
<?php
...
// ---------------------------------------------------------------
function finSession(&$dConfig,&$dReponse,&$dSession){
// $dConfig:配置字典
// $dSession:包含会话信息的字典
// $dReponse:响应页面的参数字典
// 会话记录
if(isset($dSession)){
// 将请求参数放入会话中
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// 无会话
session_destroy();
}
// 呈现响应
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// 脚本结束
exit(0);
}//结束会话
某个操作可以决定不继续当前会话。为此,只需在调用函数 finSession 时,不向参数 $dSession 传递任何值,此时会话将被删除(session_destroy)。 如果字典 $dSession 存在,则将其保存到会话中,随后该会话会被记录(session_write_close)。 因此,当前操作可以通过向字典 $dSession 中添加元素,将数据存储在会话中。需要注意的是,控制器会自动将会话中当前请求的参数保存下来。这样,在处理下一个请求时,如有需要,即可调用这些参数。
3.5. 向客户端发送响应
函数 finSession 的最终目的是向用户发送响应。 我们曾提到,响应可能采用不同的页面模板。这些模板通过配置存储在 $dConfig['vuesReponse'] 中。在采用双模板的应用程序中,可能包含以下内容:
<?php
…
$dConfig['vuesReponse']['modele1']=array('url'=>'m-modele1.php');
$dConfig['vuesReponse']['modele2']=array('url'=>'m-modele2.php');
当前操作通过 $dReponse['vuereponse'] 指定所需的模板。控制器通过以下指令显示该模板:
将此响应发送给客户端后,控制器终止运行(exit)。
3.6. 操作的执行
控制器等待带有 action=XX 参数的请求。 如果请求中不存在该参数,且请求为 GET,则操作取值为 'init'。这是对控制器发出的第一个请求的情况,其形式为 http://machine:port/chemin/main.php。
根据配置,每个操作都关联有一个负责处理该操作的脚本。例如:
<?php
...
// 配置应用程序的操作
$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');
预设了两个操作:
当前操作无法紧跟上一操作的情况 | |
当请求的操作在操作字典中不存在时 |
应用程序特有的操作采用“方法:操作”的形式表示,其中“方法”指请求的 GET 或 POST 方法,“操作”指请求的操作,例如:init、calculerimpot、retourformulaire、effacerformulaire。 需要注意的是,无论使用哪种参数发送方法(GET 或 POST),操作都会通过以下序列获取:
实际上,即使表单已提交,仍可编写:
表单中的元素将被提交(method='post')。不过,请求的URL将是 main.php?action=calculerimpot。 URL 的参数将从字典 $_GET 中获取,而表单的其他元素则将从字典 $_POST 中获取。
凭借操作字典,控制器将按以下方式执行请求的操作:
<?php
...
// 处理操作
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
如果请求的操作不在操作字典中,则会执行对应于无效操作的脚本。一旦操作脚本在控制器中加载完毕,它就会被执行。 需要注意的是,它既可以访问控制器变量($dConfig、$dSession),也可以访问 PHP 的超级全局字典($_GET、 $_POST、$_SERVER、$_ENV、$_SESSION)。脚本中包含应用逻辑以及对业务类的调用。在任何情况下,该操作都必须
- 在需要将元素保存到当前会话时,必须通过字典 $dSession 进行配置
- 在 $dReponse['vuereponse'] 中指定要显示的响应模板名称
- 最后调用 finSession($dConfig, $dReponse, $dSession)。 如果需要销毁会话,该操作只需以调用 finSession($dConfig, $dReponse) 结束。
为保持一致性,该操作可将视图所需的所有信息放入字典 $dReponse 中。但这并非强制要求。仅 $dReponse['vuereponse'] 这一值是必不可少的。 需要注意的是,所有操作脚本均以调用函数 finSession 结束,而该函数本身又以 exit 操作结束。因此,操作脚本执行完毕后不会返回。
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(无论是作为链接还是作为提交类操作的目标)
- 用户在查看该视图时被允许直接在浏览器中输入的 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>
该基本视图将整合到由多个基本视图组成的视图组合中,从而形成最终的响应。在此视图上,有一个可动态定位的链接。 因此,视图 erreurs.php 可根据具体情况显示 n 个不同的链接。这将产生应用程序的 n 种不同状态。在第 i 种状态下,将显示视图 erreurs.php 并包含链接 lieni。在此状态下,仅允许使用链接 lieni。
应用程序的状态列表以及每个状态下可执行的操作将记录在字典 $dConfig['etats'] 中:
<?php
...
// 配置应用程序状态
$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 状态下,允许的操作列表位于表 $dConfig['etats'][E]['actionsautorisees'] 中。 其中明确了该操作允许的方法(GET/POST)及其名称。 在上例中,有四个可能的操作:get:init、post:alculerimpot、get:retourformulaire 和 post:effacerformulaire。
借助字典 $dConfig['etats'],控制器可以判断当前操作 $sAction 在应用程序的当前状态下是否被授权。 该状态由每次操作构建,并存储在会话中的 $dSession['etat'] 中。控制器用于检查当前操作是否被允许的代码如下:
<?php
.....
// 操作序列是否正常?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// 流程异常
$sAction='enchainementinvalide';
}//if
// 操作处理
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
..........
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// 检查当前操作相对于前一状态是否被允许
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// 操作验证
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
逻辑如下: 如果操作 $sAction 位于列表 $dConfig['etats'][$etat]['actionsautorisees'] 中,或者该列表不存在 此时将允许执行任何操作。$etat是上一轮客户端请求/服务器响应周期结束时的应用程序状态。该状态已存储在会话中,并从该处检索。 若发现请求的操作非法,则执行脚本 $dConfig['actions']['enchainementInvalide']['url']。该脚本将负责向客户端发送相应的响应。
在开发阶段,可以不填写字典$dConfig['etats']。在这种情况下,所有状态都允许执行所有操作。可以在应用程序完全调试完成后完善该字典。它将保护应用程序免受未经授权的操作。
3.8. 调试
控制器提供两项调试功能:
- 跟踪功能可在数据流中显示一条消息 HTML
- dump 函数可用于在同一数据流中显示字典的内容
任何操作脚本均可使用这两个函数。由于操作脚本的代码被包含(include)在控制器代码中,因此 trace 和 dump 函数对脚本是可见的。
3.9. 结论
通用控制器旨在让开发人员能够专注于应用程序的动作和视图。它为开发人员提供:
- 会话管理(恢复、保存)
- 验证请求操作的有效性
- 执行与操作关联的脚本
- 根据操作执行结果的状态向客户端发送相应的响应