[转]PHP MVC及模板引擎

  模板引擎,这四个字听起来很高深的样子,一般用到“引擎”两字都会感觉比较高级,类似游戏3D引擎、Zend引擎等,其实都是唬人的,骗外行人的。所以在我初学PHP的那会,也因为这四个字导致了我觉得很难而没有去看他到底是什么样一个东西,直到很长时间以后使用Smarty才真正了解模板引擎的原理和作用。Smarty(http://smarty.php.net),PHP官方模板引擎,看名字给人感觉应该很快,其实很慢,即使他有预编译(另一个看起来很高级的名词,同样也是唬人的,下面我会讲到这个)。[注:我刚才点开Smarty发现他说他已经不是一个PHP子项目了,汗,看来确实唬人,哈玩笑^_^]。其实在PHP里,模板引擎扮演着View(其实通俗说就是页面,看英文有时候会给人很高级的错觉)的角色,这是一个很重要的角色,因为用户的交互啊,界面效果啊等等都在这里,这是最终用户看到的你的系统的样子。

  开头就说模板引擎,只是跟大家说明一下这个东西其实没有什么难理解的,明白其原理以后你会发现他是纸老虎,所以你要有信心你会很轻松看完此文。

  为了更好的说明模板引擎所扮演的角色,我不得不也谈谈MVC。这个话题恐怕互联网上谈及的很多,我也只能根据我的理解来描述,可能有不恰当的地方,欢迎讨论。通常的MVC是指Model、View和Controller。也就是模型、视图和控制器。我理解MVC也是在学了PHP不短时间后了,当时请教老廖(http://qeephp.com),才恍然大悟。

  先来说说Controller,也就是控制器,控制器是个什么东西呢?在PHP里他是扮演一个接收用户请求,把用户请求定位到指定数据模型的角色。解释起来感觉不是很好解释,来看一个简单的留言本的例子:

//用户请求可能是 <a href="http://www.example.com/guest.php?module=list">http://www.example.com/guest.php?module=list</a>
$module = $_GET['module']; 

switch ($module) {
    case 'list':
        require_once 'list.php';
        break;
    case 'add':
        require_once 'add.php';
        break;
    case 'del':
        require_once 'del.php';
        break;
    default:
        require_once 'list.php';
        break;
}

  是不是看起来很简单好像没什么东西呀,只是根据用户的请求参数包含不同的文件而已。没错,确实很容易,这个switch语句其实就一个最简单的控制器的实现。他控制什么?他控制你根据不同的用户请求参数调用不同的数据模型处理用户请求。那么这里的list可能是一个留言列表,add是添加留言,del是删除留言。Controller的传统实现可以这么简单,当然现在的很多技巧包括根据不同的用户请求包含不同的业务逻辑处理类,比如list自动定位到/model/List.class.php这样的一些技巧性操作等。

  再来说说Model,其实我们一般花比较长时间设计和编写的也是这块内容,也就是具体的业务逻辑实现。比如一个留言列表要处理些什么,都是在这里实现。还是直接看一个Model例子比较直观:

//Guest_List.class.php
class Guest_List {
    public $page = 1;
    public function __construct() {
        $this->db = DB::init($GLOBALS['dsn']);
        $this->page = (int) $_GET['page'];
    } 

    public function getList() {
        $begin = $this->page * 10;
        $sql = "SELECT * FROM guest ORDER BY addTime DESC LIMIT $begin, 10";
        return $this->db->getAll($sql);
    }
}

  这里的Guest_List就是一个简单的Model实现,构造函数取得页数page参数,getList方法查询留言列表并返回结果集。那么在list.php里可能是这样调用的:

//list.php
require_once 'Guest_List.class.php';
$model = new Guest_List();
$lists = $model->getList();

  嗯,其实很多MVC框架都是这么实现的,只不过可能加了一些自动调用的机制,会根据用户请求自动调用类,自动执行方法,呵呵。Model大功告成。这里需要明确一点就是,Model只是返回视图上所可能需要用到的数据,他不负责任何和显示有关的事情,那么显示相关的就交给View来做了。我们是不是不知不觉已经把表现和业务逻辑分离了?没错,分离就是这么简单。

  好了,来看看View怎么利用Model返回的数据来显示页面吧。最简单的例子,我们只需要在list.php里增加一行即可。

//list.php
require_once 'Guest_List.class.php';
$model = new Guest_List();
$lists = $model->getList();
//上面是Model,那么下面就是View
require_once 'list.html';

  来看看View都做些什么吧,我们用list.html来表示留言列表所展现给用户的界面文件,用html来命名看起来会更直观一些,他好像是个html文件,负责输出html代码给浏览器。来看看list.html可能长什么样子:

<!--list.html--> 

<table>

<?php foreach ($lists as $value) { ?>

<tr>

<td><?php echo htmlspecialchars($value['guest_user_name']);?></td>

<td><?php echo date('Y-m-d H:i', $value['guest_date_time']);?></td>

<td><?php echo htmlspecialchars($value['guest_content']);?></td>

</tr>

<?php } ?>

</table>

  不难看出来这个文件所做的只不过是遍历留言数组$lists,然后输出每一行的留言,对留言的内容处理做了htmlspecialchars和date转换(与显示相关的处理),除了和显示相关的操作,他没有再做任何业务逻辑了(也不应该有)。

  我发现写到这里真的没有什么好写的了,MVC就是这些(或者再做一些扩展),至于怎么做到表现和业务分离,那么就是在你的Model里只返回数据,也就是你View所需要用到的数据,而你的View拿到这些数据后负责去显示他就可以了,不应该在你的Model里做显示和视觉相关的操作,也不应该在你的View里做一些业务逻辑相关的操作,把这两者分清楚,就自然而然的表现与业务分离了。

  接下来说说负责View的模板引擎吧,其实你在上面应该已经看到了一个最简陋的模板引擎,那就是View部分的 require_once 语句。厄,实在是太简单了,模板引擎其实是调度并解析模板的东西,其中调度模板由 require_once 搞定了,那么解析呢?这里由 PHP 引擎本身来搞定了。哈,没错,我一直都认为 PHP 是个最好的模板引擎。

  不过还是不得不说说传统的模板引擎的实现原理,一般来说会有这么几个步骤:
  1、注册变量,也就是把从Model返回的数据注册到模板引擎中,告诉模板引擎这个变量可以使用,其实所谓的注册也只不过是不得不这么做,因为一般引擎内部函数是没办法直接访问Model返回的变量的(变量作用域的问题),所以不得不加一个注册操作,把这些变量转换从模板引擎类的属性等。
  2、模板解析,就是读取模板文件,按照模板语法将标签解析成 PHP 语法,或者执行一些替换操作,用变量内容替换掉模板标签,其实效果都差不多。
  3、如果不是将变量内容替换掉模板标签,那么基本上第三步就是将注册的变量和解析完的模板融合在一起输出,类似于上面的list.html,是个解析完的文件,然后输出。

  一般模板引擎还会提供不少用于显示内容处理的插件,比如日期转换、字符串处理、生成表格、生成select等,这些给页面制作提供了一些方便。Smarty还包含了一些页面缓存机制,也很不错。

  很多模板引擎都顶着语法简单的嚎头,美其名曰降低美工的学习门槛。其实我不得不问,有多少模板是由美工来做的呢?而且对比两种语法,不觉得 PHP 的简单循环和输出有什么难以理解的,对比下面两种语法:

<!-- <?php foreach ($lists as $value) { ?> -->
<?=value['userName']?>
<!-- <?php } ?> -->

  和

<!-- loop lists value -->
{value['userName']}
<!-- loop -->

  我左看右看都觉得他们差不多,呵呵,与其再学习一套语法,还不如直接用你已经非常熟悉的PHP呢,为什么要虐待自己呢?而且从可维护性的角度来讲,维护PHP语法和维护模板语法,哪种更容易呢?PHP是标准,只要会PHP都知道他怎么写,表示什么,但是模板引擎千奇百怪的,各种语法都有,不是一个统一的标准,我想谁维护一个从来没有用过的模板,都需要花不少时间去学习引擎语法。更何况即使模板可以那样写,最终还是需要一堆正则替换成PHP语法。我敢肯定,前面写的哪种模板引擎语法最终会被转换成它上面那种PHP。其实模板引擎的解析也就是将模板语法转换成PHP语法的过程。抛开效率来说,多此一举。就象《C专家编程》作者说的,即使你能用宏把C写成看起来好像另外一种语言,但是你不要这么做,同样的这句告诫是否适合于模板引擎呢,它看起来很像另外一种语言。当然我这篇文章不是来批判模板引擎的,哈。它既然存在,也有其存在的道理,某些场合还是不得不用的,比如如果你把模板提供给用户去制作和使用,那么你不得不采用标签以限制用户使用PHP语法,来增强系统安全性。

  再来说效率问题,由于模板引擎要解析模板语法,会用到很多正则匹配和替换,那么在实际运行中是比较消耗系统资源的,而且当模板标签非常复杂或者嵌套多层的时候,效率是比较低的,因为有了一种处理方法,就是预编译。所谓的预编译,就是把带有模板语法的模板,通过处理,转换成 PHP 语法的文件,只要模板文件没有被修改,那么直接包含编译后的文件即可,这样就不需要再次替换和匹配,可以大大提高效率,不过由于模板引擎的复杂性,导致编译后的结果文件仍然比我们一般写出来的PHP文件复杂得多。所以其实效率还是远低于直接编写PHP模板的。有兴趣的可以打开一个Smarty编译过的文件,看看其嵌套,其实要比直接循环来得复杂。

  本文写到这里也差不多了,具体模板引擎如何编译如何处理,各个模板引擎的方式都不一样,有兴趣的可以去下载几个比较经典的引擎看看,比如Smarty。随后我附上我自己用的PHP模板引擎。

  PS:其实我觉得MVC应该叫做CMV是不是更符合逻辑呢?没有考证过这个词的由来^_^。

我的真正PHP模板引擎:

<?php
/**
 * 模板引擎
 *
 * Copyright(c) 2005-2008 by 陈毅鑫(深空). All rights reserved
 *
 * To contact the author write to {@link mailto:shenkong@php.net}
 *
 * @author 陈毅鑫(深空)
 * @version $Id: Template.class.php 1687 2008-07-07 01:16:07Z skchen $
 * @package Template
 */
defined('FW') || exit(header('HTTP/1.0 400 Bad Request'));

class Template {
    protected static $obj;

    public $vars;
    public $includeFiles;
    public $includeFile;
    public $templates;
    public $template;
    public $contents;
    protected $_content;
    protected $_contents;
    protected $_path;

    protected function __construct() {
        $this->vars = array();
        require_once ROOT_PATH . "lib/template.func.php";
    }

    /**
     * 初始化模板引擎
     *
     * @return object 模板引擎对象
     */
    public static function &init() {
        if (is_null(self::$obj)) {
            self::$obj = new Template();
        }
        return self::$obj;
    }

    /**
     * 注册模板变量
     *
     * 注册模板变量后在模板里就可以直接使用该变量,注册与被注册变量名不一定要一样
     * 如:$template->assign('var', $value);
     * 意思是将当前变量$value注册成模板变量var,在模板里就可以直接调用$val
     *
     * @param string $var 注册到模板里的变量名的字符串形式,不包含$
     * @param mixed $value 需要注册的变量
     */
    public function assign($var, $value) {
        if (is_array($var)) {
            foreach ($var as $key => $val) {
                $this->vars[$key] = $val;
            }
        } else {
            $this->vars[$var] = $value;
        }
    }

    /**
     * 解析模板文件
     *
     * 解析模板,并将变量植入模板,解析完后返回字符串结果
     *
     * @param unknown_type $templates
     * @return unknown
     */
    public function fetch($templates) {
        if (is_array($templates)) {
            $this->templates = $templates;
        } else {
            $this->templates = func_get_args();
        }
        extract($this->vars);

        $this->_contents = '';
        foreach ($this->templates as $this->template) {
            ob_end_clean();
            ob_start();
            $this->_path = $this->getPath($this->template);
            require $this->_path;
            $this->_content = ob_get_contents();
            ob_end_clean();
            ob_start();
            $this->_contents .= $this->_content;
            $this->contents[$this->template] = $this->_content;
        }
        return $this->_contents;
    }

    public function getPath($path) {
        $path = explode(".", $path);
        $num = count($path);
        if ($num == 1) {
            return ROOT_PATH . "template" . DIRECTORY_SEPARATOR . $path[0] . ".html";
        } elseif ($num > 1) {
            $templatePath = '';
            $templatePath = $path[$num - 1];
            array_pop($path);
            $templatePath = ROOT_PATH . implode(DIRECTORY_SEPARATOR, $path) . DIRECTORY_SEPARATOR . 'template' . DIRECTORY_SEPARATOR . $templatePath . ".html";
            return $templatePath;
        } else {
            return false;
        }
    }

    public function display($templates = array()) {
        if (!is_array($templates)) {
            $templates = func_get_args();
        }
        if (empty($templates)) {
            foreach ($this->templates as $this->template) {
                echo $this->contents[$this->template];
            }
        } else {
            echo $this->fetch($templates);
        }
    }
}

//end of script
<?php
/**
 * 模板扩充函数
 *
 * Copyright(c) 2005 by 陈毅鑫(深空). All rights reserved
 *
 * To contact the author write to {@link mailto:shenkong@php.net}
 *
 * @author 陈毅鑫(深空)
 * @version $Id: template.func.php 1687 2008-07-07 01:16:07Z skchen $
 * @package Template
 */

defined('FW') || exit(header('HTTP/1.0 400 Bad Request'));

/**
 * 包含模板
 *
 * 当你需要在主模板文件里(有些模板引擎称之为layout布局模板,其实不是所有模板都是布局)
 * 再包含其他公共模板的时候,使用该函数进行包含,则所有已注册的变量均可在被包含文件里使
 * 用,貌似支持多层嵌套,没有测试过,参数可以使用数组,也可以使用多个参数,如:
 * <?=includeFile('user.header', 'user.main', 'user.footer')?> 或者
 * <?=includeFile(array('user.header', 'user.main', 'user.footer'))?>
 *
 * @param string|array $filename 模板名(module.templateName形式)
 */
function includeFile($templates) {
    $template = Template::init();
    if (is_array($templates)) {
        $template->includeFiles = $templates;
    } else {
        $template->includeFiles = func_get_args();
    }
    extract($template->vars);
    foreach ($template->includeFiles as $template->includeFile) {
        require $template->getPath($template->includeFile);
    }
}

//end of script