Use an event listener

Hello,

Today, I want to show you how to use event listeners in the Symfony framework wisely..

First, we will set a context for the use of this design pattern: We imagine that we need to manage the creation of an Article in several Circles or Topics, ie two different controllers but in the same way.

What are the advantages of using an event-listener here?

It will mutualize the code executed during the creation of the Article, decouple the relations between our classes Circles or Topics and Article and will allow a better maintainability during probable evolutions (When adding other places or publishing an Article) ...

we would therefore have 3 participating entities:

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;
    }

    // [...]
}

The goal of this step is to publish an Article in a Circle or Topic. This routine would be called from the controllers of Circles and Topics, in two almost identical actions:

Circle Controller :

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)
    {
        $circle_manager = $this->get('circle_manager');
        $circle = $cercle_manager->findByOneById($request->get('id'));

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

        // Form creation
        $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();
        }
    }
}

Topic Controller :

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);

        ### This part of the code is redundant ###
        $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();
        }
        ### End of redundant code portion ###
    }
}

We notice that some code is redundant, so it is difficult to maintain! We can make this code shared (via the events listeners)...

How to create and use an event-listener ?

In order to structure our approach, we will proceed to 4 essential steps :

  1. Create an event (specific to our use case),
  2. Create an event listener,
  3. Declare the listener in the Symfony container via a yaml file,
  4. Use the Symfony Framework dispatcher to distribute the event.

Event creation :

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;
    }
}

Event listener creation :

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éclare the listener :

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 }

Using the Symfony Dispatcher (in a trait, for example) :

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);
    }
}

Which would give in two controllers :

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);
        }
    }
}

Here is a concrete example where we can judge a good use of an event listener.

See you soon,

Mathieu