4 Minutes read

Comprendre l’Argument Resolver de Symfony : Injection Magique ou Logique ?

Vous pensez que l’argument resolving est magique ? En réalité, sa logique est bien plus simple qu’il n’y paraît.

Image générée avec l’intelligence artificielle
Image générée avec l’intelligence artificielle

Qu’est-ce que l’argument resolving ?

L’argument resolving, c’est le moment où Symfony résout les arguments des méthodes d’un contrôleur. Entre autres, c’est le mécanisme qui injecte le fameux objet Request dans la méthode de notre contrôleur, ou encore la variable $id provenant de l’URL path/{id}.

Je vous ai dit que vous l’utilisez quotidiennement…

Un peu d’histoire pour continuer.

L’argument resolving a été introduit dans Symfony 2. À l’époque, la résolution des différents arguments était assurée par le ControllerResolver. La fonctionnalité, dans sa première version, était un peu rigide : il n’était pas possible de configurer des resolvers personnalisés, et seuls certains types d’arguments pouvaient être résolus, comme l’objet Request ou un attribut de la requête, comme mentionné précédemment.

Dans la version 3.x de Symfony, c’est à cette version qu’a été introduite la conception de l’argument resolving que l’on connaît aujourd’hui. La fonctionnalité est devenue extensible, donc la possibilité de créer des resolvers personnalisés est devenue possible.

C’est à partir de cette version-là qu’ont été introduits des resolvers built-in, comme RequestAttributeValueResolver, qui est chargé de résoudre les arguments qui seront peuplés par les attributs de la requête, ou encore ServiceValueResolver, qui permet d’injecter directement tout type de service présent dans le conteneur de services de notre application. Vous pouvez consulter ici la liste complète des built-in resolvers.

Maintenant qu’on comprend ce qu’est l’argument resolving, voyons voir comment cela fonctionne sous le capot.

Comment l’argument resolving est déclenché ?

Tout commence dans HttpKernel::handleRaw(). Symfony cherche à identifier le contrôleur à appeler. Une fois trouvé, c’est l’ArgumentResolver qui prend le relais. Il identifie les arguments de la méthode du contrôleur via l’API de réflexion, puis il itère dessus pour les résoudre un par un.

Mais comment l’ArgumentResolver sait quel resolver appeler et à quel moment ?

La réponse à cette question est simple. Tous les resolvers doivent implémenter l’interface ValueResolverInterface. Ces services sont ensuite tagués avec le tag controller.argument_value_resolver. Ils forment alors un pool de resolvers qui est injecté dans le service ArgumentResolver.

Au moment où l’ArgumentResolver itère sur les arguments des différentes méthodes des contrôleurs, il parcourt également le pool de resolvers. Pour chaque argument, il essaie d’identifier s’il existe un resolver capable de le prendre en charge (c’est-à-dire un resolver qui le "supporte"). Si c’est le cas, le resolver traite l’argument. Dans le cas contraire, une exception est levée pour indiquer que l’argument de la méthode du contrôleur n’a pas pu être résolu.

Mais comment fonctionne un resolver ?

Pour vous montrer la résolution d’un argument, je vais prendre l’exemple du RequestValueResolver. J’ai volontairement simplifié la logique du resolver afin que l’idée derrière soit plus facile à comprendre.

Voici le code du resolver, suivi d’une petite explication :

namespace SymfonyComponentHttpKernelControllerArgumentResolver;

use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpKernelControllerValueResolverInterface;
use SymfonyComponentHttpKernelControllerMetadataArgumentMetadata;

final class RequestValueResolver implements ValueResolverInterface
{
public function resolve(Request $request, ArgumentMetadata $argument): array
{
if (Request::class === $argument->getType() || is_subclass_of($argument->getType(), Request::class)) {
return [$request];
}

return [];
}
}

L’ArgumentResolver est en train d’itérer sur une liste d’arguments à résoudre. Notre RequestValueResolver fait partie du pool de resolvers qui seront appelés, et deux scénarios sont possibles dans notre cas :

Scénario 1 : Le type de l’argument de notre itération courant est Request. Dans ce cas, on retourne l’objet Request, et on passe à l’itération suivante.

Scénario 2 : Le resolver n’est pas capable de résoudre l’argument de l’itération courante. Il retourne alors un tableau vide, ce qui indique à l’ArgumentResolver de passer au resolver suivant pour ce même argument.

En ce qui concerne la dernière étape, la liste des arguments résolus par l’ArgumentResolver est retournée, et le contrôleur est ensuite appelé avec ces arguments.

Pour finir, voici un aperçu d’ensemble. Il s’agit également d’un code simplifié, conçu pour illustrer la vue d’ensemble du mécanisme.


// Identification du contrôleur en fonction de la requête courante
if (false === $controller = $this->resolver->getController($request)) {
throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo()));
}

// Dispatch de l’événement kernel.controller
$event = new ControllerEvent($this, $controller, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
$controller = $event->getController();

// Déclenchement de la résolution des arguments de la méthode du contrôleur
$arguments = $this->argumentResolver->getArguments($request, $controller, $event->getControllerReflector());

// Dispatch de l'événement kernel.controller_arguments
$event = new ControllerArgumentsEvent($this, $event, $arguments, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS);
$controller = $event->getController();
$arguments = $event->getArguments();

// Appel du contrôleur
$response = $controller(...$arguments);

Le code est un extrait de la méthode HttpKernel::handleRaw() .

Et voilà, comme vous pouvez le voir, il s’agit d’une fonctionnalité à la fois puissante, scalable et facilement personnalisable, qui permet d’injecter les arguments dans une action de contrôleur sans contrainte d’ordre, et qui améliore la maintenabilité, favorise la séparation des responsabilités et rend les contrôleurs plus propres.

Avant de créer un nouveau resolver, il est recommandé de consulter la liste des resolvers built-in, car celui dont vous avez besoin existe peut-être déjà.

J’espère que ce mécanisme vous semble désormais plus clair et que son fonctionnement n’a plus rien de magique.

Si cet article vous a été utile, abonnez-vous au Medium d’ekino pour découvrir régulièrement des décryptages techniques, retours d’expérience et bonnes pratiques autour du développement.


Comprendre l’Argument Resolver de Symfony : Injection Magique ou Logique ? was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.