[Symfony] Symfony4 AbstractController et création d’alias de service
Aujourd’hui, je vous propose de faire le point sur la déclaration de service dans Symfony (notamment Symfony 4) et de comment les récupérer dans les contrôleurs. Nous allons aussi voir la différence entre les classe Controller et AbstractController étendu par nos contrôleurs.
❗ Pour la suite de l’article vous devez avoir un installation de Symfony 4 (Pour Symfony 3, AbstractController n’est pas disponnilbe) et une route pointant vers l’action ‘indexAction’ du contrôlleur DefaultController.
Le service
Pour notre exemple, nous allons créer un service permettant de formater/filtrer nos chaînes de caractères. Ci dessous, la classe avec une méthode permettant supprimer tout les caractère spéciaux d’une chaîne.
1 2 3 4 5 6 7 8 9 10 11 |
<php namespace App\Format; class FormatText { public static function removeSpecialChar(string $text) : string { return preg_replace('/[^A-Za-z0-9\-]/', '', $text); // Removes special chars. } } |
Utilisation du service dans un controlleur étendant de de la classe Controller de Symfony (Comme sur Symfony 3)
Déclarons ce service auprès de Symfony dans le fichier config/services.yaml
1 2 3 4 |
services: app.format_text: class: App\Format\FormatText public: true |
Appelons notre service dans un contrôleur héritant du contrôleur de base de Symfony (recommandé sur Symfony 3, mais nous verrons par la suite que les choses ont évolués 😉 ). Dans celui ci, rien de bien compliqué, on retourne simplement une réponse contenant une chaîne dans laquelle les caractères spéciaux sont supprimés.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; class DefaultController extends Controller { public function indexAction() { $formatText = $this->get('app.format_text'); return new Response($formatText->removeSpecialChar('toto è ; ?')); } } |
Cette action va affiché une page blanche avec le texte “toto”.
Utilisation du service dans un contrôleur étendant de de la classe AbstractController de Symfony (Recomandé sur Symfony 4)
Cette méthode fonctionne et était utilisé par Symfony 3, mais les développeur de Symfony se sont rendu compte que votre contrôleur chargeait tout les services souvent inutilement. Dans Symfony 4, il est recommandé d’étendre AbstractController et non Controller.
Modifiez donc votre contrôleur de la sorte et rafraîchissez votre page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; class DefaultController extends AbstractController { public function indexAction() { $formatText = $this->get('app.format_text'); return new Response($formatText->removeSpecialChar('toto è ; ?')); } } |
❗ 😡 Mais tu te fou de nous, ça génère une erreur :
Effectivement, l’erreur ci-dessous à dû s’afficher :
1 2 3 4 |
ServiceNotFoundException Service "app.format_text" not found: even though it exists in the app's container, the container inside "App\Controller\DefaultController" is a smaller service locator that only knows about the "http_kernel", "parameter_bag", "request_stack", "router" and "session" services. Unless you need extra laziness, try using dependency injection instead. Otherwise, you need to declare it using "DefaultController::getSubscribedServices()". |
L’erreur vient du fait que AbstractController contient un conteneur de service réduit (comme l’explique l’erreur). Vous trouverez les services inclue par défaut dans la classe AbstractController :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static function getSubscribedServices() { return array( 'router' => '?'.RouterInterface::class, 'request_stack' => '?'.RequestStack::class, 'http_kernel' => '?'.HttpKernelInterface::class, 'serializer' => '?'.SerializerInterface::class, 'session' => '?'.SessionInterface::class, 'security.authorization_checker' => '?'.AuthorizationCheckerInterface::class, 'templating' => '?'.EngineInterface::class, 'twig' => '?'.Environment::class, 'doctrine' => '?'.ManagerRegistry::class, 'form.factory' => '?'.FormFactoryInterface::class, 'security.token_storage' => '?'.TokenStorageInterface::class, 'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class, 'parameter_bag' => '?'.ContainerBagInterface::class, 'message_bus' => '?'.MessageBusInterface::class, ); } |
Quelques explication s’impose 😉
La méthode ‘getSubscribedServices’ retourne un tableau de la forme ‘Nom du service’ => ‘Classe du service’ (La classe doit également être définie dans le fichier services.yaml comme alias de notre service). Seul ces services seront accessible dans notre contrôleur.
💡 le point d’interrogation ‘?’ indique un service qui n’est pas obligatoire. Je ne suis pas aller voir en détaille le fonctionnement de Symfony à ce sujet, mais je suppose que les services de se type ne sont inclue que à la demande.
Comme je le disait ci dessus, nous devons déclarer notre classe comme alias de notre service (nous verrons ci-dessous, que créer cette Alias à une autre utilité très pratique).
1 2 3 4 5 6 7 |
services: app.format_text: class: App\Format\FormatText public: true App\Format\FormatText: alias: app.format_text |
Les deux dernières ligne permettent de déclarer la classe ‘App\Format\FormatText’ comme alias du service ‘app.format_text’. Il ne nous reste plus qu’a dire à notre contrôleur qu’il doit injecter ce service dans le gestionnaire de service. Pour cela, nous allons surcharger la méthode ‘getSubscribedServices’ comme ci-dessous.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; class DefaultController extends AbstractController { public static function getSubscribedServices(): array { return array_merge(parent::getSubscribedServices(), [ // on merge le tableau des services par defaut avec notre tableau personnalisé 'app.format_text' => 'App\Format\FormatText', ]); } public function indexAction() { $formatText = $this->get('app.format_text'); return new Response($formatText->removeSpecialChar('toto è ; ?')); } } |
Mettez a jours votre page et paf ça fait des chocapic ! la magie opère (oui la blague date d’une publicité d’il y a 15 ans, mais je ne suis plus tout jeune ^^)
Notre alias nous permet d’utiliser notre service comme injection de dépendance dans les contrôlleurs
C’est bien, nous avons un service accessible depuis nôtres contrôleur qui pourtant n’utilise que les services minimums, mais l’alias que nous avons ajouté permet des chose bien plus puissantes. En effet, nous pouvons utiliser l’injection de dépendance automatique de Symfony pour injecter notre service dans les contrôleurs. Reprenons notre exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; class DefaultController extends AbstractController { public function indexAction(\App\Format\FormatText $formatText) { return new Response($formatText->removeSpecialChar('toto è ; ?')); } } |
Le type du paramètre ‘$formatText’ est trouvé dans la liste des services connu par Symfony et ce dernier va donc créer une instance de notre service au sain de la variable ‘$formatText’.
Et ça c’est vraiment pratique !
Voilà, c’est tout pour aujourd’hui, si vous avez des questions ou/et remarque, n’hésitez pas à les écrire en commentaire.
Merci Jérémy pour ce tutoriel !!
Je viens de mettre à jour mon projet pour régler les “deprecations” et ça en a soulevé une nouvelle à l’usage non référencée par le “Symfony Profiler” … pour le framework “symfony/translation”.
Je l’utilisais comme service dans les différents Controller de mon application et j’ai compris après coup qu’il s’agissait de la correction de la dépréciation “Controller” => “AbstractController” et la fonction “getSubscribedServices” qui était la fautive.
Personnellement j’ai opté pour la première solution, à savoir créer un “DefaultController” qui étend “AbstractController” et dans lequel je surcharge la fonction “getSubscribedServices”.
Après je fais étendre tous mes Controllers par “DefaultController” de sorte à ne pas avoir à ajouter à chaque méthodes le Service “Translator”.
Article intéressant, mais qui partagent certaines mauvaises pratiques, selon moi 🙂
1/ L’utilisation du AbstractController est certe pratique, mais viole les principes SOLID (le D notamment).
2/Par défaut, les services sont privés, pour “interdire” l’utilisation du container ($this->get()) et forcer l’injection de dépendances. Le code est ainsi mieux découplé et testable unitairement.
3/ Concernant l’injection de dépendance justement, le parametre ‘autowire’ (activable par défaut dans la clé _service, ou pour chaque définition de services dans services.yml) permet d’injecter le service au bon moment.
cf la doc: https://symfony.com/doc/current/service_container/autowiring.html#an-autowiring-example
Du coup, votre exemple fonctionnerait mieux ainsi:
Dans services.yml:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\Format\FormatText:
autowire: true
Et dans votre controller, l’action reste identique (il n’est plus necessaire d’appeler un action **Action):
public function index(\App\Format\FormatText $formatText)
{
return new Response($formatText->removeSpecialChar(‘toto è ; ?’));
}
@MadCat34 : merci pour le commentaire constructif 🙂
Pour le 1/ : Il y a une meilleur pratique pour le controller ?
Pour le reste, en effet, je ne connaissait pas l’autowire à l’époque. Je vais essayer de mettre a jour l’article.