In general, template engines are a “good thing.”
I say this as a long time PHP/Perl programmer, user of many template engines (fastTemplate, Smarty, Perl’s HTML::Template), and as 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. There are, however, a couple of reasons why you might choose Smarty (or a similar solution), which will be explored later in this article.
This article discusses template theory. We’ll see why most “template engines” are overkill, and finally, we’ll review a lightweight, lightning fast alternative.
Key Takeaways
- Template engines are widely used for separating business logic from presentation, but often introduce unnecessary complexity with multiple files and parsing layers.
- Smarty is highlighted as a notable exception among template engines, praised for its comprehensive feature set but criticized for its size and complexity.
- A proposed alternative solution utilizes plain PHP code within templates, leveraging PHP’s native capabilities for simplicity and performance.
- This PHP-based approach allows for easy integration with PHP byte-code caching solutions, enhancing performance without complex parsing.
- Security concerns arise with PHP-based templates, especially when external users can modify templates, as there is no inherent security to restrict PHP code execution within templates.
- The article introduces a lightweight PHP template system, exemplified through code snippets, that achieves separation of business and presentation logic efficiently.
Download And Licensing
The template class and all the examples used in this article can be downloaded here: template.zip. You may use the code in these files according to the MIT Open Source License as published on OSI.
A Little Background on Template Engines
Let’s first delve into the background of template engines. Template engines were designed to allow the separation of business logic (for example, retrieving data from a database or calculating shipping costs) from the presentation of data. Template engines solved two major problems:
- How to achieve this separation
- How to separate “complex” php code from the HTML
This, in theory, allows HTML designers with no PHP experience to modify the appearance of the site without having to look at any PHP code.
However, template systems have also introduced some complexities. First, we now have one “page” built from multiple files. Typically, you might have the main PHP page responsible for business logic, an outer “layout” template that renders the overall layout of the site, an inner content-specific template, a database abstraction layer, and the template engine itself (which may or may not be comprised of multiple files). Alternatively, some people simply include “header” and “footer” files at the start and end of each PHP page.
This is an incredible number of files to generate a single page. Yet, as the PHP parser is pretty fast, the number of files used is probably not 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 can happen in a range of ways — using regular expressions, str_replaces, compiling, lexical parsing, etc.). This is why template benchmarking became popular: because template engines use a variety of different methods to parse data, some of which are faster than others (also, some template engines offer more features than others).
Template Engine Basics
Basically, template engines utilize a scripting language (PHP) written in C. Inside this embedded scripting language, you have 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 (Smarty, at least) offer an interface into a large subset of PHP, as well as a caching layer.
Why do I think Smarty is closest to right? Because Smarty’s goal is “the separation of business logic from presentation,” not “the separation of PHP code from HTML code.” While this seems like a small distinction, it is one that’s very important. The ultimate goal of any template engine 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, this function shouldn’t be concerned with what’s going to happen to the data. Yet, without some sort of logic in your template file, that’s exactly what you’d have to do.
While Smarty gets it right in that sense (allowing 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. Stated like that, it seems sort of silly. Is it actually simpler to write {foreach --args}
than <? foreach --args ?>
? If you do think it’s simpler, ask yourself whether it’s so much simpler that you can see real value in including a huge template library to achieve that separation. Granted, Smarty offers many other great features, but it seems like the same benefits could be gained without the huge overhead involved in including the Smarty class libraries.
An Alternative Solution
The solution I’m basically advocating is a “template engine” that uses PHP code as its native scripting language. I know this has been done before. And when I first read about it, I thought, “What’s the point?” However, after I examined my co-worker’s argument and implemented a template system that used straight PHP code, yet still achieved the ultimate goal of separation of business logic from presentation logic (and in around 25 lines of code, not including comments), I realized the advantages.
This system provides developers like us access to a wealth of PHP core functions we can use to format output – tasks like date formatting should be handled in the template. Also, as the templates are all straight PHP files, byte-code caching programs like Zend Performance Suite and PHP Accelerator, the templates can be automatically cached (thus, they don’t have to be re-interpreted each time they’re accessed). This is only an advantage if you remember to name your template files such that these programs can recognize them as PHP files (usually, you simply need to make sure they have a .php extension).
While I think this method is far superior to typical template engines, there are of course some issues. The most obvious argument against such a system is that PHP code is too complex, and that designers shouldn’t have to learn PHP. In fact, PHP code is just as simple as (if not simpler than) the syntax of the more advanced template engines such as Smarty. Also, designers can use PHP short-hand like this <?=$var;?>
. Is that any more complex than {$var}
? Sure, it’s a few characters longer, but if you can get used to it, you gain all the power of PHP without the overhead involved in parsing a template file.
Second, and perhaps more importantly, there is no inherent security in a PHP-based template. Smarty offers the option to completely disable PHP code in template files. It allows developers to restrict the functions and variables to which the template has access. This is not an issue if you don’t have malicious designers. However, if you allow external users to upload or modify templates, the PHP-based solution I presented here provides absolutely no security! Any code could be put into the template and run. Yes, even a print_r($GLOBALS)
(which would give the malicious user access to every variable in the script)!
That said, of the projects I’ve worked with personally and professionally, most did not allow end-users to modify or upload templates. As such, this was not an issue. So now, let’s move on to the code.
Examples
Here’s a simple example of a user list page.
<?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');
?>
There are two important concepts to note here. The first is the idea of inner and outer templates. The outer template contains the HTML code that defines the main look of the site. The inner template contains the HTML code that defines the content area of the site. Of course, you can have any number of templates in any number of layers. As I typically use a different template object for each area, there are no namespacing issues. For instance, I can have a template variable called ‘title’ in both the inner and the outer templates, without any fear of conflict.
Here’s a simple example of the template that can be used to display the user list. Note that the special foreach and endforeach; syntax is documented in the PHP manual. It is completely optional.
Also, you may be wondering why I end my template file names with a .php extension. Well, many PHP byte-code caching solutions (like phpAccelerator) require files to have a .php extension if they are to be considered a PHP file. As these templates are PHP files, why not take advantage of that?
<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>
Here's a simple example of the layout.tpl.php (the template file that defines what the whole page will look like).
<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
With a solution as simple as this, implementing template caching becomes a fairly simple task. For caching, we have a second class, which extends the original template class. The CachedTemplate class uses virtually the same API as the original template class. The differences are that we must pass the cache settings to the constructor and call fetch_cache()
instead of fetch()
.
The concept of caching is simple. Basically, we set a cache timeout that represents the period (in seconds) after which the output should be saved. Before doing all the work to generate a page, we must first test to see if that page has been cached, and whether the cache is still considered current. If the cache is there, we don’t need to bother with all the database work and business logic it takes to generate the page — we can simply output the previously cached content.
This practice presents the problem of being able to uniquely identify a cache file. If a site is controlled by a central script that displays output based on GET variables, having only one cache for every PHP file will be unsuccessful. For instance, if you have index.php?page=about_us
, the output should be completely different than that which would be returned if a user called index.php?page=contact_us
.
This problem is solved by generating a unique cache_id
for each page. To do this, we take the actual file that was requested and hash it together with the REQUEST_URI
(basically, the entire URL: index.php?foo=bar&bar=foo
). Of course, the hashing is taken care of by the CachedTemplate
class, but the important thing to remember is that you absolutely must pass a unique cache_id
when you create a CachedTemplate object. Examples follow, of course.
Use of a caching set up involves these steps.
include()
the Template source file
CachedTemplate
object (and pass the path to the templates, the unique cache_id
, and the cache time out)
fetch()
the template
fetch_cache()
call will automatically generate a new cache file
This script assumes your cache files will go in ./cache/
, so you must create that directory and chmod
it so the Web server can write to it. Also note that if you find an error during the development of any script, the error can be cached! Thus it’s a good idea to disable caching altogether while you’re developing. The best way to do this is to pass zero (0) as the cache lifetime — that way, the cache will always expire immediately.
Here’s an example of caching in action.
<?php
/**
* 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.
*/
/**
* First, include the template class.
*/
require_once('template.php');
/**
* Here is the path to the templates.
*/
$path = './templates/';
/**
* Define the template file we will be using for this page.
*/
$file = 'list.tpl.php';
/**
* 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.
*/
$cache_id = $file . $_SERVER['REQUEST_URI'];
$tpl = & new CachedTemplate($path, $cache_id, 900);
/**
* 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.
*
* This should be read aloud as "If NOT Is_Cached"
*/
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);
?>
Setting Multiple Variables
How can we set multiple variables sinultaneously? Here is an example that uses a method contributed by Ricardo Garcia.
<?php
require_once('template.php');
$tpl = & new Template('./templates/');
$tpl->set('title', 'User Profile');
$profile = array(
'name' => 'Frank',
'email' => 'frank@bob.com',
'password' => 'ultra_secret'
);
$tpl->set_vars($profile);
echo $tpl->fetch('profile.tpl.php');
?>
The associated template looks like this:
<table cellpadding="3" border="0" cellspacing="1">
<tr>
<td>Name</td>
<td><?=$name;?></td>
</tr>
<tr>
<td>Email</td>
<td><?=$email;?></td>
</tr>
<tr>
<td>Password</td>
<td><?=$password;?></td>
</tr>
</table>
And the parsed output is as follows:
<table cellpadding="3" border="0" cellspacing="1">
<tr>
<td>Name</td>
<td>Frank</td>
</tr>
<tr>
<td>Email</td>
<td>frank@bob.com</td>
</tr>
<tr>
<td>Password</td>
<td>ultra_secret</td>
</tr>
</table>
Special thanks to Ricardo Garcia and to Harry Fuecks for thei contributions to this article.
Related Links
Here’s a list of good resources for exploring template engines in general.
- Web Application Toolkit Template View – a wealth of information about all types of template approaches
- MVC Pattern – description of 3-tier application design
- SimpleT – another php-based template engine that uses PEAR::Cache_Lite
- Templates and Template Engines – more on various template implementations
- Smarty – compiling template engine
Template Class Source Code
And finally, the Template class.
<?php
/**
* Copyright (c) 2003 Brian E. Lozier (brian@massassi.net)
*
* set_vars() method contributed by Ricardo Garcia (Thanks!)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
class Template {
var $vars; /// Holds all the template variables
var $path; /// Path to the templates
/**
* Constructor
*
* @param string $path the path to the templates
*
* @return void
*/
function Template($path = null) {
$this->path = $path;
$this->vars = array();
}
/**
* Set the path to the template files.
*
* @param string $path path to template files
*
* @return void
*/
function set_path($path) {
$this->path = $path;
}
/**
* Set a template variable.
*
* @param string $name name of the variable to set
* @param mixed $value the value of the variable
*
* @return void
*/
function set($name, $value) {
$this->vars[$name] = $value;
}
/**
* Set a bunch of variables at once using an associative array.
*
* @param array $vars array of vars to set
* @param bool $clear whether to completely overwrite the existing vars
*
* @return void
*/
function set_vars($vars, $clear = false) {
if($clear) {
$this->vars = $vars;
}
else {
if(is_array($vars)) $this->vars = array_merge($this->vars, $vars);
}
}
/**
* Open, parse, and return the template file.
*
* @param string string the template file name
*
* @return string
*/
function fetch($file) {
extract($this->vars); // Extract the vars to local namespace
ob_start(); // Start output buffering
include($this->path . $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 string $path path to template files
* @param string $cache_id unique cache identifier
* @param int $expire number of seconds the cache will live
*
* @return void
*/
function CachedTemplate($path, $cache_id = null, $expire = 900) {
$this->Template($path);
$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.
*
* @return bool
*/
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
*
* @return string
*/
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;
}
}
}
?>
Another important thing to note about the solution presented here is that we pass the file name of the template to the fetch()
method call. This can be useful if you want to re-use template objects without having to re-set()
all the variables.
And remember: the point of template engines should be to separate your business logic from your presentation logic, not to separate your PHP code from your HTML code.
Frequently Asked Questions about PHP Template Engines
What is a PHP Template Engine and why is it important?
A PHP Template Engine is a software that provides a way to separate PHP logic from the frontend HTML/CSS. It’s important because it allows developers to manage and control the presentation of data separately from the business logic. This separation makes the code cleaner, easier to maintain, and more efficient. It also allows designers and developers to work simultaneously without stepping on each other’s toes.
How does a PHP Template Engine work?
A PHP Template Engine works by parsing template files containing placeholders or special syntax for data substitution. It replaces these placeholders with actual data to generate the final HTML. The engine separates the application logic from the presentation logic, making it easier to manage and maintain the code.
What are the benefits of using a PHP Template Engine?
Using a PHP Template Engine has several benefits. It promotes code reusability, as templates can be reused across different parts of the application. It also enhances code readability and maintainability, as the separation of concerns makes the code cleaner and easier to understand. Furthermore, it allows for better collaboration between developers and designers, as they can work on the logic and presentation separately.
Are there any security risks associated with PHP Template Engines?
While PHP Template Engines can provide a layer of security by automatically escaping output, they are not immune to security risks. Developers must still be vigilant about potential vulnerabilities, such as Cross-Site Scripting (XSS) attacks. It’s important to always validate and sanitize user input and to keep the template engine and all other software up-to-date.
How can I choose the right PHP Template Engine for my project?
Choosing the right PHP Template Engine depends on your project’s specific needs. Consider factors such as the engine’s performance, ease of use, community support, and documentation. Also, consider whether the engine supports the features you need, such as caching, inheritance, and extensibility.
Can I create my own PHP Template Engine?
Yes, it’s possible to create your own PHP Template Engine. However, it requires a deep understanding of PHP and may not be the most efficient use of your time, considering the numerous well-tested and feature-rich template engines already available.
What are some popular PHP Template Engines?
Some popular PHP Template Engines include Twig, Smarty, Blade, and Mustache. Each has its own strengths and weaknesses, so it’s important to research and choose the one that best fits your project’s needs.
How can I integrate a PHP Template Engine into my existing project?
Integrating a PHP Template Engine into an existing project involves installing the engine, setting up the template directories, and modifying the code to use the engine for rendering views. The exact process varies depending on the engine and the structure of your project.
Can I use a PHP Template Engine with a framework?
Yes, many PHP frameworks come with their own template engines, but you can also use a standalone template engine if you prefer. For example, Laravel comes with the Blade template engine, but you can also use Twig or any other engine with it.
What is the future of PHP Template Engines?
The future of PHP Template Engines looks promising. With the continuous development of PHP and the increasing complexity of web applications, the demand for efficient and powerful template engines is likely to grow. New features and improvements are constantly being added to existing engines, and new engines are also being developed.
Brian is a contract Web developer currently working at RealImpact, a division of Real Networks that provides content management solutions to non-profit organizations.