den Request-Lifecycle im Detail verstehen
Zwischen dem Eintreffen eines HTTP-Requests und dem Versand der Response durchläuft Symfony eine feste Kette von Kernel-Events, die jeweils einen eigenen, klar abgegrenzten Zweck haben. Wer weiß, wann RequestEvent, ControllerEvent, ResponseEvent, TerminateEvent und ExceptionEvent genau ausgelöst werden, kann Cross-Cutting-Concerns wie Authentifizierung, Header-Injection oder Logging exakt an der richtigen Stelle platzieren, statt sie in Controllern oder Services zu verstreuen, wo sie leicht vergessen oder inkonsistent angewendet werden.
Inhaltsverzeichnis
- 1. Überblick über die Event-Kette eines Requests
- 2. RequestEvent: Request-Modifikation ganz am Anfang
- 3. ControllerEvent: Eingriff kurz vor der Controller-Ausführung
- 4. ResponseEvent: der Standardort für Header-Injection
- 5. TerminateEvent: Nacharbeiten nach dem Versand der Response
- 6. ExceptionEvent: zentrale Fehlerbehandlung
- 7. Prioritäten bei mehreren Listenern auf demselben Event
- 8. Main-Request vs. Sub-Request bei ESI und Forward
- 9. Praktische Checkliste zur Event-Auswahl
- 10. Zusammenfassung
- 11. FAQ
1. Überblick über die Event-Kette eines Requests
Ein einzelner HTTP-Request durchläuft in Symfony eine klar definierte Reihenfolge von Kernel-Events, beginnend mit kernel.request unmittelbar nach Eingang des Requests, gefolgt von kernel.controller kurz vor dem Aufruf des ermittelten Controllers, dann kernel.view falls der Controller kein Response-Objekt zurückgibt, kernel.response nach erfolgreicher Erstellung der Response, und schließlich kernel.terminate nachdem die Response bereits an den Client gesendet wurde. Parallel dazu kann jederzeit kernel.exception ausgelöst werden, sobald während der Verarbeitung eine unbehandelte Exception auftritt.
Diese Reihenfolge ist keine willkürliche Implementierungsentscheidung, sondern spiegelt den tatsächlichen Verarbeitungsablauf des HttpKernel wider. Jedes Event trägt ein eigenes Event-Objekt mit spezifischen Methoden, etwa getRequest(), getResponse() oder setResponse(), die je nach Zeitpunkt im Lifecycle unterschiedlich sinnvoll oder überhaupt verfügbar sind. Ein Versuch, in kernel.request bereits auf eine Response zuzugreifen, schlägt fehl, da zu diesem Zeitpunkt schlicht noch keine existiert.
2. RequestEvent: Request-Modifikation ganz am Anfang
Das RequestEvent wird direkt nach Eingang des Requests ausgelöst, noch bevor Symfony überhaupt ermittelt hat, welcher Controller zuständig ist. Das macht es zum idealen Ort für Logik, die auf jeden Request angewendet werden soll, unabhängig von der späteren Route, etwa das Setzen der Locale basierend auf einem Header, das frühzeitige Abfangen von Wartungsmodus-Anfragen, oder das direkte Beenden der Verarbeitung durch setResponse(), wenn eine Anfrage aufgrund bestimmter Kriterien gar nicht erst einen Controller erreichen soll.
Ein häufiger praktischer Anwendungsfall ist ein CorsListener, der bei OPTIONS-Preflight-Requests bereits im RequestEvent eine passende Response mit den nötigen CORS-Headern setzt und damit verhindert, dass der eigentliche Controller für diese technischen Vorab-Anfragen überhaupt aufgerufen wird. Wichtig ist, dass ein per setResponse() gesetztes Response-Objekt die weitere Verarbeitung der Event-Kette für kernel.controller und kernel.view komplett überspringt und direkt zu kernel.response weiterspringt.
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Setzt die Anwendungs-Locale anhand eines eigenen Request-Headers.
*/
final class LocaleFromHeaderSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 20],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$locale = $event->getRequest()->headers->get('X-App-Locale');
if ($locale !== null) {
$event->getRequest()->setLocale($locale);
}
}
}
3. ControllerEvent: Eingriff kurz vor der Controller-Ausführung
Das ControllerEvent wird ausgelöst, nachdem Symfony den zuständigen Controller anhand des Routings ermittelt hat, aber bevor dieser tatsächlich aufgerufen wird. Zu diesem Zeitpunkt liegt der Controller als Callable bereits fest und kann über getController() ausgelesen und sogar durch setController() vollständig ausgetauscht werden, was für Szenarien wie A/B-Testing oder das dynamische Umleiten auf einen alternativen Controller basierend auf Feature-Flags genutzt werden kann.
In der Praxis wird dieses Event seltener für eigene Logik genutzt als RequestEvent oder ResponseEvent, spielt aber eine zentrale Rolle bei Attribut-basierten Mechanismen wie Security-Voter-Prüfungen über #[IsGranted]-Attribute, die genau an dieser Stelle ausgewertet werden, da hier bereits bekannt ist, welche Controller-Methode aufgerufen werden soll und somit auch, welche Attribute an ihr hängen.
4. ResponseEvent: der Standardort für Header-Injection
Das ResponseEvent wird ausgelöst, nachdem eine Response feststeht, unabhängig davon, ob sie vom Controller direkt zurückgegeben oder über ein kernel.view-Event aus einem Nicht-Response-Rückgabewert erzeugt wurde. Dieses Event ist der Standardort für alles, was auf jede ausgehende Response angewendet werden soll, etwa das Setzen von Security-Headern wie Content-Security-Policy oder X-Frame-Options, das Hinzufügen von Caching-Headern, oder das Anhängen eines Request-ID-Headers für Tracing-Zwecke.
Ein wichtiger Unterschied zu RequestEvent ist, dass ResponseEvent auch dann ausgelöst wird, wenn zuvor bereits eine Exception aufgetreten und über kernel.exception in eine Fehler-Response umgewandelt wurde. Ein im ResponseEvent registrierter Security-Header-Listener wirkt also auch auf Fehlerseiten, was für sicherheitsrelevante Header ausdrücklich erwünscht ist, da auch eine 404- oder 500-Seite denselben Schutz braucht wie eine normale Antwort.
5. TerminateEvent: Nacharbeiten nach dem Versand der Response
Das TerminateEvent wird erst ausgelöst, nachdem die Response bereits vollständig an den Client gesendet wurde, was es zum idealen Ort für Arbeiten macht, die zeitaufwendig sind, aber die wahrgenommene Antwortzeit des Nutzers nicht beeinflussen sollen. Typische Beispiele sind das asynchrone Versenden von Analytics-Events, das Schreiben detaillierter Audit-Logs, oder das Dispatchen von Messenger-Nachrichten, die nicht zwingend Teil der ursprünglichen Transaktion sein müssen.
Wichtig ist, dass TerminateEvent nur bei Verwendung des klassischen PHP-FPM-Modells wirklich nach dem Response-Versand läuft. Unter FrankenPHP oder anderen Long-Running-Worker-Setups kann sich das Verhalten unterscheiden, weshalb rein zeitkritische Nacharbeiten in solchen Umgebungen zusätzlich getestet werden sollten, statt sich blind auf das klassische Verhalten zu verlassen.
6. ExceptionEvent: zentrale Fehlerbehandlung
Das ExceptionEvent wird ausgelöst, sobald irgendwo in der Verarbeitungskette, sei es im Routing, im Controller oder in einem anderen Listener, eine unbehandelte Exception auftritt. Über setResponse() kann ein Listener hier eine passende Fehler-Response erzeugen, etwa eine formatierte JSON-Fehlermeldung für API-Endpunkte oder eine benutzerfreundliche Fehlerseite für reguläre Web-Requests, und damit die Standard-Fehlerbehandlung von Symfony überschreiben.
Mehrere ExceptionEvent-Listener können für unterschiedliche Exception-Typen zuständig sein, etwa ein Listener speziell für ValidationFailedException und ein anderer als generischer Fallback für alle übrigen Exceptions. Sobald ein Listener setResponse() aufgerufen hat, wird die Verarbeitung der Event-Kette für kernel.exception normalerweise fortgesetzt, sodass nachfolgende Listener mit niedrigerer Priorität die bereits gesetzte Response noch verändern können, was bei der Reihenfolge und Priorität mehrerer Exception-Listener berücksichtigt werden muss.
7. Prioritäten bei mehreren Listenern auf demselben Event
Wenn mehrere Listener auf dasselbe Event registriert sind, bestimmt der Prioritätswert, in welcher Reihenfolge sie ausgeführt werden. Höhere Prioritätswerte laufen zuerst, niedrigere und negative Werte später, wobei der Standardwert bei fehlender Angabe 0 beträgt. Diese Reihenfolge ist entscheidend, wenn ein Listener von der Arbeit eines anderen abhängt, etwa wenn ein LocaleListener vor jedem Übersetzungs-abhängigen Listener laufen muss, damit die richtige Sprache bereits gesetzt ist, wenn andere Listener sie benötigen.
Bei Symfonys eigenen internen Listenern, etwa dem Router-Listener oder dem Firewall-Listener aus dem Security-Bundle, sind bereits feste Prioritäten vergeben, die man kennen sollte, bevor man eigene Listener mit potenziell kollidierenden Prioritäten registriert. Ein eigener Security-relevanter Listener, der vor der Firewall-Prüfung laufen soll, braucht zwingend eine höhere Priorität als der Firewall-Listener selbst, sonst greift die eigene Logik erst, nachdem Security bereits entschieden hat, den Zugriff zu verweigern.
8. Main-Request vs. Sub-Request bei ESI und Forward
Jedes Kernel-Event trägt eine isMainRequest()-Methode, die unterscheidet, ob das Event für den ursprünglichen, vom Client ausgelösten Request gilt, oder für einen internen Sub-Request, etwa durch ein forward() innerhalb eines Controllers oder durch ein Edge-Side-Include-Fragment. Viele Listener, insbesondere solche, die Security-Header setzen oder Analytics-Events versenden, sollen ausschließlich für den Main-Request ausgeführt werden, da ein Sub-Request lediglich Teil der Verarbeitung des Haupt-Requests ist und keine eigene, an den Client gesendete Response erzeugt.
Wird diese Unterscheidung vergessen, können unerwünschte Doppelausführungen entstehen, etwa ein Analytics-Event, das sowohl für den Main-Request als auch für jeden darin eingebetteten Sub-Request ausgelöst wird, oder Security-Header, die versehentlich mehrfach gesetzt und dabei inkonsistent zusammengeführt werden. Ein Blick auf isMainRequest() sollte deshalb am Anfang jeder Listener-Methode stehen, deren Logik ausschließlich für den ursprünglichen Client-Request gedacht ist.
9. Praktische Checkliste zur Event-Auswahl
Für die Wahl des richtigen Events hilft eine einfache Faustregel: Alles, was vor der Business-Logik greifen soll, etwa Locale-Ermittlung, Wartungsmodus oder frühe Zugriffskontrollen, gehört in RequestEvent. Alles, was von der konkreten Response abhängt, etwa Header-Injection oder Response-Manipulation, gehört in ResponseEvent. Und alles, was nach dem Response-Versand ohne Einfluss auf die Antwortzeit erledigt werden kann, gehört in TerminateEvent.
Für Fehlerbehandlung ist ExceptionEvent praktisch immer die richtige Wahl, während ControllerEvent seltener für eigene Anwendungslogik gebraucht wird und meist Frameworks oder Bundles vorbehalten bleibt, die auf Attribut- oder Annotation-Ebene mit dem konkreten Controller-Callable arbeiten müssen. Wer sich an diese grobe Zuordnung hält, landet in den allermeisten Fällen beim passenden Event, ohne die Event-Referenz jedes Mal neu konsultieren zu müssen.
| Event | Zeitpunkt | Typischer Anwendungsfall |
|---|---|---|
| kernel.request | direkt nach Request-Eingang | Locale setzen, frühe Zugriffskontrolle |
| kernel.controller | vor Controller-Aufruf | Controller austauschen, Attribut-Prüfungen |
| kernel.response | nach Response-Erstellung | Security-Header, Caching-Header setzen |
| kernel.terminate | nach Response-Versand | Analytics, Audit-Logs, asynchrone Nacharbeiten |
| kernel.exception | bei unbehandelter Exception | zentrale Fehler-Response erzeugen |
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
HttpKernel-Events: Das Wichtigste auf einen Blick
Reihenfolge
RequestEvent, ControllerEvent, ResponseEvent, TerminateEvent, ExceptionEvent parallel möglich
RequestEvent
idealer Ort für Locale, Wartungsmodus, frühe Response-Kurzschlüsse
ResponseEvent
Standardort für Header-Injection, wirkt auch auf Fehlerseiten
Prioritäten
höhere Werte laufen zuerst, entscheidend bei Listener-Abhängigkeiten