Symfony Twig: Eigene Extensions, Filter und Funktionen schreiben
AI generated
SF
{ }
Twig Templating
Twig in Symfony: Eigene Extensions, Filter und Funktionen schreiben
AbstractExtension, Filter vs. Funktion und ein Preis-Formatierungs-Beispiel

Wie du in Symfony mit AbstractExtension eigene Twig-Filter und -Funktionen schreibst, wann welches Konzept die richtige Wahl ist und welche Performance-Aspekte bei komplexer Twig-Logik zu beachten sind.

14 Min. Lesezeit Twig 3 Symfony 7

1. Warum eigene Twig-Erweiterungen überhaupt nötig werden

Twig liefert von Haus aus bereits eine große Sammlung an Filtern wie upper, date oder number_format, die für viele Standardfälle vollkommen ausreichen. Sobald aber projektspezifische Formatierungsregeln ins Spiel kommen, etwa ein Preis, der je nach Land unterschiedlich formatiert werden muss, oder eine wiederkehrende Logik zur Darstellung von Status-Badges, stoßen die eingebauten Filter schnell an ihre Grenzen.

Die naive Lösung wäre, diese Logik direkt im Template mit Twig-Kontrollstrukturen wie {% if %} und {% set %} nachzubauen, was Templates aber schnell unleserlich macht und Logik dupliziert, sobald dieselbe Formatierung an mehreren Stellen gebraucht wird. Eine eigene Twig-Extension löst genau dieses Problem: PHP-Code für die eigentliche Logik, ein einziger, klar benannter Filter oder eine Funktion im Template. So bleibt die Verantwortung sauber getrennt, und Änderungen an der Formatierungsregel müssen nur noch an einer einzigen Stelle im PHP-Code vorgenommen werden, statt in mehreren Templates gleichzeitig gepflegt zu werden.

2. AbstractExtension implementieren: Grundgerüst

Der Einstiegspunkt für jede eigene Twig-Erweiterung ist eine Klasse, die von Twig\Extension\AbstractExtension erbt und mindestens eine der beiden Methoden getFilters() oder getFunctions() überschreibt. Beide Methoden geben ein Array von TwigFilter- beziehungsweise TwigFunction-Objekten zurück, die jeweils einen Namen und einen PHP-Callable, meist eine Methode derselben Klasse, verbinden.

Symfony erkennt eine solche Klasse automatisch, sobald sie als Service registriert und mit dem Tag twig.extension versehen ist, was bei aktiviertem Autoconfigure standardmäßig bereits automatisch geschieht, solange die Klasse AbstractExtension implementiert. Eine manuelle Registrierung in services.yaml ist also im Regelfall nicht nötig, was den Einstieg deutlich vereinfacht.

3. Wann ein Filter, wann eine Funktion die richtige Wahl ist

Die Faustregel ist einfach: Ein Filter transformiert einen bereits vorhandenen Wert, wird also mit der Pipe-Syntax wie {{ price|money('EUR') }} verwendet und erwartet den zu transformierenden Wert als erstes Argument. Eine Funktion dagegen erzeugt oder berechnet einen komplett neuen Wert, oft ohne einen offensichtlichen Ausgangswert, und wird wie {{ current_user_badge() }} aufgerufen, ohne Pipe-Notation.

Ein Preis-Formatierungs-Filter ist deshalb ein klassischer Filter-Fall, da ein vorhandener Rohbetrag transformiert wird. Eine Funktion, die zum Beispiel die Anzahl ungelesener Benachrichtigungen aus der Datenbank lädt, ist dagegen klar eine Funktion, da sie keinen Eingabewert transformiert, sondern einen neuen Wert von Grund auf ermittelt. Das folgende Beispiel zeigt einen vollständigen Preis-Formatierungs-Filter für Symfony 7.


<?php

declare(strict_types=1);

namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

final class MoneyExtension extends AbstractExtension
{
    public function __construct(
        private readonly \NumberFormatter $formatter = new \NumberFormatter('de_DE', \NumberFormatter::CURRENCY),
    ) {
    }

    public function getFilters(): array
    {
        return [
            new TwigFilter('money', $this->formatMoney(...)),
        ];
    }

    public function formatMoney(int $amountInCents, string $currency = 'EUR'): string
    {
        return $this->formatter->formatCurrency($amountInCents / 100, $currency);
    }
}

4. Filter mit optionalen Argumenten und Standardwerten

Wie im Beispiel oben gezeigt, unterstützen eigene Filter-Methoden ganz normale PHP-Default-Werte für Parameter, sodass im Template sowohl {{ price|money }} als auch {{ price|money('USD') }} funktionieren, je nachdem ob eine abweichende Währung benötigt wird. Diese Flexibilität macht einen Filter in der Praxis deutlich wiederverwendbarer, ohne für jede Variation eine eigene Filter-Definition anlegen zu müssen.

Zusätzlich unterstützt TwigFilter eigene Optionen wie is_safe, um zu markieren, dass der Rückgabewert bereits sicheres HTML enthält und nicht automatisch escaped werden soll. Das ist etwa relevant, wenn ein Filter selbst HTML-Markup erzeugt, zum Beispiel ein Status-Badge mit farbiger Hintergrundfarbe, das ohne diese Markierung durch Twigs automatisches Escaping als reiner Text dargestellt würde.

5. Eigene Twig-Funktionen schreiben

Eine Twig-Funktion wird analog über getFunctions() und ein TwigFunction-Objekt registriert. Ein typisches Beispiel ist eine Funktion is_feature_enabled('checkout_v2'), die im Template direkt eine Feature-Flag-Abfrage ermöglicht, ohne dass der Controller diese Information vorher explizit an das Template übergeben müsste. Solche Funktionen sind besonders praktisch für Querschnittsthemen, die in vielen unterschiedlichen Templates gebraucht werden.

Wichtig ist, Funktionen mit Bedacht einzusetzen: Wird eine Funktion zu mächtig, etwa indem sie direkt Datenbankabfragen ausführt, verschiebt sich Business-Logik unbemerkt aus Controllern und Services in die Präsentationsschicht, was Tests erschwert und die Trennung von Verantwortlichkeiten aufweicht. Eine Funktion sollte deshalb idealerweise nur lesend auf bereits aufbereitete, gecachte oder sehr günstig abrufbare Daten zugreifen.

6. Twig-Extensions mit PHPUnit testen

Da eine Twig-Extension letztlich nur ein PHP-Service ist, lässt sie sich vollständig ohne Twig-Kernel oder Template-Rendering testen. Für den MoneyExtension-Filter reicht ein einfacher PHPUnit-Test, der formatMoney() direkt aufruft und das Ergebnis gegen einen erwarteten formatierten String prüft, ganz ohne den Umweg über ein gerendertes Template.

Für Fälle, in denen tatsächlich das Zusammenspiel mit echtem Twig-Rendering geprüft werden soll, etwa um sicherzustellen, dass die Pipe-Syntax im Template korrekt aufgelöst wird, bietet sich ein Integrationstest mit einer minimalen Twig\Environment-Instanz an, der die Extension per addExtension() registriert und einen kleinen Test-String rendert.

7. Performance-Aspekte bei komplexer Twig-Logik

Twig-Templates werden bei jedem Request ausgeführt, sofern kein Ergebnis-Caching greift, weshalb teure Operationen in einem Filter oder einer Funktion, etwa eine zusätzliche Datenbankabfrage pro Schleifendurchlauf in einer Produktliste, sich schnell zu einem spürbaren Performance-Problem summieren können, besonders bei Listen mit hunderten Einträgen.

In solchen Fällen ist es fast immer besser, die benötigten Daten bereits im Controller oder einem ViewModel effizient (etwa per JOIN oder einer einzigen Batch-Query) zu laden und dem Template als fertig aufbereitetes Array zu übergeben, statt die Twig-Funktion pro Zeile einzeln aufzurufen. Die Twig-Funktion oder der Filter sollte im Idealfall nur noch reine, günstige Formatierung übernehmen, keine Datenbeschaffung.

8. Ergänzung: Node-Visitor und eigene Twig-Tags

Für sehr fortgeschrittene Anwendungsfälle bietet AbstractExtension neben getFilters() und getFunctions() auch getNodeVisitors() sowie über ein separates TokenParser-Interface die Möglichkeit, komplett eigene Twig-Tags wie {% cache %} zu definieren. Das ist deutlich aufwendiger als ein Filter oder eine Funktion und in der Praxis nur in den seltensten Fällen tatsächlich nötig.

Für die allermeisten Projekte reichen Filter und Funktionen vollkommen aus, um wiederkehrende Formatierungs- und Darstellungs-Logik sauber zu kapseln. Bevor ein eigener Tag entwickelt wird, lohnt sich fast immer die Frage, ob dieselbe Anforderung nicht ebenso gut mit einer Kombination aus bestehenden Filtern, Funktionen und Twigs eingebauten Kontrollstrukturen gelöst werden kann.

9. Mehrere Extensions sinnvoll organisieren

Sobald ein Projekt wächst, sammeln sich schnell mehrere Twig-Erweiterungen an, etwa für Preisformatierung, Datumsdarstellung, Benutzer-Badges und Feature-Flags. Statt alle Filter und Funktionen in einer einzigen riesigen Klasse zu bündeln, empfiehlt es sich, thematisch getrennte Extension-Klassen zu erstellen, etwa MoneyExtension, DateExtension und FeatureFlagExtension, die jeweils eine klar abgegrenzte Verantwortung haben.

Diese Aufteilung erleichtert nicht nur das Testen jeder einzelnen Extension isoliert, sondern macht es auch für neue Teammitglieder leichter, die vorhandenen Filter und Funktionen überhaupt erst zu finden, da der Klassenname bereits verrät, wofür die Extension zuständig ist, statt in einer einzigen AppExtension mit dutzenden unzusammenhängenden Methoden suchen zu müssen.

Konzept Aufruf im Template Typischer Zweck Wichtige Klasse
Filter {{ wert|filtername(arg) }} Transformiert einen vorhandenen Wert TwigFilter
Funktion {{ funktionsname(arg) }} Erzeugt/berechnet einen neuen Wert TwigFunction
Node-Visitor / Tag {% eigener_tag %}...{% endeigener_tag %} Komplett neue Template-Syntax TokenParser / AbstractExtension
is_safe Option In TwigFilter/TwigFunction Konstruktor Verhindert doppeltes HTML-Escaping TwigFilter(['is_safe' => ['html']])

Mironsoft

Symfony-Architektur, saubere Domänenlogik und Legacy-Modernisierung

Symfony-Anwendungen, die nach zwei Jahren noch wartbar sind?

Wir prüfen bestehende Symfony-Projekte auf aufgeblähte Controller, fehlende Serviceabstraktionen und ungetestete Kernlogik und bauen daraus eine Architektur, die neue Features aufnimmt, ohne bei jedem Release brüchiger zu werden.

Architektur-Review

Bundle-Struktur, Dependency Injection und Serviceabstraktionen auf Wartbarkeit prüfen.

Legacy-Modernisierung

Schrittweise Migration veralteter Symfony-Versionen ohne kompletten Neuschrieb.

Test- und Qualitätssicherung

PHPUnit, PHPStan und CI-Pipelines für nachhaltige Codequalität aufbauen.

10. Zusammenfassung

Twig Extensions

AbstractExtension

Basisklasse, getFilters()/getFunctions() liefern die Definitionen

Filter vs. Funktion

Filter transformiert einen Wert, Funktion erzeugt einen neuen

Performance

Teure Datenbeschaffung gehört in den Controller, nicht ins Template

Organisation

Thematisch getrennte Extension-Klassen statt einer riesigen AppExtension

11. FAQ: Twig Extensions

1Muss ich eine Twig-Extension manuell in services.yaml registrieren?
In der Regel nicht. Symfony erkennt jede Klasse, die AbstractExtension implementiert, dank Autoconfigure automatisch und registriert sie mit dem Tag twig.extension, sobald sie als Service verfügbar ist.
2Wie entscheide ich, ob ich einen Filter oder eine Funktion brauche?
Transformiert die Logik einen bereits vorhandenen Wert, ist ein Filter die richtige Wahl. Wird ein komplett neuer Wert erzeugt oder berechnet, ohne offensichtlichen Eingabewert, ist eine Funktion passender.
3Kann ein Filter mehrere Argumente entgegennehmen?
Ja, alle Argumente nach dem transformierten Wert werden als normale PHP-Parameter der Callback-Methode übergeben, inklusive optionaler Parameter mit Default-Werten.
4Was bedeutet die is_safe Option bei einem Filter?
Sie markiert, dass der Rückgabewert bereits sicheres HTML enthält und von Twigs automatischem Escaping ausgenommen werden soll, was für Filter nötig ist, die selbst HTML-Markup erzeugen.
5Sollten Twig-Funktionen Datenbankabfragen ausführen dürfen?
Nach Möglichkeit nicht direkt bei jedem Aufruf innerhalb einer Schleife, da das schnell zu Performance-Problemen führt. Besser ist es, Daten vorab im Controller effizient zu laden.
6Wie teste ich eine eigene Twig-Extension?
Da eine Extension nur ein normaler PHP-Service ist, lassen sich ihre Methoden direkt mit PHPUnit testen, ganz ohne Twig-Rendering. Für Integrationstests genügt eine minimale Twig\Environment-Instanz.
7Wann lohnt sich ein eigener Twig-Tag statt eines Filters?
Nur in sehr seltenen Fällen, wenn eine komplett neue Template-Syntax benötigt wird, die sich nicht mit bestehenden Filtern, Funktionen und Kontrollstrukturen abbilden lässt.
8Wie organisiere ich mehrere Twig-Extensions in einem größeren Projekt?
Am besten thematisch getrennt in eigenen Klassen wie MoneyExtension oder DateExtension, statt alle Filter und Funktionen in einer einzigen großen AppExtension zu bündeln.
9Kann eine Twig-Funktion Services per Dependency Injection nutzen?
Ja, da eine Extension ein regulärer Symfony-Service ist, können beliebige Services ganz normal über den Konstruktor injiziert werden, etwa ein Feature-Flag-Service oder ein Repository.
10Was ist der häufigste Fehler beim Schreiben eigener Twig-Filter?
Teure Operationen wie Datenbankabfragen direkt im Filter auszuführen, obwohl der Filter pro Template-Durchlauf oft mehrfach aufgerufen wird, was zu unnötig vielen Datenbank-Roundtrips führt.