Von der Sprache selbst zur kontrollierten Ausgabe
PHP wurde ursprünglich als Templating-Sprache erfunden, lange bevor es zur allgemeinen Programmiersprache wurde, die es heute ist. Genau das macht reines PHP-Templating gefährlich unkontrolliert, weil weder Sandboxing noch automatisches Escaping eingebaut sind. Wir bauen eine minimale eigene Engine mit Variablen-Interpolation und Auto-Escaping und zeigen dabei, welche Mechanik unter der Oberfläche von Twig tatsächlich steckt.
Inhaltsverzeichnis
- 1. Warum PHP selbst schon eine Templating-Sprache ist
- 2. Grenzen von reinem PHP-Templating
- 3. Architektur einer minimalen eigenen Engine
- 4. Variablen-Interpolation implementieren
- 5. Auto-Escaping implementieren
- 6. Kontrollstrukturen minimal ergänzen
- 7. Kompilierung zu PHP-Code und Caching
- 8. Sicherheitsaspekte: Template-Injection vermeiden
- 9. Abgrenzung zu Twig: Verständnis statt Neuentwicklung
- 10. Zusammenfassung
- 11. FAQ
1. Warum PHP selbst schon eine Templating-Sprache ist
PHP begann 1995 als Sammlung von Skripten zur Verarbeitung von Formulardaten, die Abkürzung PHP stand ursprünglich für Personal Home Page Tools. Das öffnende waren von Anfang an dafür gedacht, zwischen reinem HTML-Text und ausführbarem Code hin und her zu wechseln, genau das Grundprinzip jeder Templating-Sprache.
Ein einfaches PHP-Template wie
Hallo = $name ?>
ist deshalb technisch bereits eine vollständige Template-Engine, ganz ohne zusätzliche Bibliothek. Genau diese Nähe zur eigentlichen Programmiersprache ist gleichzeitig Stärke und Schwäche: Sie erlaubt maximale Flexibilität, weil jede PHP-Funktion direkt im Template verfügbar ist, verzichtet dabei aber auf jede Schutzschicht, die eine dedizierte Template-Engine typischerweise mitbringt.2. Grenzen von reinem PHP-Templating
Das größte Problem reiner PHP-Templates ist das Fehlen von automatischem Escaping. Schreibt man = $comment ?> statt = htmlspecialchars($comment) ?>, landet jeder in $comment enthaltene HTML- oder Script-Code ungefiltert in der Ausgabe, ein klassisches Einfallstor für Cross-Site-Scripting. Da PHP dieses Escaping nicht erzwingt, hängt die Sicherheit einer Anwendung vollständig davon ab, dass jeder einzelne Entwickler bei jeder einzelnen Ausgabe manuell daran denkt.
Das zweite Problem ist fehlendes Sandboxing. Ein PHP-Template kann uneingeschränkt beliebige Funktionen aufrufen, Dateien einbinden oder sogar exec() ausführen, weil es letztlich gewöhnlicher PHP-Code ist. Für Templates, die vollständig von Entwicklern kontrolliert werden, ist das unproblematisch, sobald aber Teile eines Templates aus Nutzereingaben oder von Redakteuren ohne technisches Vertrauen stammen, wird diese uneingeschränkte Mächtigkeit zum ernsthaften Sicherheitsrisiko.
3. Architektur einer minimalen eigenen Engine
Eine minimale Templating-Engine braucht im Kern zwei Bausteine: einen Parser, der eine Template-Datei nach speziellen Platzhaltern durchsucht, und einen Compiler, der daraus validen, sicheren PHP-Code erzeugt. Statt eines vollständigen Tokenizers reicht für ein einfaches Variablen-System bereits ein regulärer Ausdruck, der Platzhalter wie {{ variable }} erkennt und durch entsprechenden PHP-Code ersetzt.
Der entscheidende Architekturentscheid ist, dass die Engine niemals rohes PHP direkt aus dem Template heraus ausführt, sondern selbst kontrolliert, welcher PHP-Code für welchen Platzhalter erzeugt wird. Damit bleibt die Mächtigkeit bewusst eingeschränkt: Nur explizit unterstützte Konstrukte wie Variablenausgabe, einfache Bedingungen und Schleifen werden übersetzt, beliebiger PHP-Code aus dem Template ist damit von vornherein ausgeschlossen.
4. Variablen-Interpolation implementieren
Der erste Schritt ist, {{ variable }}-Platzhalter im Template-Text zu finden und durch echten PHP-Code zu ersetzen. Ein regulärer Ausdruck extrahiert den Variablennamen zwischen den doppelten geschweiften Klammern, anschließend fügt der Compiler an dieser Stelle ein = ... ?> mit dem passenden Variablenzugriff ein.
Wichtig ist, den erzeugten PHP-Code in eine separate, kompilierte Datei zu schreiben statt das Template selbst als PHP auszuführen. Das erlaubt später, den Kompilierungsschritt zu cachen und den erzeugten Code auf reine Ausgabe-Anweisungen zu beschränken, ohne dass ein Redakteur, der das Template bearbeitet, jemals echten, potenziell gefährlichen PHP-Code direkt eingeben könnte.
<?php
declare(strict_types=1);
namespace App\Templating;
final class TemplateCompiler
{
public function compileVariables(string $template): string
{
// {{ name }} wird zu <?= $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. Auto-Escaping implementieren
Der Kern der Sicherheit liegt in der escape()-Methode, die jede Variablenausgabe automatisch durchläuft, bevor sie in der HTML-Antwort landet. Standardmäßig nutzt sie htmlspecialchars() mit dem Flag ENT_QUOTES, das sowohl doppelte als auch einfache Anführungszeichen kodiert, und explizit UTF-8 als Zeichensatz, um Encoding-basierte Escaping-Umgehungen zu verhindern.
Damit ein Template dennoch bewusst ungefilterten HTML-Code ausgeben kann, etwa für redaktionell freigegebenen Rich-Text, braucht es eine explizite Ausnahme wie {{{ variable }}} oder eine benannte raw()-Funktion. Entscheidend ist, dass diese Ausnahme immer sichtbar im Template-Code markiert sein muss, damit ein Review sofort erkennt, an welchen Stellen automatisches Escaping bewusst umgangen wird.
<?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');
}
/**
* Bewusste Ausnahme für bereits geprüften HTML-Code,
* MUSS im Template explizit markiert werden
*/
public function raw(mixed $value): string
{
return (string) $value;
}
}
6. Kontrollstrukturen minimal ergänzen
Über reine Variablenausgabe hinaus braucht eine nutzbare Engine mindestens einfache Bedingungen und Schleifen. Ein Platzhalter wie {% if condition %} lässt sich analog zur Variablen-Interpolation über einen weiteren regulären Ausdruck erkennen und in ein übersetzen, ein passendes {% endif %} entsprechend in .
Der pragmatische Kompromiss an dieser Stelle ist, den Ausdruck innerhalb von {% if ... %} nicht selbst zu parsen, sondern ihn direkt als PHP-Bedingung durchzureichen, solange er ausschließlich aus bereits im Kontext vorhandenen Variablen besteht. Wer echte Sandboxing-Garantien braucht, etwa weil Redakteure ohne technisches Vertrauen Templates bearbeiten, müsste an dieser Stelle einen eigenen, eingeschränkten Ausdrucks-Parser bauen statt PHP-Syntax direkt durchzureichen.
7. Kompilierung zu PHP-Code und Caching
Da jede Kompilierung eines Templates aus regulären Ausdrücken und String-Ersetzungen besteht, lohnt es sich, das Ergebnis nicht bei jeder Anfrage neu zu berechnen. Die Engine prüft deshalb zunächst, ob bereits eine kompilierte Version im Dateisystem existiert und ob deren Änderungszeitpunkt neuer ist als der des Quell-Templates, bevor sie den Compiler überhaupt aufruft.
Dieses Muster entspricht exakt dem, was Twig unter der Haube tut: Templates werden beim ersten Aufruf zu regulärem, hochoptimiertem PHP-Code kompiliert und in einem Cache-Verzeichnis abgelegt, jeder nachfolgende Aufruf lädt direkt die bereits kompilierte PHP-Datei, ohne den Kompilierungsschritt zu wiederholen, was den Performance-Unterschied zu direktem PHP-Templating auf praktisch null reduziert.
<?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. Sicherheitsaspekte: Template-Injection vermeiden
Template-Injection entsteht, wenn Nutzereingaben nicht nur als Variablenwert, sondern als Teil der Template-Struktur selbst interpretiert werden, etwa wenn ein Nutzername ungeprüft in einen Template-String eingefügt wird, bevor dieser kompiliert wird. Die Engine aus diesem Artikel ist dagegen strukturell geschützt, solange Nutzereingaben ausschließlich als Werte im Kontext-Array landen und niemals selbst als Template-Quelltext behandelt werden.
Kritisch bleibt der cachePath in der TemplateLoader-Klasse: Würde der Dateiname direkt aus Nutzereingaben gebildet statt über md5() gehasht, entstünde ein Path-Traversal-Risiko. Ebenso wichtig ist, das Cache-Verzeichnis außerhalb des öffentlich erreichbaren Web-Roots zu platzieren, damit kompilierte PHP-Dateien nicht versehentlich direkt über eine URL ausführbar werden.
9. Abgrenzung zu Twig: Verständnis statt Neuentwicklung
Twig neu zu implementieren wäre weder sinnvoll noch realistisch, das Ziel dieses Artikels ist ausschließlich, die zugrunde liegende Mechanik zu verstehen. Twig bietet gegenüber der hier gezeigten Mini-Engine erheblich mehr: echtes Sandboxing über eine SecurityPolicy mit konfigurierbaren erlaubten Tags, Filtern und Funktionen, Template-Vererbung über extends und block, kontextsensitives Escaping, das automatisch zwischen HTML-, JavaScript- und CSS-Kontext unterscheidet, sowie einen ausgereiften, performanten Compiler.
Für Anwendungen mit Redakteuren ohne technisches Vertrauen, mehrsprachigen Templates oder komplexer Vererbungslogik ist Twig fast immer die richtige Wahl. Der Eigenbau lohnt sich dagegen für sehr kleine, vollständig entwicklerkontrollierte Ausgaben, etwa CLI-Reports oder E-Mail-Templates ohne Fremdeingaben, und vor allem, um zu verstehen, warum Twig die Entscheidungen trifft, die es trifft.
| Merkmal | Reines PHP-Templating | Eigene Mini-Engine | Twig |
|---|---|---|---|
| Auto-Escaping | Nicht vorhanden, manuell nötig | Ja, über escape() erzwungen | Ja, kontextsensitiv |
| Sandboxing | Kein Schutz, voller PHP-Zugriff | Eingeschränkt auf definierte Konstrukte | Vollständig über SecurityPolicy |
| Template-Vererbung | Nicht eingebaut | Nicht eingebaut | extends und block eingebaut |
| Performance | Direkt, kein Kompilierungsschritt | Kompiliert und gecacht | Kompiliert und gecacht |
| Eignung | Entwicklerkontrollierte, kleine Ausgaben | Lernprojekte, sehr schlanke Anwendungen | Produktive Anwendungen mit Redakteuren |
Mironsoft
PHP-Modernisierung, Code-Qualität und Legacy-Refactoring
Gewachsener PHP-Code, der niemand mehr gern anfasst?
Wir modernisieren PHP-Codebasen auf aktuelle Sprachstandards, führen statische Analyse und Coding Standards ein und refactorn Legacy-Code Schritt für Schritt, ohne den laufenden Betrieb zu gefährden.
Legacy-Refactoring
Gewachsenen PHP-Code strukturiert und risikoarm modernisieren.
Code-Qualität etablieren
PHPStan, Coding Standards und CI-Checks nachhaltig im Team verankern.
Versions-Upgrade
PHP-Major-Version-Upgrades sicher planen und ohne Ausfallzeit umsetzen.
10. Zusammenfassung
Eigene Templating-Engine: Das Wichtigste auf einen Blick
PHP als Sprache
Das
Fehlendes Escaping
Reines PHP-Templating erzwingt kein Escaping, jede ungeschützte Ausgabe ist ein potenzielles XSS-Risiko.
Kontrollierter Compiler
Eine eigene Engine übersetzt nur explizit unterstützte Platzhalter, beliebiger PHP-Code aus dem Template ist ausgeschlossen.
Twig als Referenz
Twig ergänzt Sandboxing, Vererbung und kontextsensitives Escaping, die für produktive Redaktionssysteme meist nötig sind.