My blog’s templating engine

I first came across a templating engine implementation in phpBB, and the phpBB wiki gives this overview of how it is used.  I felt that the principle of separation of business and presentational logic was a sound implementation goal that helped the maintainability and clarity of the code.  I also liked the concept of preprocessing and caching what is essentially a readable and easily maintainable template into a runtime-efficient cached version, with a lot of processing being hoisted out of the normal page rendering and into the one-off preprocessing.

However, I wasn’t really happy with some aspects of the implementation:

  • The syntax seems to owe a lot to (but is incompatible with) the Serve-side Includes scripting syntax.  This allows you to embed control structures and data parameters in an HTML template.  Hence an if conditional might be written as <!-- IF loop.S_ROW_COUNT is even --> .  All extension tokens use the HTML comment extension, so that the content retains its superficial HTML conformance, even if meaningless in that form.  This just seems so cludgy to me, and fails a basic readability sanity check.
  • The support national language (NL) lookup for language content with could easily be bound at preprocessing, but this is instead bound at runtime in an inefficient way.  So instead of a simple constant string, you get the following convolved code inside the compiled template:
    $this->_rootref['L_ATTACHMENTS'])) ? $this->_rootref['L_ATTACHMENTS'] : 
    ((isset($user->lang['ATTACHMENTS'])) ? $user->lang['ATTACHMENTS'] : '{ ATTACHMENTS }')); ?>
  • The code for variable substitution is equally and quite unnecessarily convolved.
  • For what it does, the whole implementation seems verbose and unwieldy (It’s a total of 1,500 lines of code).

An A+ for the principles, but B for the implementation, so I googled around for a leaner alternative, and came across a blog post, 19 Promising PHP Template Engines.  I downloaded a few and looked at them and settled on Alan Szlosek’s Vemplator 0.6.1: simple, lean, understandable and quite close to what I wanted.  However, I had some time on my hands and there were some aspects that I kind of missed from the phpBB engine (such as NL support), and I also wanted to improve the runtime efficiency of the compiled template.  So I started refactoring and recoding, and when I was done, I don’t think that there’s a line of Alan’s code left, but the design still owes a lot to him, so I’ve left his name in the credits.

So my engine is currently some 260 lines long and does everything I need.  Anyone is free to take a copy and use it under Alan’s licence terms.  So what is it and how does it work?

Format of Templates

  • Substitution and control are marked by braced tokens.  Tokens and base variables are upper case.  The reserved tokens IF ELSE ELSEIF ENDIF FOREACH ENDFOR SWITCH CASE ENDSWITCH have the obvious meanings, but with colon as the field separator so a loop to generate a table for a set of articles might be {FOREACH:ARTICLES:ARTICLE}.  There are also some special tokens.
  • The engine is implemented by a class, and the require language is passed to the constructor.  The NL is embedded in the compiled file name, so the template “article” gets compiled into article_EN.php in the cache directory.  This means that NL strings can be embedded directly into the PHP code outside the <?php ?> escapes.  The TR token is used introduce text that might be translated.  The convention that I’ve adopted is just to use the English text as the key instead of using text keys.  Hence {TR:Password} is an example of a simple NL string.  Note that since NL strings might include a colon, the template engine will accept escaped colons within strings, for example {TR:Article Title\:}.  However, this is just my convention as the actual routine to translate literals is bound to the template object as one of the object creation parameters.
  • Plurals, possessives and embedding variables into text can alter ordering, etc., with language, so TR also supports an extended format which uses PHP printf formatting conventions, for example {TR:%s wrote\::COMMENT[author]} inserts the field ‘ author’ of the variable COMMENT into the string.  Unlike the bare strings, this variant has to generate a printf statement.
  • INCLUDE includes another template in the compiled version, for example {INCLUDE: header} includes the template “header” from the templates directory.  Note that a public class variable inlineIncludes is used to control whether includes are inlined (for performance) or maintained as separate cached files.
  • // introduces a template comment that is dropped in the cached version, for example {IF:ERROR}{// Then a simple error form is displayed}
  • { escapes the opening brace, so that you can embed styles in the HTML, for example: P.outdent {{ margin-left: -1cm; }.
  • Variables in the template are non-reserved uppercase tokens, e.g.  COMMENT, and by convention field names are lower case, also following the PHP string substitution convention on dropping the quotes as in the above TR example.
  • Variables are in fact stored as parameters to a private data object in the template instance.  The preamble of the compiled code contains a loop to map the local variables (e.g.  $varCOMMENT ) in the above example by reference to the corresponding parameter.  A significant advantage of this is that this mapping contains an existence check and if the parameter doesn’t exist then the variable is initialised to an empty string.  This means that constructs such as {IF:ERROR_FLAG} work intuitively even if the error flag parameter is not initalised.  This may seem an overhead but the loop is simple and efficient, and doing this greatly simplifies variable referencing within the body of the compiled template.
  • Here is an example of this generated preamble to give you an idea of the mapping.  (I’ve trimmed the automatically generated variable list).
    <?php function template_article( $data ) {
    $vars = explode (":","title:theme:header_scripts:script:blogtitle:bloguri");
    foreach ( $vars as $var) {
    $ucvar='var'.strtoupper($var);
    if (isset($data->$var)) $$ucvar =& $data->$var;
    else $$ucvar='';
    }?>

The programming interface

  • The constructor new template has five optional parameters:
    • basePath.  The base path for the application.  This defaults to the path defined by the server variable DOCUMENT_ROOT or the current working directory if is not set.
    • langRtn.  The name of the NL lookup routine.  This routine takes the ISO two letter language code and the lookup string and returns the translation.  The default simply returns the lookup string, so {TR:Password} will return “Password”.  So whilst I use the English text as my language key, this is my convention implemented through my lookup routine; it isn’t a design assumption of the template engine.  I also keeps my translations in a database table, rather than a set of map files (as is the norm for this type of application) because I find this more convenient to maintain and I rarely have to query this table when processing web requests (because all of the mainline translations get compiled into the templates).
    • langCode.  The ISO two letter language code for the target language.  This defaults to EN.
    • templatePath and compilePath.  These can be absolute or relative (to basePath).  The defaults are the sub-directories _templates and _cache.
  • There are four data definition methods.  By convention all data elements are lowercase names.  The corresponding HTML template variable is the same name in uppercase:
    • assign.  This can take a (key, value) pair, a keyed array or an object as an argument.  The array and object options can be used to set multiple data items at the same time.  In the case of the array the (key, value) pairs are used and in the case of an object the (property, value) pairs are used.
    • append.  Appends a string to a string data element
    • push.  This follows the array_push() calling convention and appends an element or list array to a data element.
    • clear.  This clears all data elements.
  • The last method output has a single parameter, the template name, and will return the output from the template using the defined data.  Hence a simple example of using the engine might as follows.  Note that querySet is from my extension to the mysqli class interface and is a simple way of returning an complete result set with the individual rows in mysqli::fetch_assoc format
    $page = new template(  $blog_root_dir, 'translationText' );
    $page->assign( array (
    'logged_on_user' => $blog_user,
    'blog_name'      => $blog_title,
    'function'       => $requested_page, 
    'side_articles'  => $blog_sidebar > 0 ? $db->querySet( $sqlA ) : array (),
    'side_keywords'  => $blog_keywords,
    'side_photos'    => $blog_photos  > 0 ? $db->querySet( $sqlP ) : array (),
    ) ); 
    $output = $page->output( 'article' );
    header( 'Etag: "' . md5( $output ) . '"' );
    echo $output;
  • The corresponding fragment of the template listing off the sidebar articles is:
    <div id="side_blogs">
     <h2><a href="./">{TR:Recent Articles}</a></h2>
     {IF:SIDE_ARTICLES}<ul>  {// Display the article list if any articles exist}
      {FOREACH:SIDE_ARTICLES:ARTICLE}
       <li><a href="article-{ARTICLE[id]}">{ARTICLE[title]}</a></li>
      {ENDFOR}
      </ul>
     {ENDIF}
    </div>
  • The engine generates this code fragment in the compiled template from it:
    <div id="side_blogs">
    <h3><a href="./">Recent Articles</a></h3>
    <?php if($varSIDE_ARTICLES) {?><ul>
    <?php foreach( $varSIDE_ARTICLES as $varARTICLE) {?>
    <li><a href="article-<?php echo $varARTICLE['id'];?>"><?php echo $varARTICLE['title'];?></a></li>
    <?php }?>
    </ul>
    <?php }?>
    </div>

Leave a Reply