Chapitre 4.1 — Front controller & HttpKernel en profondeur
Partie 4 : Cœur HTTP — Chapitre 1/6 Version de référence : Symfony 7.4.
⏱️ TL;DR
- Le composant
HttpKernelimplémentehandle(Request): Response. C’est le moteur : il déroule un pipeline d’événements entre l’entrée et la sortie.- Objets clés (composant HttpFoundation) :
Request(query,request,attributes,headers,cookies,files) etResponse(contenu, statut, en-têtes).- Les événements dans l’ordre :
kernel.request→kernel.controller→ (résolution des arguments) →kernel.view(si pas deResponse) →kernel.response→kernel.terminate. Sur erreur :kernel.exception.- Tout est branchable : la sécurité, le CORS, le profiler, le cache HTTP sont des listeners sur ces événements.
- Les sous-requêtes permettent de rendre un fragment (un morceau de page) via un autre contrôleur.
- Pour un dev Next.js : c’est une chaîne de middlewares nommés et ouverts, où chaque phase a un rôle précis et inspectable dans le profiler.
🎯 Objectifs
- Décrire finement le déroulé de
HttpKernel::handle(). - Manipuler les objets
RequestetResponsede HttpFoundation. - Comprendre le rôle de chaque événement et comment s’y brancher.
- Distinguer requête principale et sous-requête.
1. Le contrat : HttpKernelInterface
Tout part d’une interface d’une simplicité désarmante :
interface HttpKernelInterface
{
public function handle(
Request $request,
int $type = self::MAIN_REQUEST,
bool $catch = true
): Response;
}« Donne-moi une Request, je te rends une Response. » Tout Symfony est l’implémentation riche de ce contrat. Le public/index.php (chapitre 1.5) crée la Request à partir des superglobales, appelle handle(), envoie la Response, puis déclenche terminate().
2. Le pipeline, phase par phase
2.1 kernel.request
Premier événement. C’est ici que se jouent : la résolution de route (le RouterListener remplit Request::attributes avec _controller et les paramètres), la locale, et surtout la sécurité — le firewall authentifie l’utilisateur avant que le contrôleur ne s’exécute. Un listener peut court-circuiter en posant directement une Response (ex. une redirection de maintenance).
2.2 kernel.controller et la résolution des arguments
Le contrôleur (un callable) est identifié mais pas encore appelé. L’événement kernel.controller permet de l’inspecter/modifier. Puis l’ArgumentResolver transforme la requête en arguments typés du contrôleur — c’est le mécanisme des value resolvers (chapitre 4.4) : int $id depuis {id}, Booking $b via #[MapEntity], un DTO via #[MapRequestPayload].
2.3 L’appel du contrôleur et kernel.view
Le contrôleur s’exécute. S’il renvoie une Response, on passe directement à kernel.response. S’il renvoie autre chose (un objet, un tableau), l’événement kernel.view doit le convertir en Response. C’est exactement ce que fait API Platform : votre contrôleur (ou l’opération) renvoie un objet, et un listener sur kernel.view le sérialise en JSON (Partie 11).
2.4 kernel.response et kernel.terminate
kernel.response : la réponse est prête, on peut la modifier avant envoi (en-têtes de cache, CORS, injection de la barre de debug, cookies de session). Puis index.php envoie la réponse. Enfin, kernel.terminate s’exécute après l’envoi, pour les tâches lourdes différées (certains envois d’e-mails, logs) sans faire attendre le client.
2.5 kernel.exception
Si n’importe où une exception est levée, le pipeline bascule sur kernel.exception : un listener transforme l’exception en Response propre (page d’erreur HTML en dev/prod, ou JSON d’erreur pour une API). C’est là que Symfony mappe, par exemple, une NotFoundHttpException en 404 ou une AccessDeniedException en 403.
💡 Pour un dev Next.js : mentalement,
kernel.request≈ middleware d’entrée (auth, i18n),kernel.response≈ middleware de sortie (headers, CORS),kernel.exception≈ votreerror.tsx/gestion d’erreur,kernel.terminate≈waitUntil()(travail après réponse). La différence : ces phases sont nommées, ordonnées et ouvertes — n’importe quel service peut s’y greffer proprement.
3. L’objet Request (HttpFoundation)
Request encapsule tout ce qui concerne la requête HTTP dans des « sacs » (ParameterBag) :
use Symfony\Component\HttpFoundation\Request;
public function demo(Request $request): Response
{
$page = $request->query->getInt('page', 1); // ?page=2 (query string)
$body = $request->request->all(); // corps de formulaire (POST)
$json = json_decode($request->getContent(), true);// corps brut (JSON d'API)
$token = $request->headers->get('Authorization'); // en-tête
$locale = $request->getLocale(); // locale résolue
$isPost = $request->isMethod('POST');
$ip = $request->getClientIp();
$file = $request->files->get('avatar'); // fichier uploadé
// ...
}| Sac | Contenu |
|---|---|
$request->query | Paramètres d’URL (?a=1) |
$request->request | Corps de formulaire (application/x-www-form-urlencoded, multipart) |
$request->attributes | Attributs internes (route, _controller, params de route) |
$request->headers | En-têtes HTTP |
$request->cookies | Cookies |
$request->files | Fichiers uploadés |
$request->server | Variables serveur ($_SERVER) |
⚠️ Piège : pour une API JSON, le corps n’est pas dans
$request->request(réservé aux formulaires) mais dans$request->getContent()(le corps brut), qu’il faut décoder. En pratique, on ne le fait pas à la main :#[MapRequestPayload](chapitre 4.4) décode et valide le JSON directement en DTO. Retenez la distinctionrequest(formulaire) vsgetContent()(brut/JSON).
4. L’objet Response
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
$response = new Response('<h1>OK</h1>', Response::HTTP_OK, ['Content-Type' => 'text/html']);
$json = new JsonResponse(['status' => 'ok'], Response::HTTP_CREATED);
$response->headers->set('X-Custom', 'value');
$response->setStatusCode(Response::HTTP_NOT_FOUND);Les constantes Response::HTTP_* (HTTP_OK = 200, HTTP_CREATED = 201, HTTP_NO_CONTENT = 204, HTTP_NOT_FOUND = 404, HTTP_UNPROCESSABLE_ENTITY = 422…) rendent le code lisible et évitent les nombres magiques. Il existe des réponses spécialisées : JsonResponse, RedirectResponse, BinaryFileResponse (téléchargement), StreamedResponse (flux), Response (générique).
5. Les sous-requêtes
Symfony peut, au sein d’une requête, en déclencher une sous-requête pour rendre un fragment via un autre contrôleur — utile pour des morceaux réutilisables (une barre latérale, un bloc « réservations récentes ») ou le cache par fragment (ESI). On les invoque via forward() dans un contrôleur, ou render(controller(...)) dans Twig (Partie 7). Une sous-requête re-parcourt le pipeline, mais est marquée SUB_REQUEST (certains listeners, comme le firewall, agissent différemment).
📚 Aller plus loin : la doc « HttpKernel Component » détaille chaque événement, ses priorités et l’ordre des listeners. Le livre Create your own framework (sur symfony.com) reconstruit un mini-framework autour de HttpKernel — excellent pour démystifier.
6. Voir le pipeline : le profiler
En dev, la barre de debug (en bas de chaque page HTML) et le profiler (/_profiler) exposent tout le pipeline : route retenue, contrôleur appelé, requêtes SQL, événements déclenchés, temps de chaque phase, mémoire. C’est votre meilleur outil pour comprendre ce que fait Symfony sur une requête donnée.
# la barre de debug s'affiche automatiquement en dev sur les réponses HTML
# pour une réponse JSON, le profiler reste accessible via l'en-tête X-Debug-Token-Link💡 Pour un dev Next.js : le profiler Symfony n’a pas vraiment d’équivalent JS aussi intégré — c’est un DevTools serveur qui trace chaque requête (SQL, cache, events, sécurité). Prenez l’habitude de l’ouvrir : il répond à 90 % des « pourquoi ça fait ça ? ».
✏️ Exercices
Exercice 1. Dans quel événement du noyau la sécurité authentifie-t-elle l’utilisateur, et pourquoi à ce moment-là ?
✅ Solution
Dans kernel.request, très tôt dans le pipeline — avant l’exécution du contrôleur. C’est nécessaire pour que, lorsque le contrôleur s’exécute (ou même avant, via les voters), l’utilisateur soit déjà authentifié et ses droits connus. Un firewall peut d’ailleurs court-circuiter la requête (redirection vers login, 401) sans jamais atteindre le contrôleur.
Exercice 2. Un contrôleur d’API renvoie return $booking; (un objet, pas une Response). Que se passe-t-il dans le pipeline ?
✅ Solution
Comme la valeur retournée n’est pas une Response, l’événement kernel.view est déclenché : un listener doit convertir l’objet en Response. Avec API Platform, ce listener sérialise l’objet en JSON (selon les groupes de sérialisation) et produit la Response. Sans un tel listener, Symfony lève une erreur (« le contrôleur doit retourner une Response »).
Exercice 3. Pour lire le corps JSON d’une requête d’API à la main, utilisez-vous $request->request->all() ou $request->getContent() ? Pourquoi ?
✅ Solution
$request->getContent() (le corps brut), qu’on décode avec json_decode(..., true). $request->request ne contient que les données de formulaire (x-www-form-urlencoded/multipart), pas le JSON. En pratique, on préfère #[MapRequestPayload] qui décode et valide le JSON en DTO automatiquement (chapitre 4.4).
🧠 Quiz de révision
1. Quelle est la signature centrale de HttpKernel ?
handle(Request $request, int $type, bool $catch): Response — « une Request en entrée, une Response en sortie ».
2. Dans quel ordre s’enchaînent les principaux événements ?
kernel.request → kernel.controller → (résolution des arguments) → kernel.view (si pas de Response) → kernel.response → kernel.terminate ; kernel.exception en cas d’erreur.
3. Où trouve-t-on le corps JSON d’une requête d’API ?
Dans $request->getContent() (corps brut), à décoder — pas dans $request->request (formulaires).
4. À quoi sert kernel.terminate ?
kernel.terminate ?À exécuter des tâches après l’envoi de la réponse (logs lourds, certains e-mails), sans faire attendre le client.
5. Quel outil de dev expose tout le pipeline d’une requête ?
Le profiler (et la barre de debug) : route, contrôleur, SQL, événements, temps, mémoire, sécurité.
Prochain chapitre : 4.2 — Routing par attributs — paramètres, contraintes, méthodes, priorités, préfixes et génération d’URLs.