本文转自:http://hsboy.com/blog/archives/158-uPHPOEOEOECaAEMVCa.html
PHP的优点就在于轻量和跨平台。它和Apache以及Mysql的联合可以提供一种十分廉价的解决方案,这在开发和部署上都能体现出来。本人之所以执着于PHP也是因为这一点(虽然本人对于PHP的怨言也有不少,暂且不表)。
MVC结构是不是好的结构,它有哪些优点,是否适合于WEB或者是否适合于使用PHP进行开发的WEB项目,这在很多文章中都提到过。这里÷粤恕J导噬希???HP以及一些现成的开远项目,确实可以做出很清晰简洁的架构,这也是本文的目标所在。
一、Model部分Model中包含bean以及bean的有效性检验代码,下面是Bean类的实现:
<?php
class Bean
{
protected $_bean_name = NULL;
private $_bean_array = NULL;
/**
* set the name of the bean
*
* @param $name string the bean name to set
* @return void
*/
public function set_name($name)
{
$this->_bean_name = $name;
}
/**
* get the name of the bean
*
* @return string the name of the current bean
*/
public function get_name()
{
return $this->_bean_name;
}
/** constructor, build a bean from a array and the name of the bean */
public function __construct($bean_name, &$bean_array)
{
$this->set_name($bean_name);
$this->_bean_array = $bean_array;
foreach ($bean_array as $name => $value)
$this->$name = $value;
}
/** constructor for PHP 4 compatibility */
public function Bean($bean_name, &$bean_array)
{
$this->__construct($bean_name, $bean_array);
}
public function validate()
{
$validator = sprintf('validate_%s_bean', strtolower($this->_bean_name));
if (function_exists($validator))
return call_user_func($validator, $this);
else
print $validator;
return FALSE;
}
public function &get_array()
{
return $this->_bean_array;
}
public function set_property($property_name, $value)
{
$this->_bean_array[$property_name] = $value;
$this->$property_name = $value;
}
}
?>
Bean类实际上很简单,它用来保存一个实体的各个属性以及值。Bean类不需要被重载,它自己即可以表示所有的实体,这得归功于PHP这种解释型语言的动态性,Bean的构造函数可以根据不同的实体类型而为自己创建不同的属性以及保存它们的值。Bean类的构造函数出了一个包含属性/值对的数组以外,还需要当前构造的这个实体的类型(名称),以识别当前实例是哪种类型的实体。
Bean类有一个validate()方法用来检查当前Bean实体的各个属性是否符合要求。这个方法只能用来进行一些常规的检查,而不能参与到与业务逻辑相关的检验中,这是由它的固定性无法适用千变万化的业务逻辑所造成的。validate()方法实际上只是简单的调用了一个外部函数进行实体的有效性检验,这个外部函数会根据当前实体的不同类型而不同,其格式为“validate_[BEAN NAME]_bean”,比如对于apple实体,则其检验函数则为"validate_apple_bean",它接受一个参数,即需要被检查的Bean实体。Bean类的实体会在validate()方法中调用这个外部函数并把自身句柄(this)传递给这个函数。如上文所说,我们的目标是简洁、灵活,所以我们这里采用了这样一个设计。它所带来的好处是:
轻量级。不同的实体使用一个公共的Bean类来表示;使用中只需要为不同的实体分别实现一个validate_[BEAN NAME]_bean函数即可进行有效性检验; 低耦合。validate()方法不一定需要validate_[BEAN_NAME]_bean函数的存在(仍然归功于PHP);validate()方法使用Exception来向调用代码返回检验失败信息,不依赖返回值。下面我们来看看这个BeanException类的实现。其实很简单(简单是本文的一贯目标),它只是PHP的Exception类的一个简单扩展:
<?php
class BeanException extends Exception
{
/** the name of the related bean property to this Exception */
protected $_property_name = NULL;
public function __construct($property_name, $message)
{
$this->_property_name = $property_name;
parent::__construct($message);
}
public function get_property_name()
{
return $this->_property_name;
}
}
?>
扩展而来的BeanException类多了一个property_name属性,它表示导致异常的实体属性名称。这个属性用来帮助找出出错的源,下文中将提到如何使用这个属性。继承自Exception类的message属性用来表示对违例的描述。
至此不难理解,我们在开发中要实现的validate_[BEAN NAME]_bean函数中,需要如以下代码这样进行有效性检验并抛出违例。这是对login实体(包含了用户登录信息的实体)进行检验的函数,需要再次强调的是,这个函数只能对实体进行一般性检验,而不能进行与业务逻辑相关的检验(比如检查用户名和密码是否匹配)。
<?php
function validate_login_bean(&$login_bean)
{
if (is_null($login_bean->aclearcase/" target="_blank" >ccount) || strlen(trim($login_bean->account)) == 0)
throw new BeanException('account', '帐号必须输入');
if (is_null($login_bean->password) || strlen($login_bean->password) == 0)
throw new BeanException('password', '密码必须输入');
return TRUE;
}
?.
二、View部分
View用来与用户进行沟通,包括向用户输出信息以及从用户处获得输入。
在本文讨论的方案中,用户的输入(来自于PHP的$_REQUEST预定义数组)将被转化为一个实体Bean,然后进入系统流程,被Controller处理。将用户输入转化为实体,使用下面这个简单的函数完成:
/**
* fetch a bean from user request information
*
* @param $bean_name array the name of the bean to fetch
* @return Bean
*/
function &fetch_bean($bean_name)
{
$bean_array = strtoupper($bean_name) . '_BEAN';
$bean_array = &$GLOBALS[$bean_array];
$result = array();
foreach ($bean_array as $property)
{
if (isset($_REQUEST[$property]))
$result[$property] = $_REQUEST[$property];
else
$result[$property] = NULL;
}
return new Bean(strtolower($bean_name), $result);
}
?>
这个函数接受唯一的一个bean_name参数,然后返回一个Bean类的实例。通过Bean名称,这个fetch_bean函数可以得到这个bean的属性列表,这个属性列表来自于一个名为“$[BEAN NAME]_BEAN”的数组,比如,对于apple实体,它的属性列表就是名为“$APPLE_BEAN”的数组。毫无疑问,这些各种实体的属性列表数组需要在开发过程中声明,位于本解决方案以外。
作为WEB项目,向用户输出基本都是靠HTML完成。一直以来,我都采用Smarty模板技术作为实现View的手段。Smarty易于扩展这一点使我受益匪浅。使用Smarty进行常规的模板设计实现实体的输出(表单或者表格)非常简单,这里不多做讨论。我们要着重讨论的是利用Smarty的易于扩充的特性实现更加人性化的表单。首先我们来看两个smarty的函数扩展:
<?php
/**
* smarty functions for bean validator error message hint
*/
function smarty_function_error_message($params, &$smarty)
{
if ($smarty->get_template_vars('error_property') == $params['field'])
return '<br /><span class="error_message">' .
$smarty->get_template_vars('error_message') . '</span>';
}
function smarty_function_error_class($params, &$smarty)
{
if ($smarty->get_template_vars('error_property') == $params['field'])
return sprintf(' class="%s" title="%s"', 'error_property',
$smarty->get_template_vars('error_message'));
}
?>
这两个函数本身也是十分简单的,第一个函数通过输出一个错误信息来提示用户某个表单域(输入项目)的错误,第二个则通过输出一个CSS属性设置代码(class="error_property")来标记一个HTML标签的CSS样式为"error_message"从而实现提示用户这个项目是错误的目的。这两个函数可以结合起来用,也可以各自单独使用。下面代码是一个登录窗口的HTML模板源代码:
<form name="Login_Form" id="Login_Form" method="POST" action="">
<table align="center" border="0" cellpadding="2" cellspacing="0">
<tr bgcolor="#f9f9f9">
<td height="12" colspan="2" align="center">{ if $message }<span class="hint">{ $message }</span>{ /if } </td>
</tr>
<tr bgcolor="#f9f9f9">
<td align="right">用户名:</td>
<td><input name="account" type="text" id="account" value='{ $bean->account }' size="20" maxlength="20" />{ error_message field="account" }</td>
</tr>
<tr bgcolor="#f9f9f9">
<td align="right">密码:</td>
<td><input name="password" type="password" id="password" size="20" maxlength="20" />{ error_message field="password" }</td>
</tr>
<tr bgcolor="#f9f9f9">
<td height="12" align="center"></td>
<td><input type="Submit" name="Submit" value="登录" /> <a href="register.php">没有帐号?立刻注册一个!</a></td>
</tr>
</table>
</form>
它在正常情况下显示如下图所示:
而在只输入了用户名而没有输入密码的情况下点击提交按钮,则会显示如下图,可以看出错误信息的提示非常人性化:
三、控制器其实本文并不打算实现一个完整的控制器,这也是不容易实现的,因为难以有一种简单而有效的方法将实体与视图组织起来以实现各种复杂的业务流程。但是有了Model与View两部分工作量减少的帮助以及模式的固定,实现针对某一特定业务逻辑的控制器是相对比较简单的。我的意思也就是说,如果我们将每一个PHP文件对应与某一个特定的业务逻辑,那么它也是比较简单的。这里我们只以系统登录为例说明一下系统大多数情况下的运转流程,以下是实现登录流程的控制器代码:
<?php
require './global.inc.php';
$login_bean = fetch_bean('LOGIN');
if (is_null($login_bean->account))
{
$smarty->display('login_form.htm');
}
else
{
/* user input integrity check */
try
{
if (!$login_bean->validate())
{
$smarty->assign('message', '登录失败!您的输入有问题。');
}
}
catch(BeanException $e)
{
$smarty->assign('message', '登录失败!您的输入有问题。');
$smarty->assign('error_property', $e->get_property_name());
$smarty->assign('error_message', $e->getMessage());
$smarty->assign_by_ref('bean', $login_bean);
$smarty->display('login_form.htm');
exit;
}
/* password validate */
$result = check_login($login_bean);
if (!$result)
{
$smarty->assign('message', '登录失败!密码错误或者帐号不存在。');
$smarty->assign_by_ref('login_bean', $login_bean);
$smarty->display('login_form.htm');
}
else
{
$_SESSION[S_IS_LOGIN] = TRUE;
$_SESSION[S_USER_ID] = $result->id;
$_SESSION[S_USER_INFO] = &$result;
$smarty->display('login_ok.htm');
}
}
?>