Événements personnalisés
Il est également possible, au sein de notre code, de définir des événements personnalisés et les diffuser à l'aide du dispatcher de Symfony.
Découpler son code
Reprenons notre inscription à la newsletter.
Dans le contrôleur, nous effectuons toutes les actions annexes à l'enregistrement d'un email au même endroit.
Ainsi, nous enregistrons l'email si le formulaire est soumis et valide, puis nous envoyons un email à l'utilisateur.
À la place, nous pourrions conserver la logique d'enregistrement de l'email dans le contrôleur, puis diffuser un événement au sein de l'application, indiquant qu'un email a été enregistré.
Par conséquent, le contrôleur n'aurait plus à s'occuper des effets de bord produits par l'enregistrement d'un email. Il se contenterait d'effectuer le principal, c'est-à-dire la persistance des données, puis de notifier le reste de l'application qu'un email a été enregistré.
Cette approche est plus flexible : nous pouvons définir, ailleurs dans l'application, des bouts de code (des classes), qui réagiront à l'événement en exécutant leur propre logique.
On peut aussi imaginer que l'événement pourrait être lancé depuis un autre endroit de l'application : si on enregistrait un email via une route d'API par exemple, en plus du contrôleur réceptionnant des données de formulaire, alors on pourrait diffuser le même événement, depuis un autre endroit. Le même écouteur, ou souscripteur d'événement, serait alors déclenché.
L'utilisation d'événements est plus flexible, car elle permet de découpler son code. Au besoin, on peut désactiver ou réactiver certains listeners, plutôt que d'aller commenter/décommenter du code à un seul endroit.
En revanche, elle peut introduire un peu de flou au sein de l'application. En effet, quand on voit qu'un événement est lancé quelque part, il est souvent nécessaire d'explorer le code pour aller trouver les listeners/subscribers qui écoutent cet événement.
Définir un événement personnalisé
Pour commencer, il faut définir un événement personnalisé au sein de l'application. Pour ce faire, nous allons définir une nouvelle classe.
Si on veut notifier le reste de l'application que quelqu'un vient de s'inscrire à la newsletter, on pourrait imaginer une classe NewsletterRegisteredEvent :
<?php
namespace App\Event;
use App\Entity\NewsletterEmail;
class NewsletterRegisteredEvent
{
public const NAME = 'newsletter.registered';
public function __construct(private NewsletterEmail $newsletter)
{
}
public function getNewsletter(): NewsletterEmail
{
return $this->newsletter;
}
}
Dans cette classe, on va définir plusieurs choses : un nom pour l'événement, et la donnée concernée par l'événement.
Le nom de l'événement
On l'aura peut-être déjà remarqué dans le Kernel de Symfony, mais généralement les événements sont nommés de la manière suivante : topic.event.
Je classe les événements par sujet (topic), et un événement sera souvent un verbe (on peut utiliser le participe passé) : registered.
Le nom de l'événement, ici calqué sur le nom de la classe d'événement, doit donc être représentatif de ce qu'il se passe lorsque l'événement est lancé : newsletter.registered.
Tout comme des variables, des classes, des méthodes, etc...nous sommes libres sur le nommage des événements.
On pourrait aussi appeler l'événement ci-dessus newsletter.registered, newsletter.created, etc...peu importe, du moment que c'est clair.
La donnée associée
C'est nous qui créons la classe d'événement : c'est donc nous qui définissons la donnée utile que l'événement doit porter pour être exploité par des listeners ou des subscribers.
Ici, si notre événement concerne l'enregistrement d'un nouvel email à notre newsletter, alors la donnée que nous pouvons embarquer dans cet événement est simplement l'entité concernée. Nous ajoutons donc une propriété, privée, de type Newsletter.
Quand un listener ou subscriber sera appelé par le dispatcher, il recevra donc cette donnée. Pour notre exemple, nous allons ensuite créer un subscriber qui enverra un email à l'utilisateur qui vient de s'inscrire. Nous avons donc besoin de l'instance de Newsletter qui vient d'être enregistrée pour pouvoir récupérer l'email associé, puis déclencher l'envoi d'un email.
Dispatcher l'événement
Une fois notre classe d'événement créée, on peut revenir à notre contrôleur.
C'est dans le contrôleur qu'on voudra diffuser l'événement au moment voulu, donc une fois l'entité enregistrée.
On peut type-hinter l'interface Symfony\Component\EventDispatcher\EventDispatcherInterface dans le contrôleur pour avoir le dispatcher :
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
//...
#[Route('/newsletter/register', name: 'newsletter_register')]
public function register(
Request $request,
NewsletterEmailRepository $newsletterRepository,
EventDispatcherInterface $dispatcher
): Response {
//...
}
Ensuite, au moment où l'instance de NewsletterEmail a été enregistrée (persistée), on peut diffuser l'événement :
if ($form->isSubmitted() && $form->isValid()) {
$newsletterRepository->save($newsletter, true);
// J'instancie l'événement en lui passant l'entité concernée
// L'événement portera donc la donnée concernée, exploitable par les listeners/subscribers
$event = new NewsletterRegisteredEvent($newsletter);
// Je diffuse l'événement, en précisant son nom
$dispatcher->dispatch($event, NewsletterRegisteredEvent::NAME);
}
On pourrait instancier l'événement directement dans l'appel à la méthode dispatch
Écouter et réagir à l'événement
Pour l'instant, on lance l'événement mais personne ne l'écoute.
Nous avons vu plus tôt que les événements nous servent à découpler notre logique. Nous allons donc créer un subscriber qui écoutera l'événement newsletter.registered, et pourra se lancer lorsque cet événement sera diffusé. Ce subscriber sera alors chargé d'envoyer un email à l'utilisateur.
Création d'un subscriber
Pour créer un subscriber, on peut utiliser notre bonne vieille console et le maker :
php bin/console make:subscriber
Pour le nom de la classe, on indiquera NewsletterRegisteredSubscriber : c'est le souscripteur de l'événement newsletter.registered. On pourrait le nommer comme on le souhaite, mais vu qu'on découple notre logique, on va essayer de conserver des noms cohérents, au plus proche les uns des autres.
Ensuite, le maker nous demande à quel événement on souhaite souscrire. Il suggère quelques événements déjà présents au sein de l'application, mais celui que nous voulons, c'est newsletter.registered.
Le maker nous crée alors une classe App\EventSubscriber\NewsletterRegisteredSubscriber :
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class NewsletterRegisteredSubscriber implements EventSubscriberInterface
{
public function onNewsletterRegistered($event): void
{
//...
}
public static function getSubscribedEvents(): array
{
return [
'newsletter.registered' => 'onNewsletterRegistered',
];
}
}
À la manière du subscriber qu'on a écrit plus tôt pour hacher le mot de passe d'un utilisateur lorsqu'une entité User allait être créée, on voit que ce subscriber implémente une interface EventSubscriberInterface.
Attention, il ne s'agit pas du même namespace entre notre subscriber Doctrine et ce subscriber Symfony.
On retrouve donc la même logique avec la méthode getSubscribedEvents() par exemple, mais ici cette méthode est statique.
Ces 2 subscribers ne seront donc pas taggés de la même façon.
Implémentation du subscriber
Nous pouvons déjà changer le nom de l'événement par une référence à la constante NAME qu'on a définie dans la classe d'événement :
public static function getSubscribedEvents(): array
{
return [
NewsletterRegisteredEvent::NAME => 'onNewsletterRegistered',
];
}
On voit qu'on associe l'événement à une méthode onNewsletterRegistered. Nous n'avons plus qu'à y déplacer notre logique d'envoi d'email, en injectant les services ou les paramètres dont on a besoin, évidemment :
public function __construct(
private MailerInterface $mailer,
private string $adminEmail
) {
}
public function onNewsletterRegistered(NewsletterRegisteredEvent $event): void
{
$newsletter = $event->getNewsletter();
$email = (new Email())
->from($this->adminEmail)
->to($newsletter->getEmail())
->subject("Inscription à la newsletter")
->text("Votre email " . $newsletter->getEmail() . " a bien été enregistré, merci");
$this->mailer->send($email);
}
Si on teste le formulaire d'enregistrement à la newsletter, on devrait recevoir un email, comme avant.
Au niveau du comportement, rien de particulier n'a changé. C'est au niveau de la conception qu'on a découplé notre code.