The fastest way to EventListener in Symfony2

img-listen-lrg

Event Driven Design is really popular topic for about two years and I don’t suprised this trend. You can really fast decouple parts of code without any influence on any other – you can send emails, saving logs or data to database without additional mess in your core class.

The main idea

As you can see on the diagram we need to implement, use or register a couple of extra class to properly use events in the system.

dispatcher_uml

Our central entry point of course ConreteClassFoo where we have implemented our business logic and we want to inform every registered EventListener that they can do whatever should they do. We can do this by calling method Dispatch in Dispatcher. To pass all required information from our class to each EvenListener we need to implement an Event. I believe it’s working more as a data structure than proper class, because it mostly stores actual data from ConreteClassFoo.

Step by step – ConreteClassFoo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class ConcreteClassFoo
{

    /**
    * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
    **/

    protected $dispatcher;

    public function __construct(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }
   
    public function awesomeWork()
    {
        //do your awesome work here
        //we will initialize Event object here, but later
        $this->dispatcher->dispatch('pp.awesome_work.before', /**???**/);
        //do your awesome work here
        $this->dispatcher->dispatch('pp.awesome_work.after', /**???**/);
    }
}

As you can see I have injected Dispatcher object to our class. Let’s just imagine the awesomeWork method is responsible for registering new user in very, very complex system. The system requires to send different emails to a couple of supervisors after action, maintain system logs, register new user in a few external webservices. I think we wouldn’t like to inject all of these objects to our class, do we? Once, I have heard a great principle about the number of parameters in every method – one is the best, two is cool, three is too much and you should redesign your solution. Moreover, this would be much more comfortable when you will write tests for your class – you need to mock only and only one method in our case – the dispatch.

When you will do this exercise on your own application you would probably notice that we need to somehow inject our dispatcher to the class and the Symfony’s documentation doesn’t tell as how (or it’s quite hard to find this). We can use `event_dispatcher` service:

1
2
3
4
5
services:
    pp.awesome_work.concrete_class_foo:
        class: ConcreteClassFoo
        arguments:
            - @event_dispatcher

But as you probably noticed I have put /**???**/ comments as a parameter and promised that we will do something with that and replace with proper object. Well, let’s do it.

A Principle about the number of parameters in every method – one is the best, two is cool, three is too much and you should redesign your solution

Event

As I wrote at the beginning we can pass a piece of information from our class or method to EventListeners. Using raw arrays is, of course, bad idea. Following the Symfony’s documentation we need to define our own data structure which will extends the Event class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

use Symfony\Component\EventDispatcher\Event;

class AwesomeWorkEvent extends Event
{

    /**
     * @var OneDataClass
     */

    protected $data;

    public function __construct(OneDataClass $data)
    {
        $this->data = $data;
    }

    public function getData()
    {
        return $this->data;
    }
}

This is the simplest example I can imagine, even for this article. We pass some class called OneDataClass which was made in awesomeWork method to the constructor (because our Event object cannot exist without any data inside).

So, we need to update our ConcreteClassFoo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class ConcreteClassFoo
{

    /**
    * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
    **/

    protected $dispatcher;

    public function __construct(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }
   
    public function awesomeWork()
    {
        //do your awesome work here
        $beforeEvent = new AwesomeWorkEvent($someDataObject);
        $this->dispatcher->dispatch('pp.awesome_work.before', $beforeEvent);
        //do your awesome work here
        $afterEvent = new AwesomeWorkEvent($someDataObject);
        $this->dispatcher->dispatch('pp.awesome_work.after', $afterEvent);
    }
}

Edit: Why do we initiate AwesomeWorkEvent twice? Because @Stof70 said to do this (:D) and I think it makes sense:

The dispatcher allows to stop the propagation to next listeners by calling “$event->stopPropagation()“. As you reuse the same event instance for each dispatching, stopping the propagation of the first event will also stop the propagation of the second one (meaning it will never be dispatched to listeners).

We have added the instance of AwesomeWorkEvent called $event to dispatcher and assigned it to two actions. All event listeners will be called with this arguments now.

Listeners

That’s the last point. We need to perform our logic in some place and call actions.

1
2
3
4
5
6
7
8
9
namespace PP\AwesomeBundle\EventListener;

class AwesomeWorkListener
{
    public function doYourJob(AwesomeWorkEvent $event)
    {
        /** Do the right job **/
    }
}

So, now we’ve got the place, but the framework doesn’t know about this. We need to inform him in the configuration about our listener. To do this we can use specified tag:

1
2
3
4
5
6
;PP/AwesomeBundle/Resources/config/service.yml
services:
    pp_awesome.awesome.listener:
        class: %pp_awesome.awesome.listener.class%
        tags:
            - { name: kernel.event_listener, event: pp.awesome_work.before, method: doYourJob }

 
 
Of course, you can add more listeners to one dispatched event. If you want to see how to use Events and Event Listeners in more advanced example you can read a http://piotrpasich.com/query-manipulation/ and visit the github https://github.com/piotrpasich/symfony-search-solution .

Piotr

[FM_form id="1"]


  • Christophe Coevoet

    Hi,

    There is a small issue in your example, which could create unwanted bugs in your code when you start using advanced features of the EventDispatcher component: the dispatcher allows to stop the propagation to next listeners by calling “$event->stopPropagation()“. As you reuse the same event instance for each dispatching, stopping the propagation of the first event will also stop the propagation of the second one (meaning it will never be dispatched to listeners).
    To avoid such issue, you should create a new event instance for each dispatching.

    • http://piotrpasich.com Piotr Pasich

      Christophe,

      for some reason I totally forgot about this issue. Thanks for the advice.

  • Pingback: Today on the PHP Community … August 7, 2014 | PHP Community Magazine()

  • Piotr Karszny

    This is really good example Peter, that kind of examples should reside in Symfony’s documentation.
    BR.

    • http://piotrpasich.com Piotr Pasich

      Piotr,

      this exactly why I made this article. I’ll try to write something similar and push to Symfony’s documentation. I must admit this part i docs can be really confusing for beginners.

  • Euzebiusz Hosse

    Hi,

    The article is very good. You could improve it by changing diagram, because is a little bit confusing. At first glance I thought that ConcreteClassFoo extends Dispatcher. Maybe sequence diagram will be more readable.

    thx,
    Rafal

  • MortisDux

    Hello,
    Really nice explained!
    What if you want your event to be dispatched after your response has been sent, like kernel.terminate event, to send emails, log stats or whatever you want to do?
    Is there a way to do that? Or to use the kernel.terminate event and pass to it some data from your controller?

    Thanks in advance for your help.

    • Piotr Pasich

      Hi,

      maybe you can call the listener just a little bit earlier, when the kernel.response event is sent. This makes you able to catch some errors which can occur during saving data for logs.

      Sending emails shouldn’t be a part of the kernel.* stuff I think. Unless you want to receive an email every time when somebody refreshes your site.

    • MortisDux

      hello, thanks for the answer, actually I found it yesterday: you can listen to 2 event with your listener, in the first event that you dispatch, you set up your data, then on kernel.terminate event you check if the data is up and you process it then.
      I need that to log stat every time someone access a url.