Ceci semble évident pour tout le monde lorsque j'évoque un controller, mais beaucoup moins lorsqu'il s'agit d'une commande. Pourtant, ces deux éléments ne constituent que des points d'entrée dans votre application et ne devraient, en aucun cas, contenir de la logique métier.
La logique métier doit se trouver dans des services dont c'est l'unique rôle.
Tout l'intérêt est biensur de pouvoir réutiliser ces services, dans des contextes différents, sans qu'ils ne soient couplés au premier environnement dans lequel vous pensiez les utiliser.
Bien souvent, quand j'ai expliqué ce premier point à quelqu'un, il se met à développer un ou plusieurs services qui sont plus moins couplés avec les objets qui découlent de l'environnement d'exécution CLI.
Plus globalement, ces services sont adaptés pour tourner en CLI.
C'est une erreur. Écrivez vos services pour qu'ils répondent au besoin métier de votre application sans tenir compte du contexte dans lequel ils seront exécutés. Un service doit tout aussi bien pouvoir être appelé depuis une commande que depuis un controller.
Concrétement, vous ne drevez pas y trouver d'objet Response
ou OutputInterface
, jamais. Séparez les responsabilités, toujours.
La grande question qui revient à chaque fois que j'aborde ce sujet. Finalement, comment est-ce qu'on fait pour communiquer avec notre service pour en tirer des informations et les afficher à l'utilisateur ?
Injecter, à notre service, une instance d'une InputInterface
serait, comme on l'a vu, stupide. Notre service serait alors couplé à des objets relatifs à un environnement CLI qui n'auraient aucun sens dans un autre cadre (une requête HTTP par exemple).
Le patterne EventDispatcher est parfait pour résoudre ce problème. Un service, au cours de son exécution, peut lever des évènements qui peuvent être écoutés par n'importe quel autre code. Le service ne fait qu'alerter l'ensemble du système de ce qu'il effectue et d'autre composants peuvent choisir de réagir à ces évènements, ou pas.
Concrètement, voici ce que ça peut donner :
protected function execute(InputInterface $input, OutputInterface $output)
{
$container = $this->getContainer();
$dispatcher = $container->get('event_dispatcher');
$dispatcher->addListener(Event::MY_EVENT, function(MyEvent $event) use ($output) {
$output->writeLn(sprintf('<info>%s</info>', $event->Name()));
});
$container
->get('my_vendor.my_service')
->executeSomeJob();
}
L'exemple précédent présente encore quelques problèmes. Tout d'abord, c'est la commande qui décide à quel service elle va faire appel. Ceci ne devrait pas être de sa responsabilité. Elle devrait simplement s'assurer qu'elle reçoit un objet qui est en mesure remplir la fonction souhaité. On résoud bien souvent ce genre de problème par un contrat d'interface. Notez d'ailleurs que pour l'instant rien de permet d'assurer que le service utilisé respecte l'interface attendu.
Solution : tout injecter, command as service !
Symfony permet en effet de configurer des commandes comme n'importe quel autre service.
Command as a service