CTE Template Engine written in PHP

Background

When the server side architecture of the hej.nu project was starting to move towards the MVC (Model View Controller) pattern, a need for a template engine arose. The template engine would then be responsible for making sure that no code other than that for presenting data slipped into the "View" layer.

At that time, Smarty was the most popular (free) template engine on the market. Smarty would have been the perfect choice if it wasn't for the fact that it was lacking (at least at that time) certain behaviour. Honestly, I cannot recall exactly what that behaviour was, but it was important enough to motivate me to build my own template engine instead. Ok ok, the thought of implementing a template engine made me a bit thrilled too, and this may have influenced my decision a bit.

So I started off implementing the first version of this template engine, called CTE. The approach was to include support for everything that I thought I would need from Smarty, and then extend it to further suit my needs. The first implementation was, even if encapsulated in a class, strictly imperative; functions, states and all the mess that often results from that.

With time I got deeper into the world of object oriented programming (OOP) and patterns (as described on the hej.nu project page). I realized that there was much more to thinking in patterns and objects than having your web application implemented according to the MVC pattern (MVC was a main topic in my first patterns/OOP book). Books like Domain Driven Design: Tackling Complexity in the Heart of Software by Eric Evans helped me take the objects and patterns thinking to any area, including (but absolutely not limited to) that of implementing a template engine.

Having experienced difficult maintenance, weird bugs, the complexity of adding new features and, last but not least, the complexity of testing what essentially was one BIG class, I was very tempted to rewrite the whole thing. And that was precisely what I did.

The first thing I did was to analyze the whole domain covered by CTE. The focus was to see how I could translate this domain naturally into a set of collaborating classes. A part of the result of that analysis can be seen in this class diagram. Once the analysis and modeling was more or less settled, I started implementing. And it went fast, really fast. Not only because I've done it before, but because the code became so much less error prone, that it was a pleasure to write it. I really felt (and experienced) that I could rely on it.

I continued implementation to somewhere between an alpha and a beta version. The reason it stopped there was the fact that I stopped developing the hej.nu project. And as CTE was primarily written that project, continuing developing it for itself was not a priority.

Template Code Examples

Examples of code that could be used in templates managed by CTE. Click/tap each on code block to see the corresponding CTE-generated PHP code. Note that while CTE compressed the generated PHP code maximally, here it has been formatted to enhance readability.



Simple foreach
{foreach source=$persons item='myItem'}
  {$myItem}
{/foreach}

<?php
if (isset($this->v_initial['persons']) &&
    (is_array($this->v_initial['persons']) || is_object($this->v_initial['persons'])) &&
    count($this->v_initial['persons'])) { 
  foreach ($this->v_initial['persons'] as $this->v_foreach[1][3][0]['myItem']) { ?>
    <?php echo $this->v_foreach[1][3][0]['myItem']; ?>
<?php }} ?>



Foreach with all its features
{foreach source=$persons id='f1' key='myKey' item='myItem' enable='iteration'}
  {$cte.foreach.f1.iteration} Print iteration number
  {$myKey} Print key
  {$myItem} Print item
{foreachelse}
{/foreach}

<?php
if (isset($this->v_initial['persons']) &&
    (is_array($this->v_initial['persons']) || is_object($this->v_initial['persons'])) &&
    count($this->v_initial['persons'])) {
  
  $this->v_foreach[1]['f1']['iteration'] = 1;
  
  foreach ($this->v_initial['persons'] as $this->v_foreach[1]['f1'][0]['myKey'] => $this->v_foreach[1]['f1'][0]['myItem']) { ?>
    <?php echo $this->v_foreach[1]['f1']['iteration']; ?>
    <?php echo $this->v_foreach[1]['f1'][0]['myKey']; ?>
    <?php echo $this->v_foreach[1]['f1'][0]['myItem']; ?>
<?php }} else { ?>
<?php } ?>



Simple if-statement (using natural language)
{if $foo.bar equals $user}
  Foobar!
{/if}

<?php
if ($this->v_initial['foo']['bar'] == $this->v_initial['user']) { ?>
  Foobar!
<?php } ?>



Simple if-statement (using standard comparison operators)
{if $foo.bar == 'foobar!' && $user == $foo.bar}
  Woho!
{/if}

<?php
if ($this->v_initial['foo']['bar'] == 'foobar!' &&
    $this->v_initial['user'] == $this->v_initial['foo']['bar']) { ?>
  Woho!
<?php } ?>



Advanced if-statement (using natural language)
{if ($page is equal to 0 or $page is greater than 77) and $isNewYear}
  Happy new year!
{elseif not $isNewYear}
  No new year yet.
{else}
  No comment!
{/if}

<?php
if (($this->v_initial['page'] == 0 || $this->v_initial['page'] > 77) &&
    $this->v_initial['isNewYear']) { ?>
  Happy new year!
<?php } elseif (!$this->v_initial['isNewYear']) { ?>
  No new year yet.
<?php } else { ?>
  No comment!
<?php } ?>



Advanced if-statement demonstrating math operators)
   (using shortened natural language)
{if $page is set and ($page gt 0 or $page is div by 3) and $user not empty}
  Current page number is divisible by 3!
{elseif $page eq $maxPage-1}
  Next page is the last one!
{/if}

<?php
if (isset($this->v_initial['page']) &&
    ($this->v_initial['page'] > 0 || $this->v_initial['page'] % 3 == 0) &&
    !empty($this->v_initial['user'])) { ?>
  Current page number is divisible by 3!
<?php } elseif ($this->v_initial['page'] == $this->v_initial['maxPage'] - 1) { ?>
  Next page is the last one!
<?php } ?>



If-statement demonstrating dynamic strings) (using natural language)
{if $user equals "{$foo.bar} Freeman"}
  Hello!
{/if}

<?php if ($this->v_initial['user'] == $this->v_initial['foo']['bar'] . ' Freeman') { ?>
  Hello!
<?php } ?>



Printing various kinds of variables and expressions
{$fruit->color} Print object property.
{$fruit->slice(4)} Print object method.
{$someObject->someAssociativeArray.peter}
{$foo.myStaticKey} Print value in associative array statically.
{$foo.bar.message} Print values of multidimensional arrays.
{$foo[$someKey]} Print value from associative array dynamically.
{$foo[$someKey].message} Print by combining dynamic with static access.
{2 * (2 + (10 * 2)) * 2 - 11} Print math expression without variables.
{$cte.time + (77 * 10)} Print math expression with variable.

<?php echo $this->v_initial['fruit']->color; ?>
<?php echo $this->v_initial['fruit']->slice(4); ?>
<?php echo $this->v_initial['someObject']->someAssociativeArray['peter']; ?>
<?php echo $this->v_initial['foo']['myStaticKey']; ?>
<?php echo $this->v_initial['foo']['bar']['message']; ?>
<?php echo $this->v_initial['foo'][$this->v_initial['someKey']]; ?>
<?php echo $this->v_initial['foo'][$this->v_initial['someKey']]['message']; ?>
77
<?php echo time()+(77*10); ?>



Modifying output
{$username|upper} Print $username in upper case.
{$username|default:'N/A'} Print $username or 'N/A' if not set/empty.
{$username|lower|default:'N/A'} Combining modifiers.

<?php echo strtoupper($this->v_initial['username']); ?>
<?php echo $this->v_plugin['default']->apply($this->v_initial['username'], array('N/A')); ?>
<?php echo $this->v_plugin['default']->apply(strtolower($this->v_initial['username']), array('N/A')); ?>



Simple loop
{section id='p' source=$persons}
  Name: {$persons[p].name}
  Age: {$persons[p].age}
{sectionelse}
  No persons found.
{/section}

<?php
if (isset($this->v_initial['persons']) &&
    is_array($this->v_initial['persons']) &&
    ($s2size = count($this->v_initial['persons'])) > 0) {
    for ($s2i = 0, $s2it = 1; $s2i < $s2size; $s2i++, $s2it++) { ?>
  Name: <?php echo $this->v_initial['persons'][$this->v_section[1]['p']['index']]['name']; ?>
  Age: <?php echo $this->v_initial['persons'][$this->v_section[1]['p']['index']]['age']; ?>
<?php }} else { ?>
  No persons found.
<?php } ?>



Loop with optional iteration attributes
{section id='p' source=$persons start=1 max=5 step=2}
	{$persons[p].name}
{/section}

<?php
if (isset($this->v_initial['persons']) &&
    is_array($this->v_initial['persons']) &&
    ($s6size = count($this->v_initial['persons'])) > 0) {
    $s6max = min($s6size, 5);
    for ($s6i = 1, $s6it = 1; $s6i < $s6max; $s6i += 2, $s6it++) { ?>
      Name: <?php echo $this->v_initial['persons'][$this->v_section[1]['p']['index']]['name']; ?>
<?php }} ?>



Loop with the enable-attribute
{section id='p' source=$persons enable='first,last,size,index,iteration'}
  {if $first}First iteration!{/if}
  The current index is {$index}.
  This is iteration number {$iteration}.
  {if $last}Last iteration!{/if}
{/section}

<?php
if (isset($this->v_initial['persons']) &&
    is_array($this->v_initial['persons']) &&
    ($this->v_section[1]['p']['size'] = count($this->v_initial['persons'])) > 0) {

  $this->v_section[1]['p']['first'] = true;
  $this->v_section[1]['p']['last'] = false;

  for ($this->v_section[1]['p']['index'] = 0, $this->v_section[1]['p']['iteration'] = 1;
       $this->v_section[1]['p']['index'] < $this->v_section[1]['p']['size'];
       $this->v_section[1]['p']['index']++, $this->v_section[1]['p']['iteration']++) {

    if ($this->v_section[1]['p']['iteration'] == 2) {
      $this->v_section[1]['p']['first'] = false;
    }

    if($this->v_section[1]['p']['iteration'] == $s7max) {
      $this->v_section[1]['p']['last'] = true;
    } ?>

    <?php $this->v_section[1]['p']['first']) { ?>
      First iteration!
    <?php } ?>
    The current index is <?php echo $this->v_section[1]['p']['index']; ?>.
    This is iteration number <?php echo $this->v_section[1]['p']['iteration']; ?>.
    <?php $this->v_section[1]['p']['last']) { ?>
      Last iteration!
    <?php } ?>
<?php }}?>


Practical Examples

Some examples illustrating how CTE was used in the hej.nu project.

Template Example


<?xml version="1.0" encoding="{$lang.env.charset}"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset={$lang.env.charset}" />
    <meta http-equiv="imagetoolbar" content="no" />
    <title>{$lang.env.cycom_short_url}</title>
    <link rel="stylesheet" type="text/css" href="{#CSS_ROOT}{eval@load_css include="ins_inner_base"}" />
    <script type="text/javascript" src="{eval@load_js scramble_level="low"}"></script>
  </head>
  {subtemplate src="ii.body_start.stpl"}
    <div id="chead"><div id="chead_title">{$lang.txt.news__title_news}</div></div>
    <div id="test"></div>
    Waiting for news system ...
  {subtemplate src="ii.body_end.stpl"}
</html>

This code snippet illustrates usage of language-specific strings, CSS inclusion (which resulted in CTE-parsing of the CSS file and the files it included, rendering one big and compressed CSS file), Javascript inclusion (which also resulted in CTE parsing of the javascript file and the files it included, rendering one big and compressed Javascript file) and subtemplate inclusion (all features were supported in a subtemplate, including the subtemplate tag). The rendered CSS and Javascript files were of course cached (and so was each template file).

Subtemplate Example


<body>
  <div id="content">
    {* Add developer toolbar: *}
    {if #CYCOM_INDEVMODE}
      <div id="dev_toolbar">
        <a href="/_dev/request.php?r=tplreload&file={$ENV.D.cte.compiled_tpl_rpath}" class="dev">Recompile template</a><br />
        {if $ENV.D.cte.compile_time != -1}
          CTE compile @ {$ENV.D.cte.compile_time}s<br />
        {/if}
        Total @ {nreq@#PAGE_LOAD_TIME}s
      </div>
    {/if}
    {ph_isset &page_title}
      <div id="chead"><div id="chead_title">{&page_title}</div></div>
    {/ph_isset}

This is the 'ii.body_start.stpl' file, which was included as a subtemplate in the previous example. Note the {ph_isset} block and the {&page_title} tag. The {ph_isset} stands for "placeholder is set" and the {&page_title} tag inserts a placeholder (which can be a static string, a variable, a constant, etc). A placeholder is set in the {subtemplate} block head by adding an attribute, for example: {subtemplate src="foo.stpl" page_title=$lang.news_title}}.

Javascript Example


@{
@include "lib.browser.js"
@include "lib.event.js"
@include "function.css_setPos.js"
@include "lib.inp_radio.js"
@include "obj.list.js"
@include "obj.aarray.js"
@include "function.objclone.js"
@}

// Javascript code ...

This is how CTE-interpreted javascript inclusion looked like.

Image Assembler Example


{eval@make_button type="top_menu" ext=no label="Contact Us"}

Generates a button image out of a left part, a middle part that is extended to fit the label/text and a right part. Returns the URL of the generated image. This plugin has not yet been migrated to the OOP version of CTE, but old plugin code can be found here and the image generation code can be found here.

Work Log

From version 0.9a an attempt has been made to document as much as possible of the work done on CTE, including time estimates. The result, which should be seen as an underestimation, can be found in this document (this log covers this project, the hej.nu project and the shopping list project).

The worklog covers 770 hours of the total work done on CTE. An estimation based on the progress speed in the log would be that a total of between 1000 hours and 1400 hours have been spent working on the CTE Template Engine project.

Please note that the documented work is the effective work; i.e. no lunch, unrelated browsing or other significant breaks were included but strictly the time spent on focused and productive work (I used a physical timer which I stopped as soon as I went away for a break or started to do something unrelated). In addition to that, I was hard on myself and did not include so-called "unproductive days"; these simply appear as having zero worked hours. However, far from all days with a zero working time were "unproductive"; some were simply spent on studying, building projects or even vacation.

Codebase






All work done by Lukas Kalinski during 2004-2007.
For any questions, please write to lukas at hej dot nu.