This week, in the Symfony2 components overview series, it’s time to talk about another really important component: the EventDispatcher.

2812073310_38ee1db2ce_o

Event listeners are like ninja cats. You can’t see them, but they are there waiting for the signal $event to come

The EventDispatcher component

For many years, in the PHP world we were used to code following a linear flow, where instructions were executed one after another, jumping to functions or methods. Events change the way we design our software and allow us to decouple different parts creating small and reusable classes with a well-defined goal.

For example, imagine you have a blog system and want to send an email to all users when a new blog post is published. Following the events approach, you would create a class responsible for sending out the emails which listens the made up ‘blog.post.saved’ event.

Mediator pattern

Basically, the Mediator pattern decouples a Producer from a Consumer. As communication between objects is encapsulated with a mediator object, they no longer communicate directly with each other, but instead through the mediator.

Mediator pattern

The Producer - which in our previous example is the action that inserts the new blog post into the database – does not ask the Consumer to send out the emails. The Producer does not even know there must be sent any email at all. Instead, this is how it works:

  • The consumer says: “Hi Mediator, please inform me when a new blog post is persisted in the database”.
  • Other two consumers also want to know when a new blog post has been published. The first one wants to regenerate the RSS feed and the second one wants to send the pingbacks to notify the authors of all the cited posts. So both ask the Mediator to inform them as soon as a new blog post is persisted.
  • Finally, a new post is created, so the producer says: “Hi Mediator, I just inserted a new blog post into the database, let others know!”.
  • The Mediator informs all the listeners of the event according to priority.

As you can see, both producer and consumers don’t even know about other’s existence. They are decoupled and any consumer can be disabled without affecting the program.

Simple example

This is a simple example to explain the basic functionality of the EventDispatcher component:

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\Event;

$dispatcher = new EventDispatcher();

// add listeners
$dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) {
    echo 'Updating RSS feed' . PHP_EOL;
});
$dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) {
    echo 'Sending emails' . PHP_EOL;
});

$blogPost = new BlogPost(...);
$manager = new Manager(...);

$manager->save($blogPost);

// dispatch the event
$event = new BlogPostEvent($blogPost);
$dispatcher->dispatch('blog.post.saved');

The above example defines two listeners using anonymous functions, one for updating the RSS feed and the other one to send the emails. We used PHP anonymous functions to simplify the example, but any PHP callable may be passed. The functions receive one parameter – BlogPostEvent $event -, which provides the dispatcher object – useful for event chaining - and the event name. The class BlogPostEvent is a custom class which extends from Symfony\Component\EventDispatcher\Event allowing the listener to get the newly created blog post.

Once we defined the two listeners, our example creates the blog post object and saves it into the database using a manager. This depends on your application and it is not important for the example. Then, an event object is created and dispatched so all the listeners are informed.

The output, as expected, is:

Updating RSS feed
Sending emails

This is the moment you should start realizing how powerful events are. If tomorrow your client wants to send a tweet automatically every time a blog post is published, it will be as easy as create the class to do the actual tweeting and add the listener, without messing around with the current code. This is the magic of decoupling!

Event listeners vs Event subscribers

There is another way to listen to events using event subscribers. An event subscriber is a class that implements the EventSubscriberInterface interface and is able to define which events is going to be subscribed to.

namespace ServerGrove\BlogBundle\Event;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class BlogPostSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            'blog.post.saved' => array(
                array('updateRss', 10),
                array('sendEmails', 5)
            )
        );
    }

    public function updateRss(BlogPostEvent $event)
    {
        // ...
    }

    public function sendEmails(BlogPostEvent $event)
    {
        // ...
    }
}

The BlogPostSubscriber class tells the dispatcher that it will listen to ‘blog.post.saved’ events, executing first updateRss – it has more priority, 10 vs 5 – and then sendEmails.

To register the subscriber:

$subscriber = new BlogPostSubscriber();
$dispatcher->addSubscriber($subscriber);

Performance

The little overhead added by the dispatcher object pays by itself as the decoupling makes it really easy to add or remove features. Anyway, three aspects must be taken into account when using events to keep them under control:

  • Be specific: It really depends on your application, but as a general rule, be as much specific as possible. For example, don’t use the same event for saving posts and comments.
  • Early return: Sometimes, listeners must be executed only if some requirements are fulfilled. For example, if emails only have to be sent if the blog post has the tag ‘symfony’, must be checked as soon as possible, especially if the event is too generic and gets called often:
    $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) {
        if (!$event->getBlogPost()->hasTag('symfony')) {
            return;
        }
    
        // send emails
        // ...
    });
    
  • Stop propagation: If there are several listeners for the same event, one of them can prevent any other listeners from being called:
    $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) {
        $event->stopPropagation();
    });
    

Who’s using it

More info

Photo: Ama-gat / Ninja cat, by Xavi