Videa Blog

The Bulletproof Event Naming For Symfony Event Dispatcher

Tomáš Votruba  

I wrote intro to Symfony\EventDispatcher and how to use it with simple event.

But when it comes to dispatching events, you can choose from 4 different ways. Which one to choose and why? Today I will show you pros and cons of them to make it easier for you.

1. Start with Stringly

You can start with simple string named event:

$postEvent = new PostEvent($post);
$this->eventDispatcher('post_added', $postEvent)

Simple for start and easy to use for one place and one event.

One day I started to use in more places:

$postEvent = new PostEvent($post);
$this->eventDispatcher('post_add', $postEvent)

All looked good, but the subscriber didn't work. Fun time with event subscribers debugging was about to come.

Hour has passed. Event subscriber was registered as a service, tagged, collected by dispatcher... but I still couldn't find the issue. So I showed it to my colleague:

Oh, you've got "post_add" there, but there should be "post_added".

YAY! I copied the previous subscriber with "post_added" but I made a typo while dispatching event.

There must be a cure for this, I wished.

2. Group File with Events Names as Constants

Then I got inspired by Symfony ConsoleEvents class that collects all events from one domain in constants.

final class PostEvents
{
    /**
     * This event is invoked when post is added.
     * It is called here @see \App\Post\PostService::add().
     * And @see \App\Events\PostAddedEvent class is passed.
     *
     * @var string
     */
    public constant ON_POST_ADDED = 'post_added';

    /**
     * This event is invoked when post is published.
     * It is called here @see \App\Post\PostService::puslished().
     * And @see \App\Events\PostPuslishedEvent class is passed.
     *
     * @var string
     */
    public constant ON_POST_PUBLISHED = 'post_published';
}

Our first example will change from stringly to strongly typed:

$postAddedEvent = new PostAddedEvent($post);
$this->eventDispatcher(PostEvents::ON_POST_ADDED, $postAddedEvent)

Also subscriber becomes typo-proof:

final class TagPostSubscriber implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [PostEvents::ON_POST_ADDED => 'tagPost'];
    }

    public function tagPost(PostAddedEvent $postAddedEvent): void
    {
        // ...
    }
}

Pros

Cons

The more events you have the harder is this to maintain properly. With 5th event you might end up like this:

final class PostEvents
{
    /**
     * This event is invoked when post is published.
     * It is called here @see \App\Post\PostService::puslished().
     * And @see \App\Events\PostPuslishedEvent class is passed.
     *
     * @var string
     */
    public constant ON_POST_PUBLISHED = 'post_published';

    // 3 more nicely annotated events...

    public constant ON_POST_CHANGED = 'changed';
}

I wanted to respect open-closed principle, so global class was a no-go.

Maybe, I could put those...

3. ...Constant Names in Particular Event Classes

Like this:

final class PostAddedEvent
{
    /**
     * @var string
     */
    public const NAME = 'post_added';

    /**
     * @var Post
     */
    private $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }
}

Our example is now strongly typed and respects open-closed principle:

$postAddedEvent = new PostAddedEvent($post);
$this->eventDispatcher(PostAddedEvent::NAME, $postAddedEvent)

Like this!

Pros

All the above +

Cons

Take a step back: what is my goal?

I look for an identifier that is:

Can you see it? I think you do :)

4. Class-based Event Naming

$postAddedEvent = new PostAddedEvent($post);
$this->eventDispatcher(PostAddedEvent::class, $postAddedEvent)

It could not be simpler and meets all the conditions!

Pros

All 4 reasons above +

Which Type Do You Like?

This is my story for event naming evolution. But what is yours - which event naming system do you use? I'm curious and ready to be wrong, so please let me know in the comments if you like it or do it any different way.

Taking it Step Further

Enumag suggested such different way by removing first argument:

public function dispatch(Event $event): void
{
    $this->eventDispatcher->dispatch(get_class($event), $event);
}
$postAddedEvent = new PostAddedEvent($post);
$this->eventDispatcher($postAddedEvent);

// or in case we don't need to get changed content from the event

$this->eventDispatcher(new PostAddedEvent($post));

Or you can take it 2 steps further and eliminate visual debt ;)

EventDispatcher::dispatch([$post]);