Eine eigene Templating-Engine in PHP bauen
AI generated
8.4
PHP · Templating · Engine
Eine eigene Templating-Engine in PHP bauen
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.

15 Min. Lesezeit Auto-Escaping Template-Compiler Sandboxing Twig-Vergleich

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

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 statt , 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.

11. FAQ: Eigene Templating-Engine: Das Wichtigste auf einen Blick

1Ist reines PHP als Templating-Sprache technisch grundsätzlich falsch?
Nein, für vollständig entwicklerkontrollierte Ausgaben ohne Fremdeingaben ist es völlig ausreichend. Problematisch wird es erst, sobald Escaping oder Sandboxing tatsächlich gebraucht werden.
2Warum ist fehlendes Auto-Escaping ein Sicherheitsrisiko?
Weil jede ungeschützte Ausgabe einer Variable, die Nutzereingaben enthält, HTML- oder Script-Code ungefiltert in die Seite einschleusen kann, ein klassisches Cross-Site-Scripting-Szenario.
3Was bedeutet Sandboxing bei einer Templating-Engine konkret?
Dass ein Template nur explizit erlaubte Konstrukte wie Variablenausgabe, Filter und Kontrollstrukturen nutzen kann, nicht aber beliebige Funktionen der Wirtsprache wie exec() aufrufen darf.
4Wie erkennt die eigene Engine Variablen-Platzhalter?
Über einen regulären Ausdruck, der Muster wie {{ name }} im Template-Text findet und durch echten PHP-Code für eine kontrollierte, escapte Ausgabe ersetzt.
5Warum wird das kompilierte Template gecacht statt bei jeder Anfrage neu erzeugt?
Weil die Kompilierung aus regulären Ausdrücken und String-Ersetzungen bei jeder Anfrage unnötigen Overhead erzeugen würde, ein Cache-Vergleich über Änderungszeitpunkte macht das überflüssig.
6Wie kann ein Template trotz Auto-Escaping bewusst rohes HTML ausgeben?
Über eine explizite Ausnahme wie eine raw()-Funktion oder eine separate Syntax mit dreifachen Klammern, die im Template-Code sichtbar markiert sein muss.
7Was unterscheidet Twig grundlegend von der hier gezeigten Mini-Engine?
Twig bietet echtes Sandboxing über eine SecurityPolicy, Template-Vererbung über extends und block sowie kontextsensitives Escaping für HTML, JavaScript und CSS.
8Lohnt sich der Eigenbau einer Templating-Engine für ein reales Projekt?
Nur für sehr kleine, vollständig entwicklerkontrollierte Anwendungsfälle wie CLI-Reports. Für produktive Webanwendungen mit Redakteuren ist eine ausgereifte Engine wie Twig fast immer die bessere Wahl.
9Wie verhindert die Engine Template-Injection?
Indem Nutzereingaben ausschließlich als Werte im Kontext-Array landen und niemals selbst als Template-Quelltext interpretiert oder kompiliert werden.
10Wo sollte das Cache-Verzeichnis für kompilierte Templates liegen?
Außerhalb des öffentlich erreichbaren Web-Roots, damit kompilierte PHP-Dateien nicht versehentlich direkt über eine URL aufgerufen und ausgeführt werden können.