Je voulais écrire depuis longtemps sur le composant Sécurité de Symfony, qui est à mon avis un des plus compliqués. La principale cause de cette complexité est le très grand nombre de concepts qui interviennent, qui s’entremêlent. Guard a grandement aidé à simplifier le processus d’authentification et sa personnalisation, mais c’est à continuer.
La semaine dernière à l’occasion du hackathon EU-FOSSA une discussion sur le futur du composant a été entamée, pour recueillir des idées et les doléances pour la version 5 du composant Sécurité. D’où l’occasion de replonger dedans. Aussi, aujourd’hui je vais vous parler d’une astuce: comment récupérer le User
authentifié en paramètre du contrôleur, et pourquoi c’est une bonne chose à mon avis.
Raisonnement et motivation
L’idée est d’appliquer pour l’utilisateur connecté, User
, le même raisonnement pour que l’objet Request
. Les deux sont des « Context object », soit un objet global et unique par rapport à une requête HTTP.
En d’autres mots, lors du traitement d’une requête, on a un seul utilisateur de connecté, et c’est le même partout dans le code de notre application. Sa récupération et construction est gérée au niveau du framework, on peut donc vouloir le récupérer en type hint de notre contrôleur:
À mon avis, ce code est plus élégant et plus facile à lire que de passer par un service et un Token
, dont les existences semblent un peu artificielle, car motivées techniquement. On peut opposer deux arguments à ce code précédent: d’un côté, on oblige la présence d’un utilisateur connecté pour accéder à cette page, et d’un autre on se couple à une implementation d’User
. Par rapport au premier point, en pratique les pages privées d’une application deviennent rarement publiques sans refactor, et d’ailleurs on peut très bien autoriser $user
à être nullable. Pour le second point, on utilise ici une interface, donc un couplage faible (cette interface est de plus fournie et imposée par Symfony, actuellement).
Note: Le pattern « Context object », bien que peu connu, a été bien défini dans l’univers Java, voir ce document ou plus succinctement cette réponse StackOverflow.
Implémentation
Si vous utilisez Symfony 3.2+ / 4, la fonctionnalité est déjà incluse 🙂
Il suffit de type-hint UserInterface
ou bien UserInterface
(nullable) et un ArgumentResolver va faire le travail.
Regardons comment l’implémenter pour des versions plus anciennes de Symfony, ou bien par curiosité.
L’approche évidente serait de faire un listener écoutant l’évènement kernel.controller
, et qui décide s’il est nécessaire d’injecter l’utilisateur ou pas. Le code est relativement simple, mais il sera exécuté à chaque requête avec un appel lent à la Reflection
, ce qui rajoute une petite latence partout. On souhaite l’éviter, on ne veut pas pénaliser les performances de l’application pour un fonctionnalité utilisée que sur certaines pages.
Une approche plus astucieuse est d’utiliser la possibilité de rajouter un ParamConverter via le SensioFrameworkExtraBundle. Le fonctionnement est en deux temps, chaque converter sera appelé sur chaque route pour savoir s’il doit s’appliquer, le résultat sera caché, et lors d’une requête seuls les converters ayant répondu positivement s’exécuteront.
Le code est assez court, le voici avec deux exemples dans un contrôleur:
Une fois le ParamConverter enregistré, l’injection devrait marcher. Il est à noter que par défaut, les ParamConverter
n’ont pas besoin d’être configurés explicitement avec une annotation grâce au paramètre auto_convert
(référence).
Du reste le code est facilement modifiable à vos besoins.
Voilà, j’espère que cela vous aidera à rendre vos contrôleurs plus courts, plus lisibles et plus découpés, puisque dorénavant on n’a plus besoin d’un ServiceConfigurator
sur ce point. Bon code 🙂