aboutsummaryrefslogtreecommitdiffstats
path: root/posts/back_to_basics_php_templates_part_one.md
blob: 1c793f8c406c849a19c2469c4f5cb199fee10cac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
---
title: Back to Basics: PHP Templates (Part I)
published: 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](https://twig.symfony.com/), 
[Blade](https://laravel.com/docs/master/blade), 
[Smarty](https://www.smarty.net/) and [Plates](https://platesphp.com/)? 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](https://en.wikipedia.org/wiki/Server_Side_Includes) 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:

```php
<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:

```php
<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](https://www.php.net/manual/en/control-structures.alternative-syntax.php) 
that allow you to avoid using opening and closing brackets making the syntax a
easier to read. Next, the 
[shortcut syntax](https://www.php.net/manual/en/function.echo.php) 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](https://www.php.net/manual/en/function.htmlspecialchars.php);
- Leverage PHP's [output buffering](https://www.php.net/manual/en/ref.outcontrol.php);
- Simplify the use of template variables by using [variable extraction](https://www.php.net/extract).

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

### Escaping

Consider the following PHP code:

```php
<?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
<?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
<?php
ob_start();
$name = 'World!';
include 'page.tpl.php';
$renderedTemplate = ob_get_clean();
```

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

```php
<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
<?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
<?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:

```php
<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
<?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
<?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:

```php
<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](https://lobste.rs/s/dxxagr/back_basics_php_templates_part_i)