(Walter.Fan编译自Jason E. Sweat写的An Introduction to MVC Using PHP)
下面来看一个简单的MVC应用程序实例:
说得天花乱坠,不如实际动和做个简单的例子.在这个例子中,我们以Phrame框架来
实现MVC模式. Phrame是Jakarta Struts的一个PHP实现方案,它的控制文件不象struts
那样是XML文件,而是PHP中最常用的数组.
在Phrame中,和Struts一样,通过Action, Forms and Forwards这几个类
将模型和视图松散耦合在一起.
每一次controller初始化,它都会创建一个扩展自Action类的子类的实例
controller会调用你的Action子类的Process()方法,
一个Forwards的列表和一个包含所有HTTP request 变量的Form对象
你的Action的期望结果是去确定适当的Forward对象返回给controller.
controller会处理这个Forward对象并退出对于这次请求的处理过程
Action对象的作用,可以以它自己在HTTP request 和一个句子之间做一个类比
你可以认为在这个句子中,Action是一个动词, Model是一个名词, View是一个形容词.
换句话说,一个特定的Web请求会在一个Model(名词)上执行一个Action(动词),
或者显示一个View来描述(形容词)一个Model(名词)
这个类比描述了Action和Model关系,在选择对象名时可以加以考虑
这个例子是Phrame的Hello World例子的一个修改版本 (http://phrame.itsd.ttu.edu/).
源代码可由http://sourceforge.net/projects/phrame下载
在这个例子中,我去掉了PHP的一些警告 ,用Smarty代替XSLT来格式化视图
MVC应用程序使用一个引导(bootstap)文件,它是一个单独的PHP文件,是应用程序的核心
本例中,这个bootstap文件是hello.php,让我们来看看这个bootstap文件
<?
error_reporting(E_ALL);
define('PHRAME_LIB_PATH', '../../phrame/');
require_once PHRAME_LIB_PATH.'include.jes.php';
require_once 'Smarty.class.php';
require_once 'MappingManager.php';
require_once 'actions/HelloAction.php';
require_once 'models/Person.php';
require_once 'models/HelloErrors.php';
define('APPL_VIEW', 'hello.php?'._VIEW.'=');
define('APPL_ACTN', 'hello.php');
?>
在一个Phrame程序中,Forms, Actions and Forwards的关系是
在传统的Phrame选项数组中建立的.我觉得这个数组单调冗长不利于开发和维护
就创建了一个MappingManager类来管理维护这些关系
这个类可以被任一个Phrame应用程序扩展,它有方法可返回
Phrame使用的PHP数组.
这个类的好处是在你增加它们进可以验证Form和action mapping之间的关系
(避免了在你尝试使用Phrame选项数组时产生错误)
class HelloMap extends MappingManager
{
function HelloMap()
{
//set options
$this->_SetOptions('handle_error');
//add application forms
$this->_AddForm('helloForm','ActionForm');
//add application actions and forwards
$this->_AddMapping('sayHello','HelloAction', APPL_VIEW.'index','helloForm');
$this->_AddForward('sayHello', 'index');
$this->_AddForward('sayHello', 'hello',APPL_VIEW.'hello');
}
}
在 HelloMap class的构造函数中完成了我们所要做的.
首先,重载传统的Phrame选项数组,我们定义了一个错误处理函数
接着,看需要识别什么Form,在这个例子中,不需要扩展标准的Phrame ActionForm类
最后,定义actions和forwards, 本例中,只有一个action: sayHello
两个Forwards:“index” 和 “hello”
MappingManager::_AddMapping() 方法的参数是
(mapping的名字, 实现这个mapping的类, 调用action的缺省地址,mapping中与action关联的form)
MappingManager::_AddForward()方法的参数是
action mapping 的标识符,forward的标识符和可选的重定向URL(如果没有action与之关联)
<?
session_start();
$smarty =& new Smarty;
$map =& new HelloMap;
$controller =& new ActionController($map->GetOptions());
$errors =& new HelloErrors;
function handle_error($number, $message, $file,$line, $context)
{
appl_error($message);
}
function appl_error($psErrorMsg)
{
$errors =& new HelloErrors;
$errors->AddError($psErrorMsg);
}
?>
接下来,我们start the session, 创建几个我们要用到的对象
Smarty template object
HelloMap class object(我们先前定义的)
Phrame ActionController Object
(它需要一个选项数组,可由MappingManager::GetOptions()获得)
这节的代码还加入了一个错误Model,封装了错误处理实际上的实现方案
(保存错误在$_SESSION的一个数据中, 参见HelloErrors.php)
并定义了两个函数
appl_error(),允许你增加一个错误到系统的错误对象中去
handle_error() 一个PHP错误处理函数,它调用了函数appl_error()
这就是说,你可以直接用函数appl_error(),或者用
trigger_error()(如果PHP的错误处理句柄设成handle_error())
<?
if (array_key_exists(_ACTION,$_REQUEST))
{
//release control to controller for
//further processing
$controller->Process($map->GetMappings(),
$_REQUEST);
}
else
{
//determine and display view
$requested_view = (array_key_exists(_VIEW,
$_REQUEST)) ?
strtolower($_GET[_VIEW]) :
'index';
switch ($requested_view) {
case 'hello':
$template = $requested_view.'.tpl';
//assign view specific data
$person =& new Person;
$smarty->Assign('name',
$person->GetName());
break;
case 'index':
default:
$template = 'index.tpl';
}
//assign common data
$smarty->Assign(array(
'appl_link' => APPL_ACTN
,'appl_view' => APPL_VIEW
,'action' => _ACTION
));
//assign and clear errors
$smarty->Assign('errors',
$errors->GetErrors());
$smarty->Display($template);
exit;
}
余下的代码实现了MVC的controller组件
if语句判断是否请求了一个action.
如果是的,调用Phrame
ActionController::Process()方法激活这个框架.
如果没有指定的请求,显示一个View
else语句确定合适的View,
assign特定的数据到smarty模板
assign通用的数据到smarty模板
处理任何可能发生的错误
然后render渲染模板
跳出这个程序,我们来看看到底我们的应用程序中加入什么东西来使用Phrame
首先, 要考虑如果根本没有参数传入,我们的应用程序该干什么.
在本例中,跳过action处理,渲染index.tpl模板以显示缺省的视图
这儿是Smarty template index.tpl的一部分
<form action="{$appl_link}" method="post">
<div>
<input type="hidden" name="{$action}" value="sayHello" />
{if $errors|@count gt 0}
<ul>
{section name=e loop=$errors}
<li><b style="color:
red">{$errors[e]}</b></li>
{/section}
</ul>
{/if}
What is your name?<br />
<input type="text" name="name" value="{$name}"
/>
<input type="submit" value="OK" />
</div>
</form>
重要的是要检查什么正在进行中.
这个HTML表单提交给应用程序脚本本身,它有一个隐藏变量,
以模板变量$action命名为“sayHello”,
这就是指引controller去初始化 “sayHello” mapping的输入.
模板的下面一节是错误条件检查,如果有错误,就显示一个无序列表.
模板的最后一部分是通常表单的显示部分,一个users name文本输入框和一个提交按钮
那么,当用户点击提交按钮后会怎样呢?
浏览器会提交一个HTTP POST给这个bootstrap脚本.
$_REQUEST[_ACTION]值为“sayHello”. 作为响应,
$controller 会执行一个Perform()方法
回顾我们的HelloMap类中的mapping, “sayHello”和HelloAction类关联
ActionController::Perform()方法会创建 HelloAction类的一个实例并
执行HelloAction::Perform()方法, 见下面的代码
class HelloAction extends Action
{
function &Perform(&$poActionMapping,&$poActionForm)
{
$person =& new Person();
$errors =& new HelloErrors;
$name = $poActionForm->Get('name');
//get ActionForward depending on if
//errors were generated
if ((!$person->SetName($name)) ||($errors->HasErrors()))
{
$actionForward =$poActionMapping->Get('index');
}
else
{
$actionForward =$poActionMapping->Get('hello');
}
return $actionForward;
}
}
在你的应用程序中所需的每一个action都类似于这个文件
你可创建一个Phrame Action的子类,并重载 Perform()方法去实现一个action
最后,你的程序需要返回一个ActionForward 对象给controller去完成处理.
让我们察看一下HelloAction 代码的细节
最开始,因为性能原因,以传址方式将两个指定对象传给函数Perform
这个函数将以传址方式返回ActionForward对象
然后,创建模型的两个实例$person and $errors.
$poActionForm是由controller根据你的mappings创建的Phrame ActionForm类的实例
我们指定我们的Form为 ActionForm,而不是继承它,因为没有必需的理由
ActionForm类本身继承了Phrame 的工具类 HashMap 类,并随着$_REQUEST 关联数据预载
所以如果POST提交过后,$poActionForm->Get()方法会得到posted的变量
在本例中,我们想求用户提交的文本输入框中输入的name
接着,根据medel中的Person::SetName()方法是否成功,
由我们的action确定返回哪一个ActionForward mapping
同时,它也验证这个actionr结果没有错误触发. 如果有问题,
它会返回到“index” view,否则,显然去显示“hello” view.
最后我们来看看Person.php这个文件,对每个应用程序来说,
Models都是独一无二的,因而Phrame库中不可能有其原型,
也不必从Phrame类中继承任何类. 这个Model使用了两个常量
,把它们作为model类名的前缀以避免重名.
回顾我们的模型的内部细节,我们发现名称保存在PHP的session中.
重点注意它在我们的类以外,这个存储的细节是未知的, 你可在任何时候
改变类的定义去改变这个实现方法.
由于在我们的应用程序中所有对于session数据索引的操作都是通过单一的model类来完成的
你可始创建正确的错误处理,业务逻辑(类似于确认规则)
并加强了你的应用程序的数据一致性.
这就是MVC的贡献,便利我们可能更好的维护在持久性存贮中的数据
在这个模型中另一个令人感兴趣的就是SetName() 方法,
特别的,我们的业务逻辑规定一个名字必须小于20个字符,
如果以一个超出范围的名字长度值调用这个模型,就会触发一个错误
未来的方向
怎么扩展基于Phrame的MVC应用程序呢?
如果你需要一个额外的数据视图,你可以简单地增加一个新的模板
并扩展"case"语句以在bootstrap文件中确定一个有效的视图.
要增加一个新的action,你需要创建一个新的继承于Action的类
就象我们写的HelloAction类, 你还需要在HelloMap 类的构造函数中
增加一个 mapping以便让MVC知道当请求新的action时,该去初始化哪一个类
最后,当action完成时你需要增加forwards指向合适的View
我们开发的大多数model类都会用到数据库,
很多时候,对于Phrame我们有两个基本的方面的改变.
首先,实现一个缺省的action去显示一个view
其次,在本例中并未完全贯彻面向对象风格,可以以Factory模式
在缺省的“ShowView” action中创建应用程序view的一个实例
这个例子的另一个问题是包含文件有点低效:
每次请求都需要包含action类,甚至显示一个特殊请求的结果的view.
你可以重新设置你的应用程序,对上下文更加敏感,只包含必需的资源.
回顾一下,我们看到了以MVC框架实现PHP的好处.
并察看了一个用session作为持久性数据存贮的model,
怎样使用HelloMap和Phrame和 ActionController来控制应用程序的流程,
在应用程序中怎样使用Smarty templates来实现View
这个例子非常简单,但它演示了处理用户输入,数据校验,错误处理和应用程序流程
通过学习这个例子,我们拥有了开发基于MVC的web程序的基础