If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeBack in DeleteImagePostHandler
, we need to dispatch our new ImagePostDeletedEvent
message. Earlier, we created a second message bus service. We now have a bus that we're using as a command bus called messenger.bus.default
and another one called event.bus
. Thanks to this, when we run:
php bin/console debug:autowiring mess
we can now autowire either of these services. Just using the MessageBusInterface
type-hint will give us the main command bus. But using that type-hint plus naming the argument $eventBus
will give us the other.
Inside DeleteImagePostHandler
, change the argument to $eventBus
. I don't have to, but I'm also going to rename the property to $eventBus
for clarity. Oh, and variables need a $
in PHP. Perfect!
... lines 1 - 11 | |
class DeleteImagePostHandler implements MessageHandlerInterface | |
{ | |
private $eventBus; | |
... lines 15 - 16 | |
public function __construct(MessageBusInterface $eventBus, EntityManagerInterface $entityManager) | |
{ | |
$this->eventBus = $eventBus; | |
... line 20 | |
} | |
... lines 22 - 32 | |
} |
Inside __invoke()
, it's really the same as before: $this->eventBus->dispatch()
with new ImagePostDeletedEvent()
passing that $filename
.
... lines 1 - 5 | |
use App\Message\Event\ImagePostDeletedEvent; | |
... lines 7 - 11 | |
class DeleteImagePostHandler implements MessageHandlerInterface | |
{ | |
... lines 14 - 22 | |
public function __invoke(DeleteImagePost $deleteImagePost) | |
{ | |
... lines 25 - 30 | |
$this->eventBus->dispatch(new ImagePostDeletedEvent($filename)); | |
} | |
} |
That's it! The end result of all of this work... was to do the same thing as before, but with some renaming to match the "event bus" pattern. The handler performs its primary task - deleting the record from the database - then dispatches an event that says:
An image post was just deleted! If anyone cares... do something!
In fact, unlike with commands, when we dispatch an event... we don't actually care if there are any handlers for it. There could be zero, 5, 10 - we don't care! We're not going to use any return values from the handlers and, unlike with commands, we're not going to expect that anything specific happened. You're just screaming out into space:
Hey! An ImagePost was deleted!
Anyways, the last piece we need to fix to make this truly identical to before is, in config/packages/messenger.yaml
, down under routing
, route App\Message\Event\ImagePostDeletedEvent
to the async
transport.
framework: | |
messenger: | |
... lines 3 - 29 | |
routing: | |
... lines 31 - 32 | |
'App\Message\Event\ImagePostDeletedEvent': async |
Let's try this! Find your worker and restart it. All of this refactoring was around deleting images so... let's delete a couple of things, move back over and... yea! It's working great! ImagePostDeletedEvent
is being dispatched and handled.
Oh, and side note about routing. When you route a command class, you know exactly which one handler it has. And so, it's super easy to think about what that handler does and determine whether or not it can be handled async.
With events, it's a bit more complicated: this one event class could have multiple handlers. And, in theory, you might want some to be handled immediately and others later. Because Messenger is built around routing the messages to transports - not the handlers - making some handlers sync and others async isn't natural. However, if you need to do this, it is possible: you can route a message to multiple transports, then configure Messenger to only call one handler when it's received from transport A and only the other handler when it's received from transport B. It's a bit more complex, so I don't recommend doing this unless you need to. We won't talk about how in this tutorial, but it's in the docs.
Anyways, I mentioned before that, for events, it's legal on a philosophical level to have no handlers... though you probably won't do that in your application because... what's the point of dispatching an event with no handlers? But... for the sake of trying it, open RemoveFileWhenImagePostDeleted
and take off the implements MessageHandleInterface
part.
... lines 1 - 8 | |
class RemoveFileWhenImagePostDeleted | |
{ | |
... lines 11 - 21 | |
} |
I'm doing this temporarily to see what happens if Symfony sees zero handlers for an event. Let's... find out! Back in the browser, try to delete an image. It works! Wait... oh, I forgot to stop the worker... let's do that... then try again. This time... it works... but in the worker log... CRITICAL error!
Exception occurred while handling
ImagePostDeletedEvent
: no handler for message.
By default, Messenger requires each message to have at least one handler. That's to help us avoid silly mistakes. But... for an event bus... we do want to allow zero handlers. Again... this is more of a philosophical problem than a real one: it's unlikely you'll decide to dispatch events that have no handlers. But, let's see how to fix it!
In messenger.yaml
, take the ~
off of event.bus
and add a new option below: default_middleware: allow_no_handlers
. The default_middleware
option defaults to true
and its main purpose is to allow you to set it to false
if, for some reason, you wanted to completely remove the default middleware - the middleware that handle & send the messages, among other things. But you can also set it to allow_no_handlers
if you want to keep the normal middleware, but hint to the HandleMessageMiddleware
that it should not panic if there are zero handlers.
framework: | |
messenger: | |
... lines 3 - 4 | |
buses: | |
... lines 6 - 9 | |
event.bus: | |
default_middleware: allow_no_handlers | |
... lines 12 - 35 |
Go back and restart the worker. Then, delete another image... come back here and... cool! It says "No handler for message" but it doesn't freak out and cause a failure.
So now our command bus and event bus do have a small difference... though they're still almost identical... and we could really still get away with sending both commands and events through the same bus. Put the MessageHandlerInterface
back on the class... and restart our worker one more time.
... lines 1 - 8 | |
class RemoveFileWhenImagePostDeleted implements MessageHandlerInterface | |
{ | |
... lines 11 - 21 | |
} |
Now that we're feeling good about events... I have a question: what's the difference between dispatching an event into Messenger versus dispatching an event into Symfony's EventDispatcher?
Let's talk about that next.
I have one more interesting point about the event dispatch after we call flush on doctrine. What if a messaging system such as RabbitMQ is temporary down. Your code just has completed the transaction, but you might lose consistency because the event was not delivered to the messaging system. How is it possible to tolerate such as failer?
We could log the error. And the developer will try to figure things out and restore consistency later on.
I think the best approach to prevent such a failure is to save the event into the local table and send it to the messaging system via CRON. It will ensure that the message will be sent or rollback if the transaction has failed due to some reason. What do you think about it?
Hey Max A.!
Yes, this is a big, complex topic. I don't know if it will fully answer your question, but here are some details about this type of "atomic" problem in general: https://symfony.com/doc/cur...
Let me know if it helps :). I'll admit that this is not an area I am an absolute expert in. In general, when RabbitMQ is down, I believe the idea is to have a "fallback" storage mechanism for those messages - like a local database. There is currently no built-in fallback mechanism like that in Messenger. However, you could (I believe) wrap your "dispatch" in a try-catch and then either (A) rollback the other changes in a catch or (B) dispatch a different message (maybe that even has the original message inside of it) that would route to a local transport.
Cheers!
"Try-catch" won't solve the problem of the message has been sent to the rabbit, but the transaction has not been completed, for example, the constrain problem (email must be unique).
Hey Max A. !
Sorry for the slow reply - I had a family matter come up.
> "Try-catch" won't solve the problem of the message has been sent to the rabbit, but the transaction has not been completed, for example, the constrain problem (email must be unique).
You're totally right. I believe the thinking on this (but I'm far from an expert) would then be that you would commit the transaction first, then dispatch to Rabbit. THAT dispatch() would be surrounded by a try-catch so that, if delivery failed for some reason, you could send the message to some sort of "local" fallback system.
Let me know if this helps :).
Cheers!
Hi! I have the same question, because the dispatch() method doesn't throw a exception or any kind of throwable, so there is no way to know if the message has been dispatched at all. Someone has a workaround for this situation?
Hey John!
I believe that if dispatch() fails to deliver the message, then an exception *would* be thrown. For example, if you dig a bit into the amqp transport, you'll find this - https://github.com/symfony/... - you can see that if sending fails, that TransportException is thrown.
Does that help? Are am I talking about the wrong thing here? :)
Cheers!
Hmm, in what circumstances would we use events? Registering a user is a good example of multiple things that need to occur, but using an event system seems to add a layer of complexity without apparent benefits.
What is it for, to create a system to allow 3rd party Devs to "hook in" to and modify the process or something? 🤔
Hey Cameron,
Events allow you to add *some* extra changes without needing to overwrite the whole method for example. But if some third-party code does not have events - the only way to do some changes is overwriting that means you would literally overwrite some code even that one you don't need to change. Usually it comes to just copy/pasting some third-party code to your classes and use them instead. So, events add some complexity, but at the same time it makes third-party code more flexible - you literally can put some *your* code into third-party one. Because editing third-party code directly, i.e. editing files in vendor/ directory isn't option, right? :)
So, it depends, but in some use cases events helps a lot. But definitely not all third-party code needs them, so you shouldn't just blindly add events to your code that someone uses as third-party code, there should be some good reasons for this.
I hope it's clearer for you now!
Cheers!
Thanks, so the use case appears to be if I write a piece of code that I intend on allowing other developers to "hook" into. So if I'm building an app without this requirement, it would be less complex and therefore correct to build the app without events and instead use a controller that calls a service to complete the work?
"without needing to overwrite the whole method" - I don't see this being a problem, if the code is written correctly, i.e. where the controller calls a method that has abstracted away the complexity of the operation into a service. Adding "another" piece of code to execute should be as simple as making a single call to a service that will do the work (from the controller) - which removes the complexity of using events which only seem to further complicate development.
Apart from the above use case (allowing devs to hook into a process I've developed) it's not clear at this point in the training that there's another use case for events.
Hey Cameron! It's nice to hear from you :)
I agree with Victor that events are a good way for third-party libraries to allow users to hook into the system and extend its behavior. Another reason I can think of for using events in your own application (assuming that you are able to modify any part you want) is when you want to apply the Open-Close principle, if you want to close a specific service from being modified, then, you can dispatch an event so any other part of your system can listen to it and then perform any required action. And, perhaps if you're thinking into evolving your app into a micro-services architecture, it may help orchestrating the business rules.
Cheers!
Hey Diego, thanks, so it sounds like its a kind of notification service for other servers in a microservice architecture. That's interesting 🤔
Yea, at least those are the reasons that I can think of where you can benefit from using events. A good example of it is Doctrine, it allows you to hook into basically any step of the lifecycle of an entity
It also appears that it would be a good way to build a decoupled app - the app could then be split into seperate microservices without as much refactoring.
// 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
}
}
Thank you for this solution of "philosophical" problem :) That was exactly my case: I'm dispatching message by Symfony, but for handling I'm using external Go-handler