Skip to Content

Chapitre 1.5 — Anatomie mentale d’une requête Symfony

Partie 1 : Découvrir Symfony — Chapitre 5/5 Version de référence : Symfony 7.4 LTS (novembre 2025, PHP 8.2+).

⏱️ TL;DR

  • Une requête HTTP entre par un seul fichier, le front controller public/index.php, et ressort sous forme d’un objet Response.
  • Entre les deux, le Kernel (via le composant HttpKernel) orchestre un pipeline d’événements : kernel.requestrouting → résolution du contrôleur → résolution des arguments → exécution du contrôleur → kernel.response → envoi → kernel.terminate.
  • Le principe : Request → (votre contrôleur) → Response. Tout le framework n’est que « ce qui aide à transformer une Request en Response ».
  • L’arborescence est conventionnelle et stable : config/, src/, templates/, public/, var/, bin/console, vendor/. Vous saurez toujours où chercher.
  • Pour un dev Next.js : c’est le middleware + routing + route handler de Next, mais rendu explicite et branchable (des listeners d’événements) au lieu d’être caché dans le framework.
  • Gardez ce schéma en tête : tout le reste du cours s’y accroche.

🎯 Objectifs

  • Décrire le trajet complet d’une requête, du front controller à la réponse.
  • Nommer les événements du noyau et savoir à quoi chacun sert.
  • Reconnaître le rôle du Kernel, du routing, du contrôleur et de la Response.
  • Vous repérer dans l’arborescence d’un projet Symfony sans hésiter.

1. L’idée fondatrice : Request in, Response out

Tout Symfony tient dans une phrase :

Une application web reçoit une Request (objet PHP qui représente la requête HTTP) et doit produire une Response (objet PHP qui représente la réponse HTTP). Tout le framework existe pour aider à cette transformation.

Ces deux objets viennent du composant HttpFoundation : ils encapsulent les superglobales PHP ($_GET, $_POST, $_SERVER, en-têtes, cookies…) dans une API propre et testable. Votre contrôleur est la fonction qui, au milieu, reçoit la Request et renvoie la Response.

💡 Pour un dev Next.js : c’est exactement le contrat d’un Route Handler : export async function GET(request: Request): Promise<Response>. Symfony généralise ce contrat à toute l’application et rend explicite tout ce qui se passe autour (routing, middleware, sérialisation).

2. Le point d’entrée unique : le front controller

Contrairement au vieux PHP où chaque URL pointait vers un fichier .php différent, Symfony utilise un front controller : toutes les requêtes passent par un seul fichier, public/index.php. Le serveur web (Nginx, FrankenPHP…) redirige tout vers lui.

// public/index.php — le seul fichier PHP exposé au web use App\Kernel; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; return function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); };

Ce fichier ne fait presque rien : il charge l’autoloader de Composer et délègue à Symfony Runtime, qui instancie le Kernel et lui passe la main. C’est volontairement minuscule — toute la logique est ailleurs.

⚠️ Piège : seul le dossier public/ est exposé au web (c’est la racine web du serveur). Tout le reste — src/, config/, .env, vendor/ — est hors de la racine web, donc inaccessible depuis un navigateur. Ne placez jamais de code sensible dans public/. (Si vous connaissez le cours Moodle : même principe que la racine public/ de Moodle 5.1+.)

Depuis Symfony 5 : le public/index.php a été simplifié avec Symfony Runtime (l’autoload_runtime.php et la closure qui renvoie le Kernel). En Symfony 5 début, index.php contenait encore le Request::createFromGlobals() et l’envoi manuel de la réponse. Aujourd’hui, Runtime abstrait ça (et permet aussi FrankenPHP, RoadRunner, etc. sans changer votre code).

3. Le Kernel et le pipeline d’événements

Le Kernel (src/Kernel.php dans votre projet) est le chef d’orchestre. Il s’appuie sur le composant HttpKernel, dont la méthode handle(Request): Response déroule un pipeline d’événements. Comprendre ce pipeline, c’est comprendre Symfony.

3.1 Les événements du noyau, un par un

ÉvénementQuandÀ quoi ça sert (exemples)
kernel.requestTout débutDéterminer la route, la locale, l’authentification (le firewall de sécurité écoute ici).
kernel.controllerContrôleur trouvé, pas encore appeléModifier le contrôleur, vérifier des droits, initialiser des choses.
(résolution des arguments)Juste avant l’appelLes value resolvers transforment la requête en arguments typés du contrôleur (ex. #[MapEntity] Booking $booking).
kernel.viewLe contrôleur a renvoyé autre chose qu’une ResponseConvertir la valeur retournée en Response (ex. API Platform sérialise un objet en JSON).
kernel.responseRéponse prête, avant envoiModifier les en-têtes, injecter la barre de debug, gérer le cache HTTP, le CORS.
kernel.exceptionUne exception a été levéeTransformer l’erreur en réponse propre (page 404/500, JSON d’erreur).
kernel.terminateAprès l’envoi de la réponseTâches lourdes différées (envoi d’e-mails, logs) sans faire attendre l’utilisateur.

💡 Pour un dev Next.js : ce pipeline, c’est l’équivalent des middlewares de Next/Express, mais structuré en phases nommées et ouvert : n’importe quel service peut « écouter » un événement pour intervenir (on appelle ça un event listener / subscriber, Partie 5). L’authentification, le CORS, le cache, la barre de debug… tout est branché , de façon inspectable — pas de magie cachée.

4. Routing → Contrôleur → Response, concrètement

Le trio que vous manipulerez tous les jours :

  1. Le routing associe une URL (et une méthode HTTP) à un contrôleur. En Symfony 7.4, on le déclare par attribut directement au-dessus de la méthode :

    // src/Controller/BookingController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; class BookingController extends AbstractController { #[Route('/bookings/{id}', name: 'booking_show', methods: ['GET'])] public function show(int $id): Response { return $this->render('booking/show.html.twig', ['id' => $id]); } }
  2. Le contrôleur est votre code : une méthode qui reçoit des arguments (résolus depuis la requête) et renvoie une Response. Ici, render() produit une réponse HTML via Twig ; pour une API, on renverrait du JSON.

  3. La Response repart dans le pipeline (kernel.response) puis est envoyée au navigateur.

Depuis Symfony 5 : en Symfony 5, ce #[Route(...)] était une annotation dans un commentaire (/** @Route("/bookings/{id}") */) nécessitant doctrine/annotations. En 7.4, c’est un attribut PHP natif (#[Route(...)]) — plus rapide, validé par l’IDE, sans dépendance annexe. Le paramètre de route {id} est, lui, inchangé dans l’esprit.

5. Où vit quoi : l’arborescence d’un projet

L’un des grands conforts de Symfony : la structure est conventionnelle. Dans n’importe quel projet Symfony 7.4, vous trouverez :

mon-projet/ ├── bin/ │ └── console ← l'outil CLI (bin/console ...) : générer, migrer, debug ├── config/ │ ├── packages/ ← config de chaque bundle (framework.yaml, doctrine.yaml…) │ ├── routes/ ← routing (quand pas en attributs) │ ├── services.yaml ← configuration du conteneur de services │ └── bundles.php ← liste des bundles activés ├── migrations/ ← migrations de base de données (Doctrine) ├── public/ │ └── index.php ← LE front controller (seul dossier exposé au web) ├── src/ ← VOTRE code (namespace App\) │ ├── Controller/ ← les contrôleurs │ ├── Entity/ ← les entités Doctrine (le modèle de données) │ ├── Repository/ ← les requêtes vers la base │ ├── Service/ ← vos services métier │ └── Kernel.php ← le noyau de l'application ├── templates/ ← les templates Twig (.html.twig) ├── tests/ ← les tests (PHPUnit) ├── translations/ ← fichiers de traduction (i18n) ├── var/ │ ├── cache/ ← cache généré (ne pas committer) │ └── log/ ← journaux ├── vendor/ ← dépendances installées par Composer (ne pas committer) ├── .env ← variables d'environnement (valeurs par défaut) ├── composer.json └── composer.lock

Quelques repères mentaux :

  • src/ = votre code. C’est là que vous passez 90 % du temps.
  • config/ = comment le framework et les bundles sont réglés.
  • templates/ = le HTML (Twig), pour la partie full-stack.
  • public/ = la seule porte web ; le reste est privé.
  • var/ et vendor/ = généré/installé, jamais committé (déjà dans .gitignore).
  • bin/console = votre couteau suisse en ligne de commande (on l’utilisera dès la Partie 3).

⚠️ Piège : var/cache/ peut contenir un cache « périmé » qui provoque des comportements étranges après un changement de config. Le réflexe : php bin/console cache:clear. (On verra que le cache Symfony est très agressif — c’est une force en prod, une source de confusion en dev quand on l’oublie.)

6. La carte à garder en tête

Si vous ne devez retenir qu’un schéma de toute la Partie 1, c’est celui-ci — chaque partie suivante viendra en détailler une case :

Vous avez maintenant la carte. La Partie 2 va vous redonner le langage (PHP moderne) ; la Partie 3 fera tourner un vrai projet ; la Partie 4 ouvrira la case « Kernel / Routing / Contrôleur ». Tout est en place.

✏️ Exercices

Exercice 1. En une phrase, résumez ce que « fait » Symfony, en utilisant les mots Request et Response.

✅ Solution

Symfony est l’ensemble des outils qui aident à transformer une Request (requête HTTP entrante) en Response (réponse HTTP sortante), via un pipeline d’événements qui route l’URL vers votre contrôleur et met en forme ce qu’il renvoie.

Exercice 2. Associez chaque besoin à l’événement du noyau le plus pertinent : (a) vérifier que l’utilisateur est authentifié ; (b) ajouter un en-tête CORS à la réponse ; (c) envoyer un e-mail sans faire attendre l’utilisateur ; (d) transformer un objet renvoyé par le contrôleur en JSON.

✅ Solution

(a) kernel.request (le firewall de sécurité agit tôt, avant le contrôleur). (b) kernel.response (la réponse est prête, on modifie ses en-têtes avant envoi). (c) kernel.terminate (après l’envoi de la réponse, pour ne pas faire attendre le client). (d) kernel.view (le contrôleur a renvoyé un objet, pas une Response : on le sérialise — c’est ce que fait API Platform).

Exercice 3. Un collègue place un fichier secret.php contenant des clés d’API dans public/. Pourquoi est-ce dangereux, et où devrait-il aller ?

✅ Solution

public/ est la racine web : tout ce qui s’y trouve est accessible depuis un navigateur (https://site/secret.php), donc potentiellement exposé. Les secrets ne doivent jamais y être. Ils doivent vivre hors de la racine web : variables d’environnement (.env/.env.local, non committées) ou le coffre de secrets de Symfony (Partie 15). Le code sensible va dans src/ ; la config dans config/.

Exercice 4. Dans l’arborescence, où iriez-vous chercher : (a) la définition de l’entité Booking ; (b) la config de Doctrine ; (c) un template HTML ; (d) la commande pour vider le cache ?

✅ Solution

(a) src/Entity/Booking.php. (b) config/packages/doctrine.yaml. (c) templates/…/*.html.twig. (d) via bin/console : php bin/console cache:clear.

🧠 Quiz de révision

1. Par quel fichier unique entrent toutes les requêtes d’une app Symfony ?

Le front controller public/index.php, seul fichier PHP exposé au web ; il délègue au Kernel via Symfony Runtime.

2. Quel objet le composant HttpKernel prend-il en entrée, et que renvoie-t-il ?

Il prend une Request (méthode handle(Request)) et renvoie une Response.

3. À quoi sert l’événement kernel.terminate ?

À exécuter des tâches après l’envoi de la réponse (e-mails, logs lourds) pour ne pas faire attendre l’utilisateur.

4. En Symfony 7.4, comment déclare-t-on une route au-dessus d’une méthode de contrôleur ?

Avec un attribut PHP : #[Route('/chemin', name: '…', methods: ['GET'])] (et non plus une annotation dans un commentaire comme en Symfony 5 début).

5. Quels dossiers ne committe-t-on jamais, et pourquoi ?

vendor/ (dépendances réinstallables via composer install) et var/ (cache et logs générés). Ils sont dans .gitignore.


Fin de la Partie 1. Vous avez la carte : ce qu’est Symfony, son histoire, son écosystème, sa place face au reste, et le trajet d’une requête.

Prochaine étape : Partie 2 — PHP moderne pour dev JS/TS — on récupère le langage (types, enums, attributs, readonly, Composer/PSR-4…) avant de plonger dans le code Symfony.