Makina Blog

Le blog Makina-corpus

Processus de traitement d'une requête HTTP par Symfony


Vous utilisez Symfony pour vos développements mais vous n'en connaissez pas vraiment le fonctionnement interne ? Suivez le guide, c'est parti pour la visite !

Sommaire

Introduction

En tant qu'utilisateur d'un outil, nous ne prenons pas toujours le temps de comprendre comment il fonctionne. On sait interagir avec lui pour obtenir un résultat, mais de sa mécanique interne nous n'en avons finalement qu'une vague idée, voire nous en ignorons tout.

Je vous propose dans cet article d'explorer les rouages de Symfony et plus précisément ceux constituant son cœur de métier, à savoir réceptionner une requête HTTP et y répondre. Nous y parcourons, étape par étape, son processus de traitement d'une requête HTTP. Chaque étape sera numérotée et illustrée d'un schéma. Ainsi, les chiffres apparaissant dans les schémas servent à numéroter l'étape en cours ou à faire référence à une autre étape.

Le schéma complet et annoté du processus est disponible ici.

Le processus dans les grandes lignes

Avant d'entrer dans les détails, appréhendons d'abord le processus dans ses grandes lignes.

Schéma résumant le processus de traitement d'une requête HTTP par Symfony.

Au commencement, un internaute tape l'URL de notre application dans la barre d'adresse de son navigateur et presse la touche « Entrée » de son clavier. Le navigateur génère alors la requête HTTP appropriée et l'envoie sur le réseau à destination de notre serveur web. L'application serveur HTTP en service sur notre machine (Apache, Nginx, etc.) réceptionne la requête et, parce que nous l'avons configurée ainsi, sollicite PHP pour qu'il exécute le fichier public/index.php de notre application.

Voilà, nous venons de franchir le seuil de notre application, nous sommes dans le fichier sus-cité que l'on nomme le front controller. Il est le point d'entrée unique (en contexte HTTP) de notre application. Quelle que soit la page ou l'opération demandée par l'internaute, c'est toujours ce même fichier que PHP exécutera pour démarrer l'application.

Le front controller poursuit alors le processus en instanciant le « kernel », c'est-à-dire le noyau de l'application. Ce dernier va ensuite prendre en charge la requête afin qu'il en résulte une réponse. Il devra pour cela déterminer puis invoquer le controller destiné à construire cette réponse.

Le lancement de l'application

Étape 1 : le front controller

Si nous ouvrons le fichier public/index.php de notre projet, voici ce que nous découvrons :

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']);
};

Un léger désarroi pourrait alors vous envahir puisque rien dans ces quelques lignes de code n'évoque le lancement d'un processus… L'instant de stupéfaction écoulé, on présume alors que la magie doit se trouver dans le fichier vendor/autoload_runtime.php. Toujours est-il qu'à l'ouverture de celui-ci, la lumière n'en jaillit pas instantanément…

Depuis la version 5.3 de Symfony, le front controller ne fait lui-même plus grand chose (v5.2 vs v5.3). Il s'appuie désormais en grande partie sur le composant symfony/runtime qui est à l'origine du fichier vendor/autoload_runtime.php (généré automatiquement par son plugin Composer). C'est à ce fichier que la fonction anonyme retournée par le front controller est justement destinée.

Schéma résumant très simplement le fonctionnement du front controller Symfony

Au sein du fichier autoload_runtime.php, un objet que l'on appellera simplement le runtime est instancié pour prendre en charge la fonction anonyme en provenance du front controller. (Par défaut, ce runtime est une instance de la classe Symfony\Component\Runtime\SymfonyRuntime.) Cette « prise en charge » va consister à :

  • résoudre les arguments attendus par la fonction,
  • appeler la fonction avec les arguments résolus pour récupérer en retour une « application »,
  • déterminer le runner (lanceur) capable de lancer l'application,
  • lancer l'application via le runner pour réaliser l'opération souhaitée par l'utilisateur.

Le but de la fonction anonyme retournée par le front controller est donc de fournir une « application » au runtime qu'il se chargera de lancer.

Dans le cas d'une application Symfony classique, la fonction anonyme du front controller retourne une instance de la classe App\Kernel que le runtime lancera ensuite à l'aide d'un runner de type HttpKernelRunner.

Si vous souhaitez en savoir plus sur le sujet, je vous invite à lire la documentation du composant Runtime.

Étape 2 : le kernel, ou plutôt les kernels…

Sur un projet fraîchement initialisé, le premier et unique fichier PHP que vous trouverez à la racine du dossier src/ est le fichier Kernel.php contenant la classe App\Kernel précédemment mentionnée :

namespace App;

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;
}

Bon, là non plus le code n'est pas très bavard, mais c'est normal puisque notre classe Kernel s'appuie totalement sur les implémentations par défaut suivantes :

  • La classe abstraite Kernel fournie par le composant HttpKernel
  • Le trait MicroKernelTrait fourni par le FrameworkBundle

De ces implémentations découlent la structure et les mécaniques centrales habituelles d'un projet Symfony. Cela signifie donc que si nous souhaitions modifier cette structure et/ou ces mécaniques, il se pourrait bien que nous ayons à surcharger certaines de leurs méthodes au sein de notre classe Kernel, voire à tout réimplémenter.

Cette classe App\Kernel, matérialisant le kernel de notre application, peut être vu comme la fusion de deux autres : le kernel Symfony et le kernel HTTP. C'est ce dernier qui va particulièrement nous intéresser dans le cadre de cet article. Il est le noyau du « moteur HTTP » ayant pour rôle de répondre à la requête reçue. Quant au kernel Symfony, sur lequel nous ne nous étendrons pas outre mesure, celui-ci consiste à initialiser les systèmes de base propres au framework, comme le service container ou les bundles. Le kernel HTTP lui-même fera usage de ces systèmes pour produire sa réponse.

Schéma simpliste décomposant le kernel Symfony

Concentrons-nous donc sur la notion de kernel HTTP définie techniquement par le contrat de l'interface Symfony\Component\HttpKernel\HttpKernelInterface :


namespace Symfony\Component\HttpKernel;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

interface HttpKernelInterface
{
    public const MAIN_REQUEST = 1;
    public const SUB_REQUEST = 2;

    public function handle(
        Request $request,
        int $type = self::MAIN_REQUEST,
        bool $catch = true
    ): Response;
}

(Les commentaires ont été retirés du code pour obtenir un aperçu concis.)

Il existe par défaut trois implémentations du kernel HTTP :

  • Symfony\Component\HttpKernel\Kernel, déjà évoquée plus haut du fait que le kernel de l'application (App\Kernel) en hérite directement
  • Symfony\Component\HttpKernel\HttpCache\HttpCache, qui ajoute un cache optionnel au processus
  • Symfony\Component\HttpKernel\HttpKernel, qui se chargera concrètement de produire la réponse

La première implémentation s'appuie essentiellement sur les deux autres : quand le cache HTTP est activé (via la configuration du FrameworkBundle) elle fait appel à la seconde, HttpCache, sinon elle fait directement appel à la troisième, HttpKernel. L'implémentation HttpCache quant à elle n'est capable de restituer une réponse que lorsque celle-ci a déjà été produite et stockée en cache, elle sollicite donc l'implémentation HttpKernel pour produire toute réponse qu'elle ne retrouve pas en cache.

Bref, vous l'aurez compris, la suite du processus pour aboutir à une réponse va se dérouler dans la classe HttpKernel.

Le traitement de la requête

Nous sommes à présent rendu dans le kernel HTTP où nous allons enfin pouvoir aborder le vif du sujet. À partir d'ici, le processus va se concentrer sur l'objectif d'obtenir une réponse. Bien entendu, ce n'est pas le kernel HTTP lui-même qui va construire cette réponse, il va pour cela faire appel aux autres systèmes de l'application. Un nouveau composant Symfony entre alors notamment en jeu : le composant EventDispatcher. Grâce à ce dernier, le kernel émettra des évènements que les divers systèmes pourront écouter dans le but d'intervenir à des moments clés du processus.

L'ensemble de ces événements sont implémentés au sein du composant HttpKernel sous forme de classes étendant toutes la classe parente KernelEvent.

Étape 3 : annonce de la requête et identification de la route

Schéma représentant le début du processus interne du kernel HTTP

Le premier évènement émis par le kernel est le RequestEvent. Il annonce la réception de la requête, donnant ainsi l'opportunité aux systèmes annexes de la modifier, voire de fournir une réponse précoce au kernel pour des cas de figure ne nécessitant pas de poursuivre le traitement.

Une première étape importante dans la quête d'une réponse est réalisée à cette occasion par le RouterListener : il s'agit de l'identification de la route correspondant à la requête. Une fois identifiée, les informations de la route sont alors ajoutées à la requête sous forme d'attributs de façon à être disponibles dans la suite du processus. Lorsqu'aucune route n'a été identifiée, une exception est lancée pour interrompre le processus. Nous verrons comment sont gérés les cas d'erreur plus loin dans l'article.

Dans le cas où un listener venait à transmettre une réponse au kernel via l'évènement en question, toutes les étapes à suivre précédant l'étape 8 seraient alors sautées.

Étape 4 : résolution du controller

Parmi les informations relatives à la route dont la requête est désormais porteuse, se trouve l'attribut _controller, ayant pour valeur une référence du controller à invoquer pour générer la réponse. Si vous utilisez le format YAML pour déclarer vos routes, cette valeur n'est autre que celle que vous avez vous-même renseignée dans votre définition de la route, par exemple :

user_login:
    path: /login
    controller: App\Controller\UserController::login # <- Cette valeur ci.

C'est également une chaîne au format "class_name::method_name" qui est utilisée quand la route est déclarée à l'aide de l'attribut PHP #[Route]. Mais d'autres types de valeur sont possibles : chaîne contenant le nom d'une fonction, closure ou encore tableau (respectant le format suivant : ['class_name', 'method_name']).

Schéma représentant l'étape de résolution du contrôleur au sein du kernel HTTP

Toutes ces considérations ne sont cependant pas du ressort du kernel HTTP qui orchestre le processus sans en connaître les détails de l'implémentation. C'est le controller resolver, auquel il fait appel suite au RequestEvent, qui va récupérer la valeur de l'attribut _controller depuis la requête pour la transformer (si besoin) en un callable que le kernel pourra invoquer.

Si le controller resolver ne parvient pas à produire un callable à partir de l'attribut, une exception NotFoundHttpException est lancée. Nous ne citerons pas toutes les exceptions qui pourraient potentiellement survenir durant le processus. Vous aurez compris l'idée : à tout moment il peut être interrompu par une exception qui sera alors prise en charge par un système secondaire que nous détaillerons par la suite.

Suite à la résolution du controller, un deuxième évènement, le ControllerEvent, est lancé. Il est l'occasion de réaliser des initialisations en vue des opérations à suivre, voire de remplacer complètement le controller.

Étape 5 : résolution des arguments

Nous disposons donc maintenant de notre controller sous la forme adéquate d'un callable, mais pour l'invoquer, celui-ci va certainement nécessiter que nous lui passions certains arguments. Arguments qui lui seront utiles pour construire la réponse.

Schéma représentant l'étape de résolution des arguments du contrôleur au sein du kernel HTTP

C'est alors au tour de l'argument resolver d'être sollicité par le kernel afin de déterminer et de collecter l'ensemble des valeurs attendues par le controller en tant qu'arguments. En arrière plan, l'argument resolver s'appuie sur les multiples implémentations de l'interface ValueResolverInterface dédiées à cet effet. Les arguments auxquels on peut faire appel à l'écriture d'un controller dépendent donc directement de ces implémentations. Parmi elles, certaines vont par exemple permettre de récupérer des services depuis le conteneur de dépendances, d'autres d'obtenir des données contenues dans la requête (implémentations liées aux attributs #[MapQueryParameter], #[MapQueryString] et #[MapRequestPayload]).

Un fois l'ensemble des arguments résolu, l'évènement ControllerArgumentsEvent est propagé. On peut s'y abonner afin d'intervenir sur les arguments ou, comme avec le précédent, réaliser des opérations préparatoires pour la suite du processus, voire remplacer le controller.

Étape 6 : invocation du controller

Schéma représentant l'étape d'invocation du contrôleur au sein du kernel HTTP

Le kernel est désormais en capacité d'invoquer le controller, l'instant fatidique de lui faire générer la réponse est enfin arrivé. Le processus se poursuit alors avec cette ligne on ne peut plus parlante :


$response = $controller(...$arguments);

Suite à cet appel du controller, nous devrions théoriquement être en possession d'une réponse. Néanmoins, cette réponse pourrait ne pas être une instance de la classe Symfony\Component\HttpFoundation\Response, le kernel laissant le controller libre de retourner n'importe quelle sorte de donnée. En ce cas, un évènement ViewEvent est lancé dans l'objectif qu'un listener transforme la réponse obtenue en une instance de la classe Response.

On pourrait alors imaginer que le controller puisse retourner directement le markup d'une page sous la forme d'une chaîne, ou même une entité Doctrine, et que des listeners soient chargés, à partir de telles données, de produire la réponse finale.

Quelle que soit la réponse du controller, cette étape ne peut se terminer par une réponse autre qu'une instance de la classe Response, sans quoi une exception est jetée.

Étape 7 : cas d'erreur

Puisque la suite du processus sera la même, qu'une erreur soit survenue ou non durant les étapes précédentes, c'est le moment opportun pour jeter un œil à la tournure des choses en cas d'erreur.

Schéma représentant la gestion des cas d'erreur au sein du kernel HTTP

Lorsqu'une exception est lancée au cours des étapes 3 à 6, le processus tel qu'il est décrit ci-dessus s'interrompt. L'exception est interceptée par le kernel qui déclenche alors un évènement ExceptionEvent dans le but de recueillir une réponse appropriée au cas d'erreur survenu.

Le ErrorListener présent dans le composant HttpKernel, abonné par défaut à l'évènement, est le point de départ du système de gestion des erreurs de Symfony. Ce système va consister à obtenir une réponse en faisant de nouveau appel au kernel HTTP. La requête alors transmise au kernel dans cette situation est une copie de la requête initiale, préconfigurée avec un controller dédié aux cas d'erreur (via l'attribut _controller), à laquelle est également ajoutée l'exception capturée.

Le scénario décrit tout au long de cet article est donc rejoué une seconde fois à partir de l'étape 3 en considérant cette copie. Mais cette fois-ci, le listener indiquera au kernel qu'il s'agit alors d'une « sous-requête » (voir HttpKernelInterface et sa constante SUB_REQUEST) afin de la distinguer de la requête initiale (MAIN_REQUEST), évitant ainsi que des opérations destinées uniquement à cette dernière ne soient exécutées.

Si suite à sa diffusion, l'évènement ExceptionEvent n'est pas porteur d'une réponse, le kernel émet au préalable l'évènement FinishRequestEvent que nous préciserons plus bas, puis il relance l'exception afin de stopper net l'application.

Dans le cas contraire, le processus continue à l'étape suivante, exactement comme il en eut été si aucune exception n'avait été jetée.

Étape 8 : derniers préparatifs avant envoi

Schéma représentant l'étape finale de finition avant retour de la réponse par le kernel HTTP

Nous avons à ce stade obtenu notre réponse. Mais avant qu'elle soit transmise à l'internaute, le kernel lance l'évènement ResponseEvent offrant la possibilité de la modifier, en y ajoutant par exemple des entêtes ou des cookies. Pour ne citer qu'eux, les entêtes relatifs au cache spécifiés via l'attribut PHP #[Cache] au sein du controller sont ajoutés à la réponse lors de cet évènement.

Le kernel enchaîne ensuite avec l'émission de l'évènement FinishRequestEvent, destiné notamment à la réinitialisation ou la restauration de certains états potentiellement modifiés pendant le traitement de la requête.

Son travail prend alors fin tandis qu'il restitue sa réponse au runner.

Étape 9 : envoi de la réponse

Schéma représentant l'étape d'envoi de la réponse au sein du runner

De retour au sein du runner après que le kernel ait accompli sa tâche, l'envoi de la réponse est effectué directement depuis l'instance de la classe Response issue du kernel par un simple appel à sa méthode send().

Avant que l'application n'arrive à son terme, le runner propage un dernier évènement, le TerminateEvent, moment adéquat pour effectuer des traitements complémentaires qui, réalisés plus tôt, auraient retardé la réponse.

The end

Notre visite guidée au cœur de Symfony est terminée. J'espère qu'elle aura su vous éclairer quant au fonctionnement de son processus central :-)

Merci à Excalidraw, grâce auquel j'ai pu réaliser l'ensemble des schémas qui nous ont guidés tout au long de l'article.

Pour rappel, le schéma complet et annoté est disponible ici.

Formations associées

Formation Symfony

Formation Symfony Initiation

Nantes Du 26 au 28 mars 2024

Voir la formation

Formations Outils et bases de données

Formation Git, gestionnaire de versions

Toulouse Du 14 au 15 mai 2024

Voir la formation

Formations Outils et bases de données

Formation PostgreSQL

Nantes Du 29 au 31 janvier 2024

Voir la formation

Actualités en lien

Image
Encart article 2 : Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony
20/02/2024

Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony (2/2)

Quels virages avons-nous pris après un premier projet expé­ri­men­tal pour stabi­li­ser notre concep­tion logi­cielle, et que ferons-nous plus tard ?

Voir l'article
Image
AFUP Meet-up DBToolsBundle
15/02/2024

Meetup AFUP Nantes de février : parlons anony­mi­sa­tion avec le DbTools­Bundle Symfony

Notre expert Symfony / PHP prend la parole au Meet-up de l’AFUP Nantes le 21 février pour présen­ter le nouveau bundle Symfony déve­loppé par Makina Corpus : le DbTools­Bundle !

Voir l'article
Image
Symfony + Vue.js
21/06/2022

Créer une application Symfony/Vue.js

Pour faire des essais ou bien démarrer un nouveau projet, vous avez besoin de créer rapidement une application Symfony couplée avec un front Vue.js ? Suivez le guide !

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus