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:
- Wanting to really understand how (modern) template engines work;
- 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:
- Use a language specifically designed for the templates, e.g. Twig, Blade, Smarty;
- 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:
- Implement proper template variable escaping;
- Leverage PHP's output buffering;
- Simplify the use of template variables by using variable extraction.
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