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
services:
... lines 9 - 29
App\MessageHandler\Command\:
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
services:
... lines 9 - 29
App\MessageHandler\Command\:
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
services:
... lines 9 - 29
App\MessageHandler\Command\:
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
services:
... lines 9 - 34
App\MessageHandler\Event\:
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.

framework:
messenger:
default_bus: command.bus
buses:
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
services:
... lines 9 - 29
App\MessageHandler\Command\:
... 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.

framework:
messenger:
... lines 3 - 4
buses:
... lines 6 - 9
event.bus:
... line 11
middleware:
- 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!

4
Login or Register to join the conversation
Default user avatar
Default user avatar Gianluca | posted 1 year ago

We are using two bus for command and event; let you have an Handler who react to an Event. This handler has to perform an action that usually is handled by a Command but after it has to dispatch an Event.
Example: we have a microservices who send SMS. There is a main command "SendSMSCommand"
Another MS need to send an SMS when dispatching an Event. This Event is catched from SMS with a dedicated Handler, who has to inject: eventBus AND commandBus, cause it has to use CommandBus to handle the sending of SMS, and eventBus to throw an new SmsSentEvent. Is it correct? To use two buses in an EventHandler?

1 Reply

Hey Gianluca

Yea, I don't see a problem injecting two Message busses into the same service, as long as you have a valid use-case

Cheers!

Reply
Dang Avatar
Dang Avatar Dang | posted 1 month ago | edited

Hello guys,

In Symfony 6.2 when I run php bin/console debug:messenger, I see there a bunch of components like Symfony\Component\Mailer\Messenger\SendEmailMessage, Symfony\Component\Notifier\Message\PushMessage ... go to all the bus that I have. The problem is that I have a bus (say bus A) with a middleware which have logic only for one message handler. So if I understand correctly, the SendEmailMessage for instance could go to my bus A, through the middleware of that bus A and crash. If that's correct how can I prevent it, is there a way to say my bus A is for only one kind of message and no one else should go through it?

Thanks

Reply

Hey @Dang!

Hmm. Let's think about this. There are 2 ways for a message to be dispatched through a bus:

A) YOU are dispatching it directly in your code. So you have control of which message bus you're using.
B) Some 3rd party code (e.g. Symfony) dispatches the message. In this case, by convention, Symfony always uses the "main" message bus. Well, more precisely, it uses the main message bus, unless you tell it otherwise: https://github.com/symfony/symfony/blob/43066ff98d158946f6b5f8473802c9301f0abf49/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L2584-L2589 - the same is true for notifier - https://github.com/symfony/symfony/blob/43066ff98d158946f6b5f8473802c9301f0abf49/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L2692-L2696

Via your own code or this config, you can, in practice, control which messages go to which bus. However, on a purely theoretical level, there would still be nothing stopping something from dispatching the "wrong" message into the wrong bus. And that's why you see debug:messenger reporting things like SendEmailMessage under your bus (because it is theoretically possible for this to happen). There's no way to strictly prevent this (someone could always grab your bus and call ->dispatch() on it), but if you're concerned, I would add a middleware to your bus that detects if an "invalid" message is dispatched and throws an exception. It would still "crash" but at least in a controlled way (and then ideally you'd be notified and could fix whatever bug caused this).

Anyway, I hope this explanation helps!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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
    }
}
userVoice