From the language itself to controlled output
PHP was originally invented as a templating language, long before it became the general purpose programming language it is today. That very origin makes plain PHP templating dangerously uncontrolled, since neither sandboxing nor automatic escaping are built in. We build a minimal engine with variable interpolation and auto-escaping, and show what mechanism actually powers Twig under the surface.
Table of Contents
- 1. Why PHP itself is already a templating language
- 2. Limits of plain PHP templating
- 3. Architecture of a minimal own engine
- 4. Implementing variable interpolation
- 5. Implementing auto-escaping
- 6. Adding minimal control structures
- 7. Compiling to PHP code and caching
- 8. Security considerations: avoiding template injection
- 9. Where this differs from Twig: understanding, not reinventing
- 10. Summary
- 11. FAQ
1. Why PHP itself is already a templating language
PHP started in 1995 as a collection of scripts for processing form data, the abbreviation PHP originally stood for Personal Home Page Tools. The opening tags were designed from the very start to switch back and forth between plain HTML text and executable code, which is exactly the core principle of any templating language.
A simple PHP template like
Hello = $name ?>
is therefore technically already a complete template engine, without any additional library. That closeness to the actual programming language is both its strength and its weakness: it allows maximum flexibility because any PHP function is directly available inside the template, but it also skips every protective layer a dedicated template engine typically brings along.2. Limits of plain PHP templating
The biggest problem with plain PHP templates is the lack of automatic escaping. Writing = $comment ?> instead of = htmlspecialchars($comment) ?> lets any HTML or script code contained in $comment end up unfiltered in the output, a classic cross site scripting entry point. Since PHP does not enforce this escaping, an application's security depends entirely on every single developer manually remembering it at every single output point.
The second problem is missing sandboxing. A PHP template can call any function without restriction, include files, or even run exec(), because it is ultimately ordinary PHP code. For templates fully controlled by developers, that is unproblematic, but as soon as parts of a template come from user input or from editors without technical trust, that unrestricted power becomes a serious security risk.
3. Architecture of a minimal own engine
A minimal templating engine needs two core building blocks: a parser that scans a template file for special placeholders, and a compiler that turns them into valid, safe PHP code. Instead of a full tokenizer, a simple variable system can get by with a regular expression that recognizes placeholders like {{ variable }} and replaces them with corresponding PHP code.
The critical architectural decision is that the engine never executes raw PHP straight out of the template, it instead controls itself which PHP code gets generated for which placeholder. That keeps its power deliberately limited: only explicitly supported constructs like variable output, simple conditions, and loops get translated, arbitrary PHP code from the template is excluded from the start.
4. Implementing variable interpolation
The first step is finding {{ variable }} placeholders in the template text and replacing them with real PHP code. A regular expression extracts the variable name between the double curly braces, then the compiler inserts a = ... ?> statement with the matching variable access at that spot.
It is important to write the generated PHP code into a separate, compiled file instead of executing the template itself as PHP. That later allows caching the compilation step and limiting the generated code to pure output statements, so an editor working on the template could never enter real, potentially dangerous PHP code directly.
<?php
declare(strict_types=1);
namespace App\Templating;
final class TemplateCompiler
{
public function compileVariables(string $template): string
{
// {{ name }} becomes <?= $engine->escape($name) ?>
return preg_replace_callback(
'/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/',
static function (array $matches): string {
$variable = $matches[1];
return "<?= \$engine->escape(\$context['{$variable}'] ?? '') ?>";
},
$template,
);
}
}
5. Implementing auto-escaping
The core of security lies in the escape() method that every variable output automatically passes through before landing in the HTML response. By default it uses htmlspecialchars() with the ENT_QUOTES flag, which encodes both double and single quotes, and explicitly UTF-8 as the character set to prevent encoding based escaping bypasses.
For a template to still deliberately output unfiltered HTML, for example editorially approved rich text, it needs an explicit exception like {{{ variable }}} or a named raw() function. What matters is that this exception always stays visibly marked in the template code, so a review immediately spots exactly where automatic escaping is intentionally bypassed.
<?php
declare(strict_types=1);
namespace App\Templating;
final class TemplateEngine
{
public function escape(mixed $value): string
{
if ($value === null) {
return '';
}
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
/**
* Deliberate exception for already vetted HTML content,
* MUST be explicitly marked in the template
*/
public function raw(mixed $value): string
{
return (string) $value;
}
}
6. Adding minimal control structures
Beyond plain variable output, a usable engine needs at least simple conditions and loops. A placeholder like {% if condition %} can be recognized analogously to variable interpolation via another regular expression and translated into , with a matching {% endif %} translated into .
The pragmatic compromise here is not parsing the expression inside {% if ... %} itself, but passing it directly through as a PHP condition, as long as it consists exclusively of variables already present in the context. Anyone who needs genuine sandboxing guarantees, for example because editors without technical trust edit templates, would need to build a custom, restricted expression parser here instead of passing PHP syntax straight through.
7. Compiling to PHP code and caching
Since every compilation of a template consists of regular expressions and string replacements, it pays off not to recompute the result on every request. The engine therefore first checks whether a compiled version already exists on the filesystem and whether its modification time is newer than the source template's, before even invoking the compiler.
This pattern matches exactly what Twig does under the hood: templates get compiled into regular, highly optimized PHP code on first invocation and stored in a cache directory, every subsequent call loads the already compiled PHP file directly without repeating the compilation step, reducing the performance difference against direct PHP templating to practically zero.
<?php
declare(strict_types=1);
namespace App\Templating;
final class TemplateLoader
{
public function __construct(
private readonly string $templateDir,
private readonly string $cacheDir,
private readonly TemplateCompiler $compiler,
) {
}
public function load(string $name): string
{
$sourcePath = $this->templateDir . '/' . $name;
$cachePath = $this->cacheDir . '/' . md5($name) . '.php';
$needsCompile = !is_file($cachePath)
|| filemtime($cachePath) < filemtime($sourcePath);
if ($needsCompile) {
$compiled = $this->compiler->compileVariables(
file_get_contents($sourcePath),
);
file_put_contents($cachePath, $compiled);
}
return $cachePath;
}
}
8. Security considerations: avoiding template injection
Template injection happens when user input gets interpreted not just as a variable value but as part of the template structure itself, for example when a username gets inserted unchecked into a template string before it is compiled. The engine from this article is structurally protected against that, as long as user input lands exclusively as values in the context array and is never itself treated as template source code.
The cachePath in the TemplateLoader class stays a critical spot: if the file name were built directly from user input instead of hashed via md5(), a path traversal risk would arise. It is equally important to place the cache directory outside the publicly reachable web root, so compiled PHP files never accidentally become directly executable via a URL.
9. Where this differs from Twig: understanding, not reinventing
Reimplementing Twig would be neither sensible nor realistic, this article's goal is solely to understand the underlying mechanism. Twig offers considerably more than the mini engine shown here: genuine sandboxing via a SecurityPolicy with configurable allowed tags, filters, and functions, template inheritance via extends and block, context aware escaping that automatically distinguishes between HTML, JavaScript, and CSS context, plus a mature, high performance compiler.
For applications with editors who lack technical trust, multilingual templates, or complex inheritance logic, Twig is almost always the right choice. Building it yourself pays off instead for very small, fully developer controlled output, such as CLI reports or email templates without external input, and above all for understanding why Twig makes the decisions it makes.
| Trait | Plain PHP templating | Custom mini engine | Twig |
|---|---|---|---|
| Auto-escaping | Not present, manual effort needed | Yes, enforced via escape() | Yes, context aware |
| Sandboxing | No protection, full PHP access | Limited to defined constructs | Complete via SecurityPolicy |
| Template inheritance | Not built in | Not built in | extends and block built in |
| Performance | Direct, no compilation step | Compiled and cached | Compiled and cached |
| Fit | Developer controlled, small output | Learning projects, very lean apps | Production applications with editors |
Mironsoft
PHP modernization, code quality, and legacy refactoring
Grown PHP code nobody wants to touch anymore?
We modernize PHP codebases to current language standards, introduce static analysis and coding standards, and refactor legacy code step by step without endangering live operations.
Legacy Refactoring
Modernize grown PHP code in a structured, low-risk way.
Establishing Code Quality
Anchor PHPStan, coding standards, and CI checks sustainably in the team.
Version Upgrades
Plan and execute PHP major version upgrades safely, without downtime.
10. Summary
A Custom Templating Engine: The Essentials at a Glance
PHP as a language
The
Missing escaping
Plain PHP templating does not enforce escaping, every unprotected output is a potential XSS risk.
Controlled compiler
A custom engine translates only explicitly supported placeholders, arbitrary PHP code from the template is excluded.
Twig as a reference
Twig adds sandboxing, inheritance, and context aware escaping, features production editorial systems usually need.