Utiliser un écouteur d'événements

Bonjour,

Aujourd'hui, j'ai envie de vous montrer comment utiliser, à bon escient, les écouteurs d'événements (ou events listeners) dans le framework Symfony.

Tout d'abord, nous allons fixer un contexte pour l'utilisation de ce patron de conception : On imagine que l'on ai besoin de gérer la création d'un Article dans plusieurs Cercles ou Topics, c'est à dire deux controlleurs différents, mais de la même manière.

Quels sont les avantages à utiliser ici un event-listener ?

Il mutualisera le code exécuté lors de la création de l'Article, découplera les relations entre nos classes Cercles ou Topics et Article et permettra une meilleure maintenabilité lors d'évolutions probables (Lors de l'ajout d'autres endroits ou publier un Article)...

Revenons à nos moutons, nous aurions donc 3 entitées participantes :

class Article 
{
    // Attributes
    private $topic, $circle;

    // Getters and Setters
    public function getTopic()
    {
        return $this->topic;
    }

    public function setTopic(Topic $topic)
    {
        $this->topic = $topic;
    }

    public function getCircle()
    {
        return $this->circle;
    }

    public function setCircle(Circle $circle)
    {
        $this->circle = $circle;
    }

    // [...]
}
class Circle 
{
    // Attributes
    private $articles;

    // Getters and Setters
    public function getArticles()
    {
        return $this->articles;
    }

    public function addArticles(Article $article)
    {
        $this->articles[] = $article;
    }

    // [...]
}
class Topic 
{
    // Attributes
    private $articles; 

    // Getters and Setters
    public function getArticles()
    {
        return $this->articles;
    }

    public function addArticles(Article $article)
    {
        $this->articles[] = $article;
    }

    // [...]
}

Le but de cette démarche étant publier un Article dans un Cercle ou un Topic. Cette routine serait appellée depuis les controlleurs des Cercles et des Topics, dans deux actions quasi identiques :

Controlleur Circle :

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

use AppBundle\Form\ArticleType;
use AppBundle\Entity\Article;

class CircleController extends Controller
{
    /**
     * @Route("/circle/{id}/publish-article", name="circle_publish_article")
     */
    public function publishArticleAction(Request $request)
    {
        // On aurait pu utiliser le ParamConverter...
        $circle_manager = $this->get('circle_manager');
        $circle = $cercle_manager->findByOneById($request->get('id'));

        $article = new Article();
        $article->setCircle($circle);

        // Création du form
        $form = $this->createForm(ArticleType::class, $article);
        $form->add('save', SubmitType::class, ['label' => 'Publish']);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) 
        {
            $article = $form->getData();
            $article->setAuthor($this->getUser());

            $em = $this->getDoctrine()->getManager();
            $em->persist($article);
            $em->flush();
        }
    }
}

Controlleur Topic :

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

use AppBundle\Form\ArticleType;
use AppBundle\Entity\Article;

class TopicController extends Controller
{
    /**
     * @Route("/topic/{id}/publish-article", name="topic_publish_article")
     */
    public function createAction(Request $request)
    {
        $topic_manager = $this->get('topic_manager');
        $topic = $topic_manager->findByOneById($request->get('id'));

        $article = new Article();
        $article->setTopic($topic);

        ### Cette partie du code est redondante ###
        $form = $this->createForm(ArticleType::class, $article);
        $form->add('save', SubmitType::class, ['label' => 'Publish']);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) 
        {
            $article = $form->getData();
            $article->setAuthor($this->getUser());

            $em = $this->getDoctrine()->getManager();
            $em->persist($article);
            $em->flush();
        }
        ### Fin de partie de code redondante ###
    }
}

Nous remarquons qu'une partie de code est redondante, donc difficilement maintenable ! Nous pouvons faire en sorte que ce code soit mutualisé (via les events listeners et un peu d'huile de coude)...

Comment créer et utiliser ici un event-listener ?

Afin de bien structurer notre démarche, nous allons procéder à 4 étapes essentielles :

  1. Créer un événement (propre à notre cas d'utilisation),
  2. Créer un écouteur d'événement (ou event-listener),
  3. Déclarer l'écouteur dans le container Symfony via un fichier yaml,
  4. Utiliser le dispatcher du framework Symfony pour distribuer l'évènement.

Création de l'évènement :

namespace AppBundle\Event;

use Symfony\Component\EventDispatcher\Event;

use AppBundle\Entity\Article;

class PostCreatedEvent extends Event
{
    const NAME = 'app.event.article_created';

    protected $article;

    public function __construct(Article $article)
    {
        $this->article = $article;
    }

    public function getArticle()
    {
        return $this->article;
    }
}

Création de l'écouteur d'évènement :

namespace AppBundle\EventListener;

use AppBundle\Event\ArticleCreatedEvent;

class PostListener
{
    protected $em, 
              $article, 
              $author;

    public function __construct(\Doctrine\Common\Persistence\ObjectManager $em)
    {
        $this->em = $em;
    }

    public function onCreate(ArticleCreatedEvent $event)
    {
        $this->article = $event->getArticle();
        $this->em->persist($article);
        $this->em->flush();
    }
}

Déclarer l'écouteur :

app.article.article_listener:
    class: AppBundle\EventListener\ArticleListener
    arguments: ["@doctrine.orm.entity_manager"]
    tags:
      - { name: kernel.event_listener, event: app.event.article_created, method: onCreate }

Utilisation du dispatcher Symfony (dans un trait, par exemple) :

trait ControllerTrait 
{
    protected function getForm()
    {
        $form = $this->createForm(ArticleType::class, $article);
        $form->add('save', SubmitType::class, ['label' => 'Publish']);

        return $form;
    }

    protected function handleArticlePublication(Article $article)
    {
        $dispatcher = $this->get('event_dispatcher');
        $event = new ArticleCreatedEvent($article);
        $listener = $this->get('app.article.article_listener');

        $dispatcher->addListener('app.article.article_listener', [
            $listener, 
            'onCreateAction'
        ]);
        $dispatcher->dispatch('app.event.post_created', $event);
    }
}

Ce qui donnerait dans les deux controlleurs :

Controlleur Circle :

class CircleController extends Controller
{
    use ControllerTrait; 
    /**
     * @Route("/circle/{id}/publish-article", name="circle_publish_article")
     */
    public function publishArticleAction(Request $request)
    {
        // On aurait pu utiliser le ParamConverter...
        $circle_manager = $this->get('circle_manager');
        $circle = $cercle_manager->findByOneById($request->get('id'));

        $article = new Article();
        $article->setCircle($circle);

        $form = $this->getForm();
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) 
        {
            $article = $form->getData();
            $this->handleArticlePublication($article);
        }
    }
}

Controlleur Circle :

class TopicController extends Controller
{
    use ControllerTrait;
    /**
     * @Route("/topic/{id}/publish-article", name="topic_publish_article")
     */
    public function createAction(Request $request)
    {
        $topic_manager = $this->get('topic_manager');
        $topic = $topic_manager->findByOneById($request->get('id'));

        $article = new Article();
        $article->setTopic($topic);

        $form = $this->getForm();
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) 
        {
            $article = $form->getData();
            $this->handleArticlePublication($article);
        }
    }
}

Voici donc un exemple concret ou l'on peu juger d'une bonne utilisation d'un écouteur d'évènement.

A très bientôt,

Mathieu