Note: This article has been updated. The old version is available here.
License Note: Please consider the code in this article public domain.
In general, template engines are a "good thing." I say that as a long time PHP/perl programmer, user of many template engines (fastTemplate, Smarty, perl's HTML::Template), and author of my own, bTemplate. However, after some long discussions with a co-worker, I've decided that the vast majority of template engines (including my own) simply have it wrong. I think the one exception to this rule would be Smarty, although I think it's simply too big, and considering the rest of this article, pretty pointless.
Let's delve a little into the background of template engines. Template engines were designed to allow the separation of business logic (say, getting data from a database) from the presentation of data. Template engines solved two major problems. 1) How to achieve this separation, and 2) How to separate "complex" php code from the HTML. This, in theory, allows HTML designers with no PHP experience to modify the look of the site without having to look at any PHP code.
Template systems have also introduced some complexities. First, we now have one "page" built off multiple files. In general, you have the main PHP page in charge of business logic, an outer "layout" template in charge of rendering the main layout of the site, an inner content-specific template, a database abstraction layer, and the template engine itself (which may or may not be built of multiple files). This is an incredible amount of files to generate a single page. Considering the PHP parser is pretty fast, it's probably not that important unless your site gets insane amounts of traffic.
However, keep in mind that template systems introduce yet another level of processing. Not only do the template files have to be included, they also have to be parsed (depending on the template system, this happens in different ways - regular expressions, str_replaces, compiling, etc.). This is why template benchmarking came to be. Since template engines use a variety of different methods of parsing, some are faster and some are slower (also keep in mind, some template engines offer more features than others).
So basically what we have going on here is a scripting language (PHP) written in C. Inside this embedded scripting language, you have yet another pseudo-scripting language (whatever tags your template engine supports). Some offer simple variable interpolation and loops. Others offer conditionals and nested loops. Still others (well, Smarty at least) offer an interface into pretty much all of PHP.
Why do I say Smarty has it closest to right? Simply stated, because Smarty's goal is "the separation of business logic from presentation," not "separation of PHP code from HTML code." While this seems like a small distinction, it is one that's very important. The ultimate goals of template engines shouldn't really be to remove all logic from HTML. It should be to separate presentation logic from business logic.
There are plenty of cases where you simply need logic to display your data correctly. For instance, say your business logic is to retrieve a list of users in your database. Your presentation logic would be to display the user list in 3 columns. It would be silly to modify the user list function to return 3 arrays. After all, it shouldn't be concerned with what's going to happen to the data. Without some sort of logic in your template file, that's exactly what you would have to do.
While Smarty gets it right in that sense (it allows you to harness pretty much every aspect of PHP), there are still some problems. Basically, it just provides an interface to PHP with new syntax. When stated like that, it seems sort of silly. Is it actually more simple to write {foreach --args} than <? foreach --args ?>? If you do think it's simpler, consider this. Is it so much simpler that there is value in including a huge template library to get that separation? Granted, Smarty offers many other great features (caching, for instance), but it seems like the same benefits could be gained without the huge overhead of including the Smarty class libraries.
I'm basically advocating a "template engine" that uses PHP code as it's native scripting language. I know, this has been done before. When I read about it, I thought simply, "what's the point?" After examining my co-worker's argument and implementing a template system that uses straight PHP code, but still achieves the ultimate goal of separation of business logic from presentation logic (and in 40 lines of code!), I have realized the advantages and honestly, can probably never go back.
While I think this method is far superior, there are of course some issues. The first argument against such a system (well, the first argument I would expect) is that PHP code is too complex, and that designers shouldn't be bothered with learning PHP. In fact, PHP code is just as simple (if not more so) as the syntax of the more advanced template engines (such as Smarty). Also, you can use PHP short-hand like this: <?=$var;?>. Honestly, is that any more complex than {$var}? Sure, it's a few characters shorter, but if you can get used to it, you get all the power of PHP without all the overhead of parsing a template file.
Here's a simple example of a user list page.
<?php
require_once('./includes/global.php');
$tpl = & new Template('index.tpl'); // this is the outer template
$tpl->set('title', 'User List');
$body = & new Template('body.tpl'); // This is the inner template
/*
* The get_list() method of the User class simply runs a query on
* a database - nothing fancy or complex going on here.
*/
$body->set('user_list', $user->get_list());
$tpl->set('content', $body);
echo $tpl->fetch('index.tpl');
?>
Here's a simple example of the template to display the user list. Note that the special foreach
and endforeach;
syntax is documented in the manual.
<table cellpadding="3" border="0" cellspacing="1" bgcolor="#CCCCCC">
<tr>
<td bgcolor="#F0F0F0">Id</td>
<td bgcolor="#F0F0F0">Name</td>
<td bgcolor="#F0F0F0">Email</td>
<td bgcolor="#F0F0F0">Banned</td>
</tr>
<?php foreach($users as $user): ?>
<tr>
<td bgcolor="#FFFFFF" align="center"><?=$user['id'];?></td>
<td bgcolor="#FFFFFF"><?=$user['name'];?></td>
<td bgcolor="#FFFFFF"><a href="mailto:<?=$user['email'];?>"><?=$user['email'];?></a></td>
<td bgcolor="#FFFFFF" align="center"><?=($user['banned'] ? 'X' : ' ');?></td>
</tr>
<?php endforeach; ?>
</table>
Here's a simple example of the layout.tpl (the template file that defines what the whole page will look like).
<html>
<head>
<title><?=$title;?></title>
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1><?=$title;?></h1>
<?=$content;?>
</body>
</html>
And finally, the Template class. Note that there is caching code in here as well (for those that have been asking for it). I'm not entirely certain I'm happy with the caching API, as this is just a quick example of how easy it is to implement. There are also more samples of each at the bottom of the following example. You can see the following file in action here, but don't expect much, it's a very simple example.
<?php
class Template {
var $vars; /// Holds all the template variables
/**
* Constructor
*
* @param $file string the file name you want to load
*/
function Template($file = null) {
$this->file = $file;
}
/**
* Set a template variable.
*/
function set($name, $value) {
$this->vars[$name] = is_object($value) ? $value->fetch() : $value;
}
/**
* Open, parse, and return the template file.
*
* @param $file string the template file name
*/
function fetch($file = null) {
if(!$file) $file = $this->file;
extract($this->vars); // Extract the vars to local namespace
ob_start(); // Start output buffering
include($file); // Include the file
$contents = ob_get_contents(); // Get the contents of the buffer
ob_end_clean(); // End buffering and discard
return $contents; // Return the contents
}
}
/**
* An extension to Template that provides automatic caching of
* template contents.
*/
class CachedTemplate extends Template {
var $cache_id;
var $expire;
var $cached;
/**
* Constructor.
*
* @param $cache_id string unique cache identifier
* @param $expire int number of seconds the cache will live
*/
function CachedTemplate($cache_id = null, $expire = 900) {
$this->Template();
$this->cache_id = $cache_id ? 'cache/' . md5($cache_id) : $cache_id;
$this->expire = $expire;
}
/**
* Test to see whether the currently loaded cache_id has a valid
* corrosponding cache file.
*/
function is_cached() {
if($this->cached) return true;
// Passed a cache_id?
if(!$this->cache_id) return false;
// Cache file exists?
if(!file_exists($this->cache_id)) return false;
// Can get the time of the file?
if(!($mtime = filemtime($this->cache_id))) return false;
// Cache expired?
if(($mtime + $this->expire) < time()) {
@unlink($this->cache_id);
return false;
}
else {
/**
* Cache the results of this is_cached() call. Why? So
* we don't have to double the overhead for each template.
* If we didn't cache, it would be hitting the file system
* twice as much (file_exists() & filemtime() [twice each]).
*/
$this->cached = true;
return true;
}
}
/**
* This function returns a cached copy of a template (if it exists),
* otherwise, it parses it as normal and caches the content.
*
* @param $file string the template file
*/
function fetch_cache($file) {
if($this->is_cached()) {
$fp = @fopen($this->cache_id, 'r');
$contents = fread($fp, filesize($this->cache_id));
fclose($fp);
return $contents;
}
else {
$contents = $this->fetch($file);
// Write the cache
if($fp = @fopen($this->cache_id, 'w')) {
fwrite($fp, $contents);
fclose($fp);
}
else {
die('Unable to write cache.');
}
return $contents;
}
}
}
/**
* Example of file-based template usage. This uses two templates.
* Notice that the $bdy object is assigned directly to a $tpl var.
* The template system has built in a method for automatically
* calling the fetch() method of a passed in template.
*/
$tpl = & new Template();
$tpl->set('title', 'My Test Page');
$tpl->set('intro', 'The intro paragraph.');
$tpl->set('list', array('cat', 'dog', 'mouse'));
$bdy = & new Template('body.tpl');
$bdy->set('title', 'My Body');
$bdy->set('footer', 'My Footer');
$tpl->set('body', $bdy);
echo $tpl->fetch('index.tpl');
/**
* Example of cached template usage. Doesn't provide any speed increase since
* we're not getting information from multiple files or a database, but it
* introduces how the is_cached() method works.
*/
/**
* Define the template file we will be using for this page.
*/
$file = 'index.tpl';
/**
* Pass a unique string for the template we want to cache. The template
* file name + the server REQUEST_URI is a good choice because:
* 1. If you pass just the file name, re-used templates will all
* get the same cache. This is not the desired behavior.
* 2. If you just pass the REQUEST_URI, and if you are using multiple
* templates per page, the templates, even though they are completely
* different, will share a cache file (the cache file names are based
* on the passed-in cache_id.
*/
$tpl = & new CachedTemplate($file . $_SERVER['REQUEST_URI']);
/**
* Test to see if the template has been cached. If it has, we don't
* need to do any processing. Thus, if you put a lot of db calls in
* here (or file reads, or anything processor/disk/db intensive), you
* will significantly cut the amount of time it takes for a page to
* process.
*/
if(!($tpl->is_cached())) {
$tpl->set('title', 'My Title');
$tpl->set('intro', 'The intro paragraph.');
$tpl->set('list', array('cat', 'dog', 'mouse'));
}
/**
* Fetch the cached template. It doesn't matter if is_cached() succeeds
* or fails - fetch_cache() will fetch a cache if it exists, but if not,
* it will parse and return the template as usual (and make a cache for
* next time).
*/
echo $tpl->fetch_cache($file);
?>
In short, the point of template engines should be to separate your business logic from your presentation logic, not separate your PHP code from your HTML code.
This document © Copyright 2003 Brian Lozier (brian@massassi.net)
Last Modified: 12/12/2006 (added license note)