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 SubscribeSo... what the heck is an event? Let me give you an example. Suppose a user registers on your site. When that happens, you do three things: save the user to the database, send them an email and add them to a CRM system. The code to do this might all live in a controller, a service or a SaveRegisteredUserHandler
if you had a SaveRegisteredUser
command.
This means that your service - or maybe your command handler - is doing three separate things. That's... not a huge deal. But if you need to suddenly do a fourth thing, you'll need to add even more code. Your service - or handler - violates the single responsibility principle that says that each function should only have to accomplish a single task.
This is not the end of the world - I often write code like this... and it doesn't usually bother me. But this code organization problem is exactly why events exist.
Here's the idea: if you have a command handler like SaveRegisteredUser
, it's supposed to only perform its principle task: it should save the registered user to the database. If you follow this practice, it should not do "secondary" tasks, like emailing the user or setting them up in a CRM system. Instead, it should perform the main task and then dispatch an event, like UserWasRegistered
. Then, we would have two handlers for that event: one that sends the email and one that sets up the user in the CRM. The command handler performs the main "action" and the event helps other parts of the system "react" to that action.
As far as Messenger is concerned, commands and events all look identical. The difference comes down to each supporting a different design pattern.
And... we already have a situation like this! Look at DeleteImagePost
and then DeleteImagePostHandler
. The "main" job for this handler is to remove this ImagePost
from the database. But it also has a second task: deleting the underlying file from the filesystem.
To do that, well, we're dispatching a second command - DeletePhotoFile
- and its handler deletes the file. Guess what... this is the event pattern! Well, it's almost the event pattern. The only difference is the naming: DeletePhotoFile
sounds like a "command". Instead of "commanding" the system to do something, an event is more of an "announcement" that something did happen.
To fully understand this, let's back up and re-implement all of this fresh. Comment out the $messageBus->dispatch()
call and then remove the DeletePhotoFile
use statement on top.
... lines 1 - 10 | |
class DeleteImagePostHandler implements MessageHandlerInterface | |
{ | |
... lines 13 - 21 | |
public function __invoke(DeleteImagePost $deleteImagePost) | |
{ | |
... lines 24 - 29 | |
//$this->messageBus->dispatch(new DeletePhotoFile($filename)); | |
} | |
} |
Next, to get a clean start: remove the DeletePhotoFile
command class itself and DeletePhotoFileHandler
. Finally, in config/packages/messenger.yaml
, we're routing the command we just deleted. Comment that out.
framework: | |
messenger: | |
... lines 3 - 29 | |
routing: | |
... lines 31 - 32 | |
#'App\Message\DeletePhotoFile': async |
Let's look at this with fresh eyes. We've successfully made DeleteImagePostHandler
perform is primary job only: deleting the ImagePost
. And now we're wondering: where should I put the code to do the secondary task of deleting the physical file? We could put that logic right here, or leverage an event.
Commands, events & their handlers look identical. In the src/Message
directory, to start organizing things a bit better, let's create an Event/
subdirectory. Inside, add a new class: ImagePostDeletedEvent
.
namespace App\Message\Event; | |
class ImagePostDeletedEvent | |
{ | |
... lines 7 - 17 | |
} |
Notice the name of this class: that's critical. Everything so far has sounded like a command: we're running around our code base shouting: AddPonkaToImage
! And DeleteImagePost
! We sound bossy.
But with events, you're not using a strict command, you're notifying the system of something that just happened: we're going to fully delete the image post and then say:
Hey! I just deleted an image post! If you care... uh... now is your chance to... uh... do something! But I don't care if you do or not.
The event itself could be handled by... nobody... or it could have multiple handlers. Inside the class, we'll store any data we think might be handy. Add a constructor with a string $filename
- knowing the filename of the deleted ImagePost
might be useful. I'll hit Alt + Enter and go to "Initialize Fields" to create that property and set it. Then, at the bottom, I'll go to "Code -> Generate" - or Command + N on a Mac - and select "Getters" to generate this one getter.
namespace App\Message\Event; | |
class ImagePostDeletedEvent | |
{ | |
private $filename; | |
public function __construct(string $filename) | |
{ | |
$this->filename = $filename; | |
} | |
public function getFilename(): string | |
{ | |
return $this->filename; | |
} | |
} |
You may have noticed that, other than its name, this "event" class looks exactly like the command we just deleted!
Creating an event "handler" also looks identical to command handlers. In the MessageHandler
directory, let's create another subdirectory called Event/
for organization. Then add a new PHP class. Let's call this RemoveFileWhenImagePostDeleted
. Oh... but make sure you spell that all correctly.
namespace App\MessageHandler\Event; | |
... lines 5 - 6 | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class RemoveFileWhenImagePostDeleted implements MessageHandlerInterface | |
{ | |
... lines 11 - 21 | |
} |
This also follows a different naming convention. For commands, if a command was named AddPonkaToImage
, we called the handler AddPonkaToImageHandler
. The big difference between commands and events is that, while each command has exactly one handler - so using the "command name Handler" convention makes sense - each event could have multiple handlers.
But the inside of a handler looks the same: implement MessageHandlerInterface
and then create our beloved public function __invoke()
with the type-hint for the event class: ImagePostDeletedEvent $event
.
... lines 1 - 2 | |
namespace App\MessageHandler\Event; | |
use App\Message\Event\ImagePostDeletedEvent; | |
... line 6 | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class RemoveFileWhenImagePostDeleted implements MessageHandlerInterface | |
{ | |
... lines 11 - 17 | |
public function __invoke(ImagePostDeletedEvent $event) | |
{ | |
... line 20 | |
} | |
} |
Now... we'll do the work... and this will be identical to the handler we just deleted. Add a constructor with the one service we need to delete files: PhotoFileManager
. I'll initialize fields to create that property then, down below, finish things with $this->photoFileManager->deleteImage()
passing that $event->getFilename()
.
... lines 1 - 2 | |
namespace App\MessageHandler\Event; | |
use App\Message\Event\ImagePostDeletedEvent; | |
use App\Photo\PhotoFileManager; | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class RemoveFileWhenImagePostDeleted implements MessageHandlerInterface | |
{ | |
private $photoFileManager; | |
public function __construct(PhotoFileManager $photoFileManager) | |
{ | |
$this->photoFileManager = $photoFileManager; | |
} | |
public function __invoke(ImagePostDeletedEvent $event) | |
{ | |
$this->photoFileManager->deleteImage($event->getFilename()); | |
} | |
} |
I hope this was delightfully boring for you. We deleted a command and command handler... and replaced them with an event and an event handler that are... other than the name... identical!
Next, let's dispatch this new event... but to our event bus. Then, we'll tweak that bus a little bit to make sure it works perfectly.
I think, while having constructor argument "$filename" in command DeletePhotoFile is a great idea, having the same argument in the "ImagePostDeletedEvent" is not conceptually OK. From the name of it, the argument should represent an ImagePost entity (whether its ID or a whole entity altogether, as discussed in Chapters 7 and 8). So, then the event handler could retrieve necessary information, including the file path.
Hey Volodymyr T.!
Hmm, indeed - I agree! I was trying to pass a "little information as possible" in the message classes (which is good! Don't pass extra stuff!). But you're 100% correct that the name doesn't fit. If I'm truly only going to pass the $filename
to ImagePostDeletedEvent
, then probably it should have a different name. Or I should pass the id (as you suggested) even if I don't need it at the moment. Nice catch.
Cheers!
I'm dispatching an event to sync and async transports at the same time, how do I tell specific handlers to only process events from specific transports? I have a NewOrderEvent that then needs to be logged (sync), set a review reminder (sync), email alerts (async), sms alerts (async), combine offer pdf docs into a single pdf (async), create thumbnails from pdf pages (async).
Hey Al Bunch
Wow, that's a lot to do! Thank god Messenger is so awesome :)
I think the answer to your problem is in this video: https://symfonycasts.com/sc...
If it doesn't please let us know and we will help you out
Cheers!
// 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
}
}
Hi,
I understand the diff between Command and Event ... I have a question, which is, for you, the best way to get result from the bus, for example, in the controller ? Let You have to register a user, and to return in the json response, the Entity Just created. How do you get the result in the controller from the bus?