Using Symfony's Event Dispatcher in Fork CMS

Symfony's Event Dispatcher has been around in Fork CMS for some time now, but is only recently being integrated in our modules. It's not hard to use it though, and it's a great tool to decouple code in your modules. This post will explain in small steps how you can use it in your own Fork CMS modules.

What are events?

To fully understand Symfony's Event Dispatcher, you need to grasp some more abstract concepts. The first thing you need to understand are events. In programming, an event can be seen as a message that represents something that happened in your code. An example of this could be a "ProfileRegistered" event.

In our code, we will represent this event using an object. This object contains data about the event that has happened. For a ProfileRegistered event, this will possibly contain some data about the registered profile, or even the full profile object.

What is an event dispatcher?

An event dispatcher is a service that will send your events to possible listeners. In our example, it will receive the "ProfileRegistered" event and call all hooked listeners. The amount of listeners can be anything. It's possible to have zero listeners, but you can also have multiple listeners for the same event. This makes it easy to add or remove extra code without needing to change existing classes.

How can we implement this in Fork?

Since we use Symfony's Event Dispatcher component, the implementation in Fork is almost exactly the same as in Symfony. The only difference is where we locate our objects. We use modules instead of bundles, so we're putting our events and listeners in there.

1. The event

The first thing we need to create is our event. This will just be a plain php object that receives some data and can return it back to the event listeners. For our ProfileRegistered event, it can look something like this:

// src/Frontend/Modules/Profiles/Event/ProfileRegistered.php

namespace Frontend\Modules\Profiles\Event;

use Symfony\Component\EventDispatcher\Event;
use Frontend\Modules\Profiles\Engine\Profile;

/**
 * This event will contain all data about a registration
 */
final class ProfileRegistered extends Event
{
    /**
     * @var Profile
     */
    protected $profile;

    /**
     * @param Profile $profile
     */
    public function __construct(Profile $profile)
    {
        $this->profile = $profile;
    }

    /**
     * @return Profile
     */
    public function getProfile()
    {
        return clone $this->profile;
    }

Note that we clone our profile event object in the getProfile method. We do this to make sure the listeners can't edit the profile object before it is passed to another listener. We don't want to have a listener that changes the data of the newly registered profile. They can only use the data, but not change it.

2. Dispatching our event

We have created our event object, but it's just an unused class in our directory structure now. We want to dispatch this event whenever a profile is registered. This event can happen in two different places in our applications: a visitor can register in the frontend, or an admin can add a profile from the backend. We'll add this code on these two places:

// src/Frontend/Modules/Profiles/Actions/Register.php

use src/Frontend/Modules/Profiles/Event/ProfileRegistered;

// add this code after the user has been registered/added
// and you have fetched the Profile object
$this->get('event_dispatcher')->dispatch(
    'profiles.profile_registered',
    new ProfileRegistered($profile)
);

As you can see, the event_dispatcher is a service in our service container (you can read more about service containers in Fork here: http://www.fork-cms.com/blog/detail/module-specific-services).

We call the "dispatch" method on it and pass it two parameters. The first one is the name of your event. This will couple your event to the listeners. In this case, listeners will need to listen to the "profiles.profile_registered" event to be executed and to receive the ProfileRegistered object.
The second argument is our ProfileRegistered object. We insert our profile in there, to make sure the listeners can use it.

3. Creating an event listener

We now have an event that gets dispatched, but we aren't doing anything with it. In most cases, you will create at least one listener for an event. In this case, we willl add a listener that sends a confirmation email to the visitor that registered a new profile. Other use cases could be: sending push notifications, adding a search index, notifying an external api, ... The possibilities are endless.

First, we will create a listener class. This could also be called a "subscriber" in some other languages/frameworks. A listener class will need a method that will be called when the event occurs. I will name the class "RegistrationConfirmationMailListener" and name the method "onProfileRegistered". These names can be everything you want, but in most cases, you want to explain what happens in the class name and add a reference to the dispatched event in the method name. Our example class looks like this:

// src/Frontend/Modules/Profiles/EventListener/RegistrationConfirmationMailListener.php

namespace Frontend\Modules\Profiles\EventListener;

use Swift_Mailer;
use Swift_Message;
use Frontend\Modules\Profiles\Event\ProfileRegistered;

class RegistrationConfirmationMailListener
{
    /**
     * @var Swift_Mailer
     */
    private $mailer;

    /**
     * @var Swift_Mailer $mailer
     */
    public function __construct(Swift_Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    /**
     * @var ProfileRegistered $event
     */
    public function onProfileRegistered(ProfileRegistered $event)
    {
        $profile = $event->getProfile();

        // build an email message.
        $message = Swift_Message::newInstance('Subject');
        $message->setTo(array($profile->getUserName(), $profile->getEmail()));

        // ... You'll probably want to add content too
        $this->mailer->send($message);
    }

We pass our mailer class in our constructor using dependency injection. The onProfileRegistered method contains all the logic for this listener.

4. Hook your listener to the event

To make our listener really listen to our event, we still have to add the listener to our configuration. If you module contains a module specific services.yml file (in Backend/Modules/YourModule/Resources/config/services.yml), you can add it there, if this is not the case, you can use the app/config/config.yml file. You will need to add a part that looks like this:

# app/config/config.yml

# You should add it under the services: key. Note: indentation is important!
services:
    profiles.registration_confirmation_mail_listener:
        class: Frontend\Modules\Profiles\EventListener\RegistrationConfirmationMailListener
        arguments:
            - @mailer
        tags:
            - { name: kernel.event_listener, event: profiles.profile_registered, method: onProfileRegistered }

This registers our listener in the service container. More info about building services can be found in this blogpost: www.fork-cms.com/blog/detail/module-specific-services.
To convert our service in an event listener, we add a tag with the name 'kernel.event_listener'. The event key contains the name of the event we're listening to and the method is used to set the called method.

Wrap up

It may seem like some work to set this up for your modules, but when you start using it, you will definitly see that it is totally worth it. It makes adding/removing functionality so much easier. You can now change what happens after events without the need to touch your actual action classes. This way, you're sure you're not introducing bugs in your action class, for functionality that does not even belong there. It keeps your classes smaller and more understandable. If you want to, you can also easily create unit tests for your event listener class!

The FormBuilder module already uses this functionality in Fork, for example to send notification emails to the admin when a form has been submitted. If you're implementing this, and you're stuck, you can always take a look in there to get some inspiration.

Have fun creating your own events and making your modules expandable!