Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Event & Command Bus Organization

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We already organized our new event class into an Event subdirectory. Cool! Let's do the same thing for our commands. Create a new Command/ sub-directory, move the two command classes inside... then add \Command to the end of the namespace on both classes.

Let's see... now that we've changed those namespaces... we need to update a few things. Start in messenger.yaml: we're referencing AddPonkaToImage. Add Command to that class name. Next, in ImagePostController, all the way on top, we're referencing both commands. Update the namespace on each one.

And finally, in the handlers, we have the same thing: each handler has a use statements for the command class it handles. Add the Command\ namespace on both.

Cool! Let's do the same thing for the handlers: create a new subdirectory called Command/, move those inside... then add the \Command namespace to each one. That's... all we need to change.

I like it! There was nothing technical about this change... it's just a nice way to organize things if you're planning to use more than just commands - meaning events or query messages. And everything will work exactly the same way it did before. To prove it, at your terminal, run debug:messenger:

php bin/console debug:messenger

Yep! We see the same info as earlier.

Binding Handlers to One Bus

But... now that we've separated our event handlers from our command handlers... we can do something special: we can tie each handler to the specific bus that it's intended for. Again, it's not super important to do this, but it'll tighten things up.

Let me show you: open up config/services.yaml. This App\ line is responsible for auto-registering every class in the src/ directory as a service in the container.

The line below repeats that for classes in the Controller/ directory. Why? This will override the controller services registered above and add a special tag that controllers need to work.

We can use a similar trick with Messenger. Say App\MessageHandler\Command\, then use the resource key to re-auto-register all the classes in the ../src/MessageHandler/Command directory. Whoops - I typo'ed that directory name - I'll see a huge error in a few minutes... and will fix that.

... lines 1 - 7
... lines 9 - 29
resource: '../src/MessageHandler/Command'
... lines 32 - 44

If we only did this... absolutely nothing would change. This would register everything in this directory as a service... but that's already done by the first App\ entry anyways.

But now we can add a tag to this with name: messenger.message_handler and bus: set to... the name of my bus from messenger.yaml. Copy messenger.bus.default and say bus: messenger.bus.default.

... lines 1 - 7
... lines 9 - 29
resource: '../src/MessageHandler/Command'
... line 32
tags: [{ name: messenger.message_handler, bus: messenger.bus.default }]
... lines 34 - 44

There are a few things going on here. First, when Symfony sees a class in our code that implements MessageHandlerInterface, it automatically adds this messenger.message_handler tag. This is how Messenger knows which classes are message handlers.

We're now adding that tag manually so that we can also say exactly which one bus this handler should be used on. Without the bus option, it's added to all buses.

We also need to add one more key: autoconfigure: false.

... lines 1 - 7
... lines 9 - 29
resource: '../src/MessageHandler/Command'
autoconfigure: false
tags: [{ name: messenger.message_handler, bus: messenger.bus.default }]
... lines 34 - 44

Thanks to the _defaults section on top, all services in our src/ directory will, by default, have autoconfigure enabled... which is the feature that's responsible for automatically adding the messenger.message_handler tag to all services that implement MessageHandlerInterface. We're turning it off for services in this directory so that the tag isn't added twice.

Phew! You can see the end result by running debug:messenger again.

php bin/console debug:messenger

Oh, the end result is a huge error thanks to my typo! Make sure you're referencing the MessageHandler directory. Try debug:messenger again:

php bin/console debug:messenger

Nice! The event bus no longer says that we can dispatch the two commands two it. What this really means is that the command handlers were added to the command bus, but not to the event bus.

Let's repeat this for the events: copy this section, paste, change the namespace to Event\, the directory to Event and update the bus option to event.bus - the name of our other bus inside messenger.yaml.

... lines 1 - 7
... lines 9 - 34
resource: '../src/MessageHandler/Event'
autoconfigure: false
tags: [{ name: messenger.message_handler, bus: event.bus }]
... lines 39 - 44

Cool! Try debug:messenger again:

php bin/console debug:messenger

Perfect! Our two command handlers are bound to the command bus and our one event handler is tied to the event bus.

Again, doing this last step wasn't that important... but I do really like these sub-directories... and tightening things up is nice.

Renaming the Command Bus

Oh, but while we're cleaning things up, back in config/packages/messenger.yaml, our main bus is called messenger.bus.default, which becomes the bus's service id in the container. We used this name... just because that's the default value Symfony uses when you have only one bus. But because this is a command bus, let's... call it that! Rename it to command.bus. And above, use that as our default_bus.

default_bus: command.bus
... lines 7 - 35

Where was the old key referenced in our code? Thanks to the fact that we autowire that service via its type-hint... almost nowhere - just in services.yaml. Change the bus option to command.bus as well.

... lines 1 - 7
... lines 9 - 29
... lines 31 - 32
tags: [{ name: messenger.message_handler, bus: command.bus }]
... lines 34 - 44

Check everything out by running debug:messenger one more time:

php bin/console debug:messenger

That's nice: two buses, each with a great name and only aware of the correct handlers.

Oh, and this AuditMiddleware is something that we really should also use on event.bus: it logs the journey of messages... which is equally valid here.

... lines 3 - 4
... lines 6 - 9
... line 11
- App\Messenger\AuditMiddleware
... lines 14 - 37

If you love this organization, great! If it seems like too much, keep it simple. Messenger is here to do what you want. Next, let's talk about the last type of message bus: the query bus.

Leave a comment!

This tutorial is built with Symfony 4.3, but will work well on Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // v1.8.0
        "doctrine/doctrine-bundle": "^1.6.10", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
        "doctrine/orm": "^2.5.11", // v2.6.3
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.1
        "sensio/framework-extra-bundle": "^5.3", // v5.3.1
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/flex": "^1.9", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/property-access": "4.3.*", // v4.3.2
        "symfony/property-info": "4.3.*", // v4.3.2
        "symfony/serializer": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.5", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.7
        "symfony/debug-bundle": "4.3.*", // v4.3.2
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/var-dumper": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2