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.
Inhaltsverzeichnis
- 1. Warum eigene Twig-Erweiterungen überhaupt nötig werden
- 2. AbstractExtension implementieren: Grundgerüst
- 3. Wann ein Filter, wann eine Funktion die richtige Wahl ist
- 4. Filter mit optionalen Argumenten und Standardwerten
- 5. Eigene Twig-Funktionen schreiben
- 6. Twig-Extensions mit PHPUnit testen
- 7. Performance-Aspekte bei komplexer Twig-Logik
- 8. Ergänzung: Node-Visitor und eigene Twig-Tags
- 9. Mehrere Extensions sinnvoll organisieren
- 10. Zusammenfassung
- 11. FAQ
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