---
title: 'The Template Engine'
source: 'https://academia.sh/en/courses/web-application-framework/template-engine'
course: 'The Web Application Framework'
language: en
updated: '2026-10-21T05:03:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Builds a small compiler that turns a template into PHP code, with layout, component and loop constructs'
    - 'Shows with sandbox output that the escaping step runs at every output point'
    - 'Measures when the compilation step is skipped in a compiled-file cache'
    - 'Reads the lack of escaping in the raw output syntax as an explicitly chosen exception'
---

# The Template Engine

A template compiler with layouts, components and loops is built; the lesson measures that escaping runs on every output, that compilation is skipped for an unchanged template, and that escaping is not wired on raw output.

When the object container bound two different things to the same name, which one came back depended on registration order. In the view layer a similar question changes direction: when a template produces output, which step that the developer did not write runs every time? The answer is a step that is assumed to be safe, and this lesson measures that assumption.

The measurement axis of this lesson is the same as in the rest of the course: did the expected part run in this request. Every measurement starts with `Ledger::newRequest()`, parts are recorded with `Ledger::onPath()`, `Ledger::ran()` and `Ledger::defer()`, and the result is read from the `classify()` function. The decision order is the function itself: first **ran**, then **deferred**, then **skipped**, and if none of these applies, **not wired**. The developer names the expected part; the function only says which outcome the named part falls into. This lesson's outcome is **ran**.

**VD1.** Templates are `.html` files under `templates/` in the working directory, and their compiled counterparts are written as `.php` under `cache/`; a real template engine builds a parser, and here regular expressions are enough. **VD2.** Escaping is done with `htmlspecialchars` and only harmless markup is measured; the template data is always an array in the developer's hands. **VD3.** File times have one-second resolution; to keep the measurement deterministic, the time is moved forward with `touch()` and `sleep` is not used.

The file below builds the `Ledger` class and the `classify()` function. It is the same measurement setup as in the previous lessons and is used here as it is.

```php
<?php
// ran.php — did an expected part run in this request?
declare(strict_types=1);

final class Ledger {
    public static array $path = [];        // parts placed on the request's path
    public static array $ran = [];         // parts called within the request
    public static array $deferred = [];    // parts left outside the request
    public static function newRequest(): void { self::$path = self::$ran = self::$deferred = []; }
    public static function onPath(string $part): void { self::$path[] = $part; }
    public static function ran(string $part): void { self::$ran[] = $part; }
    public static function defer(string $part): void { self::$deferred[] = $part; }
}

function classify(string $part): string {
    return match (true) {
        in_array($part, Ledger::$ran, true) => 'ran',
        in_array($part, Ledger::$deferred, true) => 'deferred',
        in_array($part, Ledger::$path, true) => 'skipped',
        default => 'not wired',
    };
}
```

## Compiling a Template into Code

A template engine turns a text file into PHP code, writes that code once, and runs it on every request. The file below contains a compiler with nine rules, a layout mechanism and a component call. `{{ name }}` becomes escaped output, the raw output syntax becomes unescaped output, `{% each ... %}` becomes a loop, `{% layout ... %}` and `{% section ... %}` become the layout, and `{% component ... %}` becomes a component. Escaping is done in the `escape()` function, and the function records itself when it is called; for every `{{ }}` the compiler generates code that calls this function, so the step is not left to the template author.

```php
<?php
// template.php — a small template compiler and engine
declare(strict_types=1);
require_once 'ran.php';

function escape(mixed $value): string {
    Ledger::ran('escaping');
    return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}

final class TemplateEngine {
    public array $sections = [];
    private ?string $layout = null;
    private array $open = [];

    public function __construct(private string $dir, private string $cache) {
        if (!is_dir($cache)) mkdir($cache);
    }

    private function compile(string $source): string {
        $rules = [
            '/\{\{!\s*(\w+)\s*\}\}/'              => '<?= $$1 ?>',
            '/\{\{\s*(\w+)\s*\}\}/'               => '<?= escape($$1) ?>',
            '/\{%\s*each (\w+) as (\w+)\s*%\}/'   => '<?php foreach ($$1 as $$2): ?>',
            '/\{%\s*\/each\s*%\}/'                => '<?php endforeach; ?>',
            '/\{%\s*section (\w+)\s*%\}/'         => '<?php $engine->openSection(\'$1\'); ?>',
            '/\{%\s*\/section\s*%\}/'             => '<?php $engine->closeSection(); ?>',
            '/\{%\s*layout (\w+)\s*%\}/'          => '<?php $engine->selectLayout(\'$1\'); ?>',
            '/\{%\s*yield (\w+)\s*%\}/'           => '<?= $engine->sections[\'$1\'] ?? \'\' ?>',
            '/\{%\s*component (\w+)\s*%\}/'       => '<?= $engine->component(\'$1\', get_defined_vars()) ?>',
        ];
        return preg_replace(array_keys($rules), array_values($rules), $source);
    }

    private function run(string $name, array $data): string {
        $source = "{$this->dir}/$name.html";
        $compiled = "{$this->cache}/$name.php";
        Ledger::onPath('compile');
        if (!is_file($compiled) || filemtime($compiled) < filemtime($source)) {
            Ledger::ran('compile');
            file_put_contents($compiled, $this->compile(file_get_contents($source)));
        }
        $data['engine'] = $this;
        return self::runFile($compiled, $data);
    }

    private static function runFile(string $__file, array $__data): string {
        extract($__data);
        ob_start();
        require $__file;
        return ob_get_clean();
    }

    public function show(string $name, array $data = []): string {
        $page = $this->run($name, $data);
        if ($this->layout === null) return $page;
        Ledger::ran('layout');
        $layout = $this->layout;
        $this->layout = null;
        return $this->run($layout, $data);
    }

    public function component(string $name, array $data): string {
        Ledger::ran('component');
        unset($data['__file'], $data['__data']);
        return $this->run($name, $data);
    }

    public function selectLayout(string $name): void { $this->layout = $name; }
    public function openSection(string $name): void { $this->open[] = $name; ob_start(); }
    public function closeSection(): void { $this->sections[array_pop($this->open)] = ob_get_clean(); }
}

function report(array $parts): void {
    foreach ($parts as $part) printf("  %-10s %s\n", $part, classify($part));
}
```

Compilation runs when the compiled file does not exist or is older than the source. `Ledger::onPath('compile')` is written on every call, `Ledger::ran('compile')` only when the file is written. When a part is placed on the path but never called, `classify()` classifies it as **skipped**; this lesson's opposing measurement rests on that difference.

Three small templates are needed: a main layout, a list page, and one component per row. Each time it runs, the file empties the cache so that the first request always starts from scratch.

```php
<?php
// templates.php — writes three templates and empties the compile cache
if (!is_dir('templates')) mkdir('templates');
if (!is_dir('cache')) mkdir('cache');
foreach (glob('cache/*') as $old) unlink($old);

file_put_contents('templates/main.html', <<<'HTML'
<title>{{ title }}</title>
<main>{% yield content %}</main>
HTML);

file_put_contents('templates/list.html', <<<'HTML'
{% layout main %}
{% section content %}
<ul>
{% each posts as post %}{% component card %}{% /each %}
</ul>
{% /section %}
HTML);

file_put_contents('templates/card.html', "<li>{{ post }}</li>\n");
```

## Seeing Escaping Run

The measurement makes three requests. In the first request the three values in the list are printed with `{{ post }}`; one of the values contains `&`, one is `<b>bold</b>`, and one is a double quote. Expected: the escaping step runs, and the markup stays in the output as text. The second request runs the same template unchanged; compilation should be **skipped** because the compiled file is not older than the source, but escaping still runs on every request, because it sits inside the compiled code. Before the third request, the time of `card.html` is moved forward.

```php
<?php
// escaping.php — escaping runs on every {{ }} output; compilation is skipped on the second request
require_once 'template.php';
require_once 'templates.php';

$engine = new TemplateEngine('templates', 'cache');
$data = ['title' => 'Posts', 'posts' => ['Tom & Jerry', '<b>bold</b>', '"quote"']];

Ledger::newRequest();
echo $engine->show('list', $data), "\n";
report(['escaping', 'layout', 'component', 'compile']);

Ledger::newRequest();
$engine->show('list', $data);
echo "second request\n";
report(['escaping', 'compile']);

touch('templates/card.html', time() + 5);
Ledger::newRequest();
$engine->show('list', $data);
echo "card.html updated\n";
report(['compile']);
```

```
<title>Posts</title>
<main><ul>
<li>Tom &amp; Jerry</li>
<li>&lt;b&gt;bold&lt;/b&gt;</li>
<li>&quot;quote&quot;</li>
</ul>
</main>
  escaping   ran
  layout     ran
  component  ran
  compile    ran
second request
  escaping   ran
  compile    skipped
card.html updated
  compile    ran
```

In the first request all four parts are **ran**. The layout ran because the page template calls the main template after it has run; the component ran because it is called on every turn of the loop; escaping ran at every `{{ }}` point; and compilation ran because the cache was empty. In the second request the distinction is clear: compilation is **skipped**, escaping **ran**. Compilation is a cache decision, escaping is not; even when the decision changes, escaping keeps running because it is inside the compiled code. In the third request only the changed component's file is recompiled, and the main template stays the same. This measurement measures an outcome, not a count: how many times something ran was a question asked in an earlier course and is not asked here.

## The Opposing Measurement: Skipped Compilation

The three requests above already contain the opposing measurement. The compile row of the second request is the case where a step is placed on the path but not called: `Ledger::onPath('compile')` was written, `Ledger::ran('compile')` was not. This means **skipped**. It is not an error; it is the purpose of the cache. The reverse case, where the expected part is never placed on the path at all, is the subject of the next section.

## The Bounding Measurement

The claim that escaping runs on every output is bounded when the raw output syntax is used. The measurement below writes two templates: one with `{{ summary }}`, the other printing the same value with the raw output syntax. The value is the markup `<b>bold</b>`.

```php
<?php
// raw.php — escaping is not wired in the raw output syntax
require_once 'template.php';

file_put_contents('templates/raw.html', "<p>{{! summary }}</p>\n");
file_put_contents('templates/escaped.html', "<p>{{ summary }}</p>\n");
$engine = new TemplateEngine('templates', 'cache');
$data = ['summary' => '<b>bold</b>'];

foreach (['escaped', 'raw'] as $name) {
    Ledger::newRequest();
    echo $engine->show($name, $data);
    echo "$name\n";
    report(['escaping']);
}
```

```
<p>&lt;b&gt;bold&lt;/b&gt;</p>
escaped
  escaping   ran
<p><b>bold</b></p>
raw
  escaping   not wired
```

In raw output, escaping is **not wired**. The compiler never put the escaping call at that point; `onPath()` was not written, and neither was `ran()`. This is why the outcome is not **skipped**. Escaping runs everywhere except at the raw output point, and the deviation is limited to that point. There is no error in the output and no warning; the page is produced. Raw output exists to print trusted markup, but a person who puts the marker on an unescaped value disables the step with their own hand. For this reason raw output is an explicitly chosen exception, and its syntax is deliberately different: it should be distinguishable even when read out of the corner of the eye.

## Summary

- A template engine turns a template into PHP code and writes the compiled file to a cache; layouts, components and loops are part of this translation.
- This lesson's outcome is **ran**: because the escaping step is embedded in the code the compiler generates, it ran at every `{{ }}` point.
- In an unchanged template the compilation step is **skipped**; escaping **ran** on every request regardless of compilation.
- In the raw output syntax escaping is **not wired**: the outcome is not **skipped**, because the part was never placed on the path.
- Raw output is an explicitly chosen exception; its danger is that it gives no error.

## Next Step

A template trusts its data and prints every value given to it. But when a form submitted by a user does not satisfy a rule, does it reach the save step? The next lesson, Forms and Validation, measures validation and examines the save step falling into the **skipped** outcome.
