aboutsummaryrefslogtreecommitdiffstats
path: root/posts/back_to_basics_php_templates_part_one.md
diff options
context:
space:
mode:
authorFran├žois Kooman <fkooman@tuxed.net>2020-03-09 12:10:11 +0100
committerFran├žois Kooman <fkooman@tuxed.net>2020-03-09 12:10:11 +0100
commita4e9be087b8eede21f33f6995f26e50d9f17bf3d (patch)
treee1a3405550293c32a5aa7572a07726bafb18178f /posts/back_to_basics_php_templates_part_one.md
parentec3829807ad6a27dcabb749767ee513055bbb727 (diff)
downloadwww.tuxed.net-a4e9be087b8eede21f33f6995f26e50d9f17bf3d.zip
www.tuxed.net-a4e9be087b8eede21f33f6995f26e50d9f17bf3d.tar.gz
www.tuxed.net-a4e9be087b8eede21f33f6995f26e50d9f17bf3d.tar.xz
add post on template engines
Diffstat (limited to 'posts/back_to_basics_php_templates_part_one.md')
-rw-r--r--posts/back_to_basics_php_templates_part_one.md279
1 files changed, 279 insertions, 0 deletions
diff --git a/posts/back_to_basics_php_templates_part_one.md b/posts/back_to_basics_php_templates_part_one.md
new file mode 100644
index 0000000..7c7f398
--- /dev/null
+++ b/posts/back_to_basics_php_templates_part_one.md
@@ -0,0 +1,279 @@
+---
+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.