Symfony HttpKernel-Events: den Request-Lifecycle im Detail verstehen
AI generated
SF
{ }
Symfony · HttpKernel · Events · Architektur
Symfony HttpKernel-Events
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.

17 Min. Lesezeit HttpKernel Events Symfony 7 · Event-Lifecycle

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

11. FAQ: HttpKernel-Events: Das Wichtigste auf einen Blick

1In welcher Reihenfolge laufen die HttpKernel-Events ab?
RequestEvent zuerst, dann ControllerEvent, gegebenenfalls kernel.view, danach ResponseEvent und zuletzt TerminateEvent. ExceptionEvent kann parallel dazu jederzeit ausgelöst werden, sobald eine unbehandelte Exception auftritt.
2Wofür eignet sich RequestEvent am besten?
Für Logik, die auf jeden Request angewendet werden soll, bevor der Controller ermittelt wird, etwa Locale-Setzung, Wartungsmodus-Prüfungen oder frühe Zugriffskontrollen mit direkter Response über setResponse().
3Warum ist ResponseEvent der richtige Ort für Security-Header?
Weil es auch dann ausgelöst wird, wenn zuvor eine Exception aufgetreten und in eine Fehler-Response umgewandelt wurde, sodass Security-Header konsistent auf jede ausgehende Antwort angewendet werden, inklusive Fehlerseiten.
4Wann läuft TerminateEvent genau?
Erst nachdem die Response bereits vollständig an den Client gesendet wurde, weshalb es sich für zeitaufwendige Nacharbeiten wie Analytics oder Audit-Logging eignet, die die wahrgenommene Antwortzeit nicht beeinflussen sollen.
5Wie funktioniert Fehlerbehandlung über ExceptionEvent?
Ein Listener kann über setResponse() eine passende Fehler-Response erzeugen und damit die Standard-Fehlerbehandlung von Symfony überschreiben, wobei mehrere Listener für unterschiedliche Exception-Typen zuständig sein können.
6Was bedeuten Prioritäten bei mehreren Listenern auf demselben Event?
Höhere Prioritätswerte laufen zuerst aus, niedrigere und negative Werte später. Das ist entscheidend, wenn ein Listener von der Arbeit eines anderen abhängt, etwa Locale vor Übersetzung.
7Was ist der Unterschied zwischen Main-Request und Sub-Request?
Der Main-Request ist der ursprünglich vom Client ausgelöste Request, ein Sub-Request entsteht durch forward() oder ESI-Fragmente. isMainRequest() unterscheidet beide, was für Listener wichtig ist, die nur einmal pro Client-Anfrage laufen sollen.
8Wofür wird ControllerEvent typischerweise genutzt?
Seltener für eigene Anwendungslogik, häufiger für Framework-interne Mechanismen wie Attribut-basierte Security-Prüfungen, da hier bereits bekannt ist, welche konkrete Controller-Methode aufgerufen wird.
9Kann ein Listener in RequestEvent die Controller-Ausführung komplett verhindern?
Ja, über setResponse() im RequestEvent wird die weitere Verarbeitung bis kernel.view übersprungen und direkt zu kernel.response weitergesprungen, ohne dass der Controller je aufgerufen wird.
10Verhält sich TerminateEvent unter FrankenPHP anders als unter PHP-FPM?
Das Verhalten kann sich unterscheiden, da TerminateEvent unter klassischem PHP-FPM zuverlässig nach dem Response-Versand läuft, während Long-Running-Worker-Setups zusätzliches Testen erfordern können.