总体上来说,模板引擎是一个好东西。只是现有的模板引擎,做得不够好。Smrty最接近真理所在,但是它做得太过庞大笨拙了。有没有优雅高效的解决方案?有,本文尝试着讨论一些PHP的模板引擎应该完成什么样的工作,并且怎么制作一个小巧轻量级的解决方案。
By Brian Lozier
这个layout.tpl.php是一个简单的例子(定义了整个页面看上去是什么样子的模板文件)
And here's the parsed output.
而这是解析后的输出。
Beyond The Template Engine
超越模板引擎
By Brian Lozier
译者:taowen
In general, template engines are a "good thing."
总体来说,模板引擎是一个"好东西"
作为一个PHP/Perl的程序员,许多模板引擎(fastTemplate, Smarty, Perl的 HTML::Template)的用户,以及我自己的bTemplate [1]的作者,我讲这句话很多次了。
然而,在同事进行了长时间的讨论之后,我确信了大量的模板引擎(包括我自己写的)根本是错误的。 我想唯一的例外是Smarty [2],虽然我认为它太庞大了,并且考虑到这篇文章的其余部分相当的没有观点。然而,就你为什么选择Smarty(或者类似的解决方案)有几个理由,这些将在文章后面探究。
这篇文章讨论模板的理论。我们将看到为什么大部分"模板引擎"是过于肥大,并且最终我们将回过头来看一个轻量级的,小巧快速的另类选择。
下载和授权
一些关于模板引擎的背景知识
让我们首先研究一下模板引擎的背景知识。模板引擎被设计出来用于把商业逻辑(例如从数据库中获取数据或者计算贸易耗费)从数据的表现分离开来。模板引擎解决了两个主要问题:
- 如何实现这种分离
- 如何从HTML中分离"复杂"的php代码
这从理论上使得没有PHP经验的HTML设计者能够不看任何PHP代码的条件下修改站点的外观。
然而,模板系统也引入了一些复杂性。首先,我们现在有一个从多个文件得来的"页面"。典型的,你可能有一个主PHP页负责业务逻辑,一个外面的"布局"模板把整个站点的整体布局进行渲染,一个内部的内容特定的模板,一个数据库抽象层,以及模板引擎本身(这些可能是也可能不是由多个文件组成)。也有可能,一些人仅仅简单地在每个PHP页面的首尾处包含"头部"和"尾部"文件。
这产生的单个页面的文件数量是很可观的。然而,因为PHP解析器非常快,用到的文件数量可能不是那么重要除非你的站点流量很大。
然而,要记住模板系统引入了另外一个处理的层次。模板文件不仅仅是必须被包含,他们还必须被解析(取决于模板系统,这个行为有很多种方式来完成 —— 使用正则表达式,字符串替换,编译,词法分析,等等)。这就是为什么对模板进行测速变得流行起来:因为模板引擎使用各种方法来解析数据,它们中的一些比另外一些要快(而且,一些模板引擎提供了比其他引擎更加丰富的功能)。
然而,要记住模板系统引入了另外一个处理的层次。模板文件不仅仅是必须被包含,他们还必须被解析(取决于模板系统,这个行为有很多种方式来完成 —— 使用正则表达式,字符串替换,编译,词法分析,等等)。这就是为什么对模板进行测速变得流行起来:因为模板引擎使用各种方法来解析数据,它们中的一些比另外一些要快(而且,一些模板引擎提供了比其他引擎更加丰富的功能)。
模板引擎基础知识
简单地说,模板引擎利用了用C写的脚本语言(PHP)。在这些嵌入的脚本语言中,你有另外一个伪脚本语言(无论你的模板引擎支持何种标签)。某些提供了简单的变量改写和循环。另外一些呢,则提供了条件和嵌套循环。而再其他的呢(至少有Smarty)提供了一个PHP的比较大的子集的接口,以及一个缓冲层。
为什么我认为Smarty最接近于正确的方向?因为Smarty的目标是"把业务逻辑从表现中分离出来"而不是"PHP代码和HTML代码的分离"。这看上去区别不大,但是它正是要点所在。任何模板引擎的最终目标不应该是从HTML移除所有的逻辑。它应该是把表现逻辑从业务逻辑中分离出来。
有很多你仅仅需要逻辑来正确显示你的数据的例子。例如,你的业务逻辑是从你的数据库中获取一个用户列表。你的表现逻辑可能是把用户列表用3列显示。可能修改用户列表函数使得它返回3个数组是很笨的办法。毕竟函数不应该关心数据接下来要怎么处理这样的事情。然而,在你的模板文件中缺少一些逻辑,那些正是你要做的事情。
在这点上Smarty是正确的(使得你利用PHP的很多东西),但是仍然有许多问题。基本上,它仅仅提供了一个以新语法访问PHP的接口。以那开始,它看上去不那么聪明了。是不是事实上写
{foreach --args}
比 <? foreach --args ?>
更加简单?如果你认为这样简单一些,问问你自己是不是在包含一个巨大的模板库来到成这种分离时能够看到真正的意义要更加简单一些。诚然,Smarty提供了许多其他很好的特性,但是看上去这些益处能够在不用承担包含Smarty类库的情况下也能获得。别样的解决方案
我主要要鼓吹的一个解决方案是一个使用PHP代码作为它的原生脚本语言的"模板引擎"。我知道这以前有人做过。而且当我第一次看到的时候,我想,"为什么要这样做?",然而我在考虑过我同事的论据之后,并且实现了一个直接使用PHP代码仍然实现了把业务逻辑和表现逻辑分离的最终目标的模板系统时(只用了大约25行代码,不包括注释),我意识到了好处所在。
这个系统给像我们这样的开发者提供了对PHP核心函数的访问权利,我们能够使用他们来格式化输出——像日期格式化这样的任务应该在模板中处理。而且,因为模板是普通的PHP文件,像Zend Performance Suite [6]和PHP Accelerator [7]这样的字节码缓存程序,能够自动缓存模板(因而,它们不需要在每次被访问时都被重新解释执行)。只要你记得把你的模板文件命名为程序能够辨认出是PHP文件的名字(通常,你仅仅需要确保它们有一个.php的后缀),这确实是一个好处。
当我认为这种方法比经典的模板引擎要高明得多时,肯定还有一些要商榷的问题。最明显的反面意见是,PHP代码太复杂了,而且设计者不应该强迫去学习PHP。事实上,PHP代码和像Smarty这样的高级模板引擎的语法差不多简单(如果不是更简单的话)。而且,设计者能够使用像
<?=$var;?>
这样的简写PHP。这要比{$var}
复杂很多?当然,这要长一些,但是如果你习惯了,你能够获得了PHP的威力而且不用承受解析模板文件带来的负担。 第二,而且可能更重要的,在基于PHP的模板中没有固有的安全。Smarty提供了选项在模板文件中彻底禁用PHP代码。它使得开发者能够约束模板能够访问的函数和变量。如果你没有不怀好意的设计者,这不会是什么问题。然而,如果你允许外部的用户上传或者修改模板,我在此展示的基于PHP的解决方案绝对没有任何安全可言!任何代码都能放入模板中并且得到运行。是的,甚至是一个
print_r($GLOBALS)
(这将改有恶意的用户访问脚本中任何变量的权利)。但是,我个人或者工作上写过的项目中,绝大多数不允许最终的用户修改或者上传模板。如果是这样,问题就不存在了。因此现在让我们来看看代码吧。
例子
这是一个简单的用户列表页面的例子。
<?php
require_once('template.php');
/**
* This variable holds the file system path to all our template files.
*/
$path = './templates/';
/**
* Create a template object for the outer template and set its variables.
*/
$tpl = & new Template($path);
$tpl->set('title', 'User List');
/**
* Create a template object for the inner template and set its variables. The
* fetch_user_list() function simply returns an array of users.
*/
$body = & new Template($path);
$body->set('user_list', fetch_user_list());
/**
* Set the fetched template of the inner template to the 'body' variable in
* the outer template.
*/
$tpl->set('body', $body->fetch('user_list.tpl.php'));
/**
* Echo the results.
*/
echo $tpl->fetch('index.tpl.php');
?>
其中有两个值得注意的重要的概念。第一个就是内部和外部模板的概念。外部模板包含定义站点主要外观的HTML代码。而内部模板包含定义站点内容区域的HTML代码。当然,你能够在任意数目的层上有任意数目的模板。因为通常我们给每个区域使用不同的模板对象,所以没有名字空间的问题。例如,我能在内部和外部模板中都有变量叫"title",而不用害怕有什么冲突。
这是一个用来显示用户列表的模板的简单例子。注意特殊的foreach和endforeach;语法在PHP手册中有说明 [8]。它完全是可选择的。
而且,你可能奇怪我为什么要用.php的后缀来命名我的模板文件。呵呵,许多PHP字节码缓存解决方案(比如 phpAccelerator)如果要被认成PHP文件,需要文件有一个.php后缀。因为这些模板是PHP文件,为什么不去获得这些好处?
<table>
<tr>
<th>Id</th>
<th>Name</th>
<th>Email</th>
<th>Banned</th>
</tr>
<? foreach($user_list as $user): ?>
<tr>
<td align="center"><?=$user['id'];?></td>
<td><?=$user['name'];?></td>
<td><a href="mailto:<?=$user['email'];?>"><?=$user['email'];?></a></td>
<td align="center"><?=($user['banned'] ? 'X' : ' ');?></td>
</tr>
<? endforeach; ?>
</table>
这个layout.tpl.php是一个简单的例子(定义了整个页面看上去是什么样子的模板文件)
<html>
<head>
<title><?=$title;?></title>
</head>
<body>
<h2><?=$title;?></h2>
<?=$body;?>
</body>
</html>
And here's the parsed output.
而这是解析后的输出。
<html>
<head>
<title>User List</title>
</head>
<body>
<h2>User List</h2>
<table>
<tr>
<th>Id</th>
<th>Name</th>
<th>Email</th>
<th>Banned</th>
</tr>
<tr>
<td align="center">1</td>
<td>bob</td>
<td><a href="mailto:bob@mozilla.org">bob@mozilla.org</a></td>
<td align="center"> </td>
</tr>
<tr>
<td align="center">2</td>
<td>judy</td>
<td><a href="mailto:judy@php.net">judy@php.net</a></td>
<td align="center"> </td>
</tr>
<tr>
<td align="center">3</td>
<td>joe</td>
<td><a href="mailto:joe@opera.com">joe@opera.com</a></td>
<td align="center"> </td>
</tr>
<tr>
<td align="center">4</td>
<td>billy</td>
<td><a href="mailto:billy@wakeside.com">billy@wakeside.com</a></td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">5</td>
<td>eileen</td>
<td><a href="mailto:eileen@slashdot.org">eileen@slashdot.org</a></td>
<td align="center"> </td>
</tr>
</table>
</body>
</html>
Caching
缓存
因为解决方案简单如斯,实现模板缓存成为了一个非常简单的任务。为了实现缓存,我们有一个二级类,它扩展了原来的模板类。CachedTemplate类事实上使用和原来的模板类相同的API。不同点是我们必须传递缓存的设置给构造函数,并且调用
fetch_cache()
而不是fetch()
。缓存的概念是简单的。简单的说,我们设置一个缓存时间来调表输出应该被保存的时长(以秒为单位)。在产生一个页面的所有工作开展之前,我们必须首先测试页面是否已经被缓存了,而且缓存是否仍然没有过期。如果缓存在这那,我们不需要在去麻烦数据库和业务逻辑来产生页面——我们可以简单地输出原先缓存地内容。
这种方法需要解决唯一地标识缓存文件的问题。如果一个站点是被一个显示基于GET变量的中心脚本所控制,对每个PHP文件只有一个缓存不会有什么帮助。例如,如果
index.php?page=about_us
和用户调用index.php?page=contact_us
得到的显示完全不同。问题是通过给每个页面产生一个唯一的
cache_id
来解决的。为了做到这个目的,我们把事实上被请求的文件变成REQUEST_URI
(基本上就是整个URL:index.php?foo=bar&bar=foo
)。当然,这个转换过程是受到CachedTemplate类控制的,但是要记住的重要的事情是你绝对要在创建CachedTemplate
对象时传递一个唯一的cache_id
。当然下面有例子来说明。使用缓存包括以下步骤。
include()
模板源文件- 创建一个新的
CachedTemplate
对象(并且传递路径,唯一的cache_id
和缓存过期时间给模板) - 测试内容是否已经被缓存了
- 如果还促拿了,显示文件并且结束脚本
- 否则,进行所有的处理并且
fetch()
模板 - 对
fetch_cache()
的调用将自动产生一个新的缓存文件
这个脚本假定你的缓存文件将放到
./cache/
中,因此你必须创建那个目录并且改变它的目录权限(chmod
)使得Web服务器能够写入文件。而且还要注意如果你在编写脚本的过程中发现了错误,错误也会被缓存!因而在你开发的过程中禁用缓存是一个好主意。最好的办法是给cache的生存周期传递0——这样,缓存总是立即就失效了。