Utiliser les événements Symfony2 pour un code SOLID

Cet article a été publié depuis plus de 6 mois, cela signifie que le contenu peut ne plus être d'actualité.

En tant que développeur, nous essayons d'écrire du code simple, maintenable et facilement extensible. Pour cela, nous tentons de mettre en oeuvre les principes évoqués dans l'acronyme SOLID. Le "S" correspond à la notion de "Single Responsability" (responsabilité unique). Cela signifie que vos classes doivent avoir une et une seule responsabilité, une seule raison d'exister.

Prenons pour exemple, un service Symfony dont l'objectif est d'enregistrer les données d'un utilisateur issu d'un formulaire. Le code pourrait alors ressembler à quelque chose comme cela :

// Controller/UserController.php
public function newAction(Request $request)
{
  $user = new User();

  $form = $this->createForm('user_form', $user, [
    'action' => $this->generateUrl('app_new_user'),
    'method' => 'POST'
  ]);
  $form->add('submit', 'submit', ['label' => 'Create']);

  $form->handleRequest($request);
  if ($form->isValid()) {
    $manager = $this->get('app.user_manager');
    $manager->save($user);

    return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
  }

  return [ 'form' => $form->createView() ];
}

// Manager/UserManager.php
public function save(User $user)
{
  $this->entityManager->persist($user);
  $this->entityManager->flush();
}

Ce code respecte bien le principe de responsabilité unique, chacune de nos classes n'a qu'un seul objectif. Mais imaginons maintenant que nous souhaitons envoyer un email à l'utilisateur que nous venons de créer (pour le notifier de la création de son compte et lui envoyer ses informations de connexion par exemple).

Rien de difficile, il suffit alors de modifier notre manager pour envoyer le mail lors de la création du compte :

// Manager/UserManager.php
public function save(User $user)
{
  $this->entityManager->persist($user);
  $this->entityManager->flush();

  $this->emailManager->sendNewAccountNotification($user);
}

Sauf qu'en ajoutant cette ligne, nous venons de casser le principe de responsabilité unique de notre service gérant les utilisateurs. Réfléchissez bien, si vous souhaitez réutiliser la classe UserManager dans une autre application, mais que cette dernière ne souhaite pas envoyer de notification, comment allez-vous faire ?

Il vous faudra alors supprimer tous les appels à notre EmailManager ou mettre en place des conditions permettant de ne pas exécuter cette fonction. Dans le cas présent, l'exemple est très simple et cela peut être fait facilement. Mais imaginez une application de plusieurs milliers de lignes. Cela est tout de suite plus compliqué.

Heureusement, Symfony2 nous permet d'éviter ce couplage très simplement, au travers de la gestion des événements. L'idée est très simple, une fois l'enregistrement du nouvel utilisateur effectué, nous allons émettre un signal afin d'indiquer le succès de la création. Ce signal pourra alors être capter par différentes classes afin de déclencher différentes actions (un envoi de notification dans notre exemple).

Commençons par modifier notre classe UserManager :

// Manager/UserManager.php
public function __construct(EventDispatcherInterface $dispatcher, ...)
{
  $this->dispatcher = $dispatcher;
  // ...
}

public function save(User $user)
{
  $this->entityManager->persist($user);
  $this->entityManager->flush();

  $this->dispatcher->dispatch('user.create', new UserEvent($user));
}

La classe UserEvent est tout simple un conteneur pour les informations traitées par le formulaire (ici notre utilisateur créé).

use Symfony\Component\EventDispatcher\Event;

class UserEvent extends Event
{
  private $user;

  public function __construct(User $user)
  {
    $this->user = $user;
  }

  public function getUser()
  {
      return $this->user;
  }
}

Il ne reste plus qu'à intercepter le signal et à envoyer notre email :

<?php

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class UserNotificationListener implements EventSubscriberInterface
{
  private $emailManager;

  public function __construct(EmailManagerInterface $emailManager)
  {
    $this->emailManager = $emailManager;
  }

  public function onUserCreate(UserEvent $event)
  {
    $this->emailManager->sendNewAccountNotification($event->getUser());
  }

  public static function getSubscribedEvents()
  {
    return [
      'user.create' => 'onUserCreate',
    ];
  }
}

Sans oublier de déclarer le service qui va bien :

// Resources/config/services.yml
services:
  listener.user_mailer_notification:
    class: Listener\UserNotificationListener
    arguments:
      - @app.manager.email
    tags:
      - { name: kernel.event_subscriber }

Nous avons donc maintenant des classes qui ont bien une responsabilité unique. De ce fait, le risque d'erreur est réduit. De plus cela, les rend plus facilement testables. Il sera également plus facile de les réutiliser dans d'autres projets.

Si vous souhaitez obtenir plus d'information concernant la gestion des événements dans Symonfy2, je vous invite à lire la documentation très bien rédigé à son sujet.