The Request Lifecycle
Symfony turns a request into a response by walking a fixed sequence of events, and your code can hook into any step.
public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): ResponseEvery request enters a Symfony application through one file: public/index.php, called the front controller. It turns the incoming HTTP request into a Request object, hands that to the kernel, and sends back the Response the kernel returns.
The kernel holds no application logic. It runs a fixed sequence of steps, and at each step it dispatches an event — a named notification that other code can register to receive. Routing, the profiler, session handling and your own code all attach at these points.
The sequence
| Event | Event class | When |
|---|---|---|
kernel.request | RequestEvent | Before routing. A listener may set a response here and end the request early. |
kernel.controller | ControllerEvent | The controller has been found but not called. It can be swapped. |
kernel.controller_arguments | ControllerArgumentsEvent | Arguments are resolved and can be changed. |
kernel.view | ViewEvent | Only when the controller returned something that is not a Response. |
kernel.response | ResponseEvent | A response exists and can still be modified. |
kernel.finish_request | FinishRequestEvent | Per-request state is reset. |
kernel.terminate | TerminateEvent | After the response has been sent to the client. |
kernel.exception | ExceptionEvent | Whenever a throwable escapes, at any point above. |
What trips people up
The same sequence also runs for sub-requests, such as a template rendering another controller. A listener that must act once per browser request has to call isMainRequest() and return early otherwise. And kernel.terminate only fires because the front controller calls terminate() after send().
Example
<?php
// public/index.php
declare(strict_types=1);
use App\Kernel;
use Symfony\Component\HttpFoundation\Request;
require_once dirname(__DIR__).'/vendor/autoload.php';
// 1. Build an object out of PHP's request superglobals.
$request = Request::createFromGlobals();
$kernel = new Kernel('dev', true);
// 2. handle() runs the whole lifecycle in order:
// kernel.request, kernel.controller, kernel.controller_arguments,
// the controller itself, kernel.view (only when the controller did not
// return a Response), kernel.response, kernel.finish_request.
// Any throwable along the way becomes kernel.exception.
$response = $kernel->handle($request);
// 3. Write the status line, the headers and the body to the client.
$response->send();
// 4. Work that may happen after the client already has its answer.
// This is what dispatches kernel.terminate.
$kernel->terminate($request, $response);When to use it
- Work out where to put code that must run on every request, such as reading a header or forcing HTTPS.
- Explain why a listener fires twice when a template embeds another controller.
- Return a maintenance response before routing without editing any controller.
- Move slow work, such as sending mail, to after the response has been sent.
More examples
The shorthand front controller
This is the file a new project actually ships, and it is the same four steps as the long form. The runtime component takes the kernel you return, builds the Request, calls handle(), send() and terminate(), and chooses how to do that based on where the code runs: PHP-FPM, the dev server, a console command or a worker. Read the long form to understand the lifecycle; write the short form in real projects, because it is the one that works under every runtime.
<?php
// public/index.php
declare(strict_types=1);
use App\Kernel;
// autoload_runtime.php is installed by the symfony/runtime component.
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
// Return a callable instead of running anything yourself.
return static function (array $context): Kernel {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};Ending a request before routing
Shows the short-circuit path through the lifecycle. With the parameter on, any request returns 503 with the body "Down for maintenance"; no controller of yours is called, but kernel.response still runs, so other listeners can still add headers. Reach for this whenever a whole-application answer has to come before routing: maintenance mode, a blocked IP range, or a redirect from a retired domain.
<?php
// src/EventListener/MaintenanceListener.php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
// The event comes from the __invoke() type declaration.
// 256 is a high priority, so this runs before routing.
#[AsEventListener(priority: 256)]
final class MaintenanceListener
{
public function __construct(
// parameters:\n app.maintenance: '%env(bool:MAINTENANCE)%'
#[Autowire(param: 'app.maintenance')]
private bool $maintenance,
) {
}
public function __invoke(RequestEvent $event): void
{
if (!$event->isMainRequest() || !$this->maintenance) {
return;
}
// Setting a response here skips routing and the controller.
$event->setResponse(new Response(
'Down for maintenance',
Response::HTTP_SERVICE_UNAVAILABLE,
));
}
}A listener on kernel.view
With this listener registered, a controller can end with return ['id' => 7, 'title' => 'Hello']; and the request answers 200 with {"id":7,"title":"Hello"}. The controller returned no Response, so the kernel dispatched kernel.view and this listener built one. This is the hook a view layer uses: controllers return data, one listener decides the format. If you remove the listener and leave the array return in place, the kernel throws instead, because nothing produced a response.
<?php
// src/EventListener/ArrayToJsonListener.php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ViewEvent;
#[AsEventListener]
final class ArrayToJsonListener
{
public function __invoke(ViewEvent $event): void
{
// Whatever the controller returned.
$result = $event->getControllerResult();
if (!is_array($result)) {
return;
}
$event->setResponse(new JsonResponse($result));
}
}
Discussion