François' Blog

Back to Basics: PHP Templates (Part I)

Published on 2020-03-09

This series of blog posts will take you along the process of creating a fully featured, but minimalist PHP template engine from scratch. Why would anyone ever want to do this when there are so many available template engines like Twig, Blade, Smarty and Plates? Good question!

The reasons I came up with are:

  1. Wanting to really understand how (modern) template engines work;
  2. Avoiding big and complicated template "frameworks" as dependencies in your applications.

This first part of the post will compare various template engine concepts and come up with a first version of a template engine that works for many simple cases. Part II will cover template inheritance and part III will talk about internationalization and how to support the concept of template themes.

We will restrict ourselves to PHP, and not talk about e.g. SSI which is also powerful and can be enough for some use cases. Look into that first!

For the purpose of this article I define templates as a means to separate application "logic" from "presentation". In other words: we want to avoid mixing application code with the way it is presented to the user through their browser.

Broadly speaking, there are two different approaches to building PHP templates:

  1. Use a language specifically designed for the templates, e.g. Twig, Blade, Smarty;
  2. Use native PHP for the templates (Plates).

The first approach is what template engines like Smarty, Twig and Blade have been using. The example below shows a simple Twig template:

<html>
    <head><title>{{ pageTitle }}</title></head>
    <body>
        <ul>
{% for myFavoriteAnimal in myFavoriteAnimals %}
            <li>{{ myFavoriteAnimal }}</li>
{% endfor %}
        </ul>
    </body>
</html>

You can see that the Twig project designed its own template syntax. The native PHP foreach loop is replaced by {% for %} and PHP's echo is replaced by {{ ... }}.

A possible reason for creating a new template syntax is that it may be easier to understand for template designers in case they don't understand PHP. Another is that it becomes easier to automatically "escape" template variables to mitigate cross site scripting (XSS).

A drawback of having a custom template syntax is that it can be relatively slow. During page display, the template needs to be parsed first which is slower than directly outputting HTML and running the embedded PHP code. For that reason, all template engines that have their own template syntax have a caching mechanism that converts templates to actual PHP first. Of course, having a caching mechanism introduces its own problems...

When we convert the above Twig example to PHP, it looks like this:

<html>
    <head><title><?=$pageTitle; ?></title></head>
    <body>
        <ul>
<?php foreach ($myFavoriteAnimals as $myFavoriteAnimal): ?>
            <li><?=$myFavoriteAnimal; ?></li>
<?php endforeach; ?>
        </ul>
    </body>
</html>

As you can see, it looks quite similar to the Twig example! This was accomplished by using two neat PHP features: Alternative syntax for control structures that allow you to avoid using opening and closing brackets making the syntax a easier to read. Next, the shortcut syntax of echo allows you to replace <?php echo $v; ?> with <?=$v; ?>, simplifying the template further.

When looking for a minimal template engine, we can't justify creating our own template syntax. We have to leverage PHP itself as much as possible. However, we can make the use of native PHP for templates easier with some neat tricks.

For this we have to explore some concepts of PHP that can help us with that:

It should be noted that the solutions presented below build heavily on the way Plates works.

Escaping

Consider the following PHP code:

<?php
$userId = $_GET['user_id'];
?>
<p>Hello <?=$userId; ?></p>

The problem is that the variable $_GET['user_id'] is not "escaped", i.e. one could inject data by specifying the query parameter user_id with JavaScript code. This is a XSS vulnerability.

The fix is straightforward, but a bit unwieldy in (native) PHP. Most template engines use something like this:

<?php
$userId = $_GET['user_id'];
?>
<p>Hello <?=htmlspecialchars($_GET['user_id'], ENT_QUOTES, 'UTF-8'); ?></p>

Now it is safe to display the value of the user_id query parameter on the page. Of course, when building a template engine, it is not great to need to specify the whole htmlspecialchars command every time you want to output a variable, so we'll have to figure something out for that.

Output Buffering

In PHP you can use ob_start(), ob_get_clean() and some of its variants. This allows you to capture whatever the script sends as output in a variable:

<?php
ob_start();
$name = 'World!';
include 'page.tpl.php';
$renderedTemplate = ob_get_clean();

For example, the file page.tpl.php contains:

<p>Hello <?=$name;?></p>

The variable $renderedTemplate will now contain <p>Hello World!</p>, both the HTML and the output of the evaluated PHP! This concept is very powerful and a fundamental part of our minimalist template engine.

Variable Extraction

Most template engines allow you to specify template variables as an array, e.g.:

<?php
$template->render(
    'template_name',
    [
        'user_id' => 'foo,
        'user_groups' => [
            'admin',
            'employee',
        ]
    ]
);

A template engine can use extract to convert the keys of the array to actual PHP variables. So calling extract on ['user_id' => 'foo'] will actually create the variable $user_id in the current scope. This allows you to use $user_id in your template instead of $templateVariables['user_id'].

With these concepts explained, we can now create the very first version of our Template.php class.

<?php
class Template
{
    public function render($templateName, array $templateVariables = [])
    {
        extract($templateVariables);
        ob_start();
        include $templateName.'.tpl.php';

        return ob_get_clean();
    }
}

The accompanying page.tpl.php contains the following:

<html>
    <head><title><?=$pageTitle; ?></title></head>
    <body>
        <ul>
<?php foreach ($myFavoriteAnimals as $myFavoriteAnimal): ?>
            <li><?=$myFavoriteAnimal; ?></li>
<?php endforeach; ?>
        </ul>
    </body>
</html>

We call the Template class like this, from e.g. index.php:

<?php
$t = new Template();
echo $t->render(
    'page',
    [
        'pageTitle' => 'My Favorite Animals',
        'myFavoriteAnimals' => ['Dog', 'Cat', 'Donkey'],
    ]
);

This works, but as you can see, the variables are not yet escaped and would introduce a potential XSS vulnerability. In order to fix this, we add the method e to the Template class. The Template class thus becomes:

<?php
class Template
{
    public function render($templateName, array $templateVariables = [])
    {
        extract($templateVariables);
        ob_start();
        include $templateName.'.tpl.php';

        return ob_get_clean();
    }

    private function e($v)
    {
        return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
    }
}

As we saw before, the include as used in the output buffering example allows you to use existing variables from your template. The neat trick here is that from the template you can also use $this! So by adding the method e to the Template class we can use $this->e($v) from the template, making the output safe for display in the browser. To make the example complete, we update page.tpl.php like this:

<html>
    <head><title><?=$this->e($pageTitle); ?></title></head>
    <body>
        <ul>
<?php foreach ($myFavoriteAnimals as $myFavoriteAnimal): ?>
            <li><?=$this->e($myFavoriteAnimal); ?></li>
<?php endforeach; ?>
        </ul>
    </body>
</html>

This wraps up the basics. Presented is a fully working minimalist template engine. Stay tuned for the next parts that will talk about template inheritance, internationalization and themes.

UPDATE: see the discussion on lobste.rs

History