Beyond The Template Engine

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.

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:

  1. How to achieve this separation
  2. 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' : '&nbsp;');?></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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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.

  1. include() the Template source file

  • create a new CachedTemplate object (and pass the path to the templates, the unique cache_id, and the cache time out)
  • test to see if the content is already cached
  • if so, display the file and script execution ends
  • otherwise, do all the processing and fetch() the template
  • the 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.

    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.

    Win an Annual Membership to Learnable,

    SitePoint's Learning Platform

    • Thomas

      This article is dated 2003, but the discussion about template engine for PHP is still open, and since PHP didn’t created an easy and strong presentation layer as Ruby, we still have to lay on template engine for a better management of templates. I tried many, Smarty, Twig, which I found complex and overheading what we really need just a separation of the business logic by the presentation logic, which Smarty advertise, but unluckily add so much logic into the presentation that it became almost another language into the language. I instead enjoied Tiny But Strong and Rain TPL, both light and immediate to use that are really useful and time saving. Before pass totally to Ruby on Rails, PHP have still a lot to offer, just chose the right tools to let your life easier :)

    • http://haml-to-php.com Marc Weber

      Well – you’ve forgotten about an important point: Quoting. You should always use htmlentities or such. And if you take that into account PHP looses. Why? There are many ways to quote HTML – but a template engine should only use one correct way – such as htmlentities. Also today there is HTML and XHTML – you want consistent style so that you can check your code using HTML checkers. If you consider those points you’ll note that HAML (-> http://haml-lang.com) got it right – you can quote by = and you can insert verbatim by !=. HAML-TO-PHP implements this correctly. Seeing “$any_value” scares me. Trusting designers to remember using it scares me as well.