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 SubscribeDe vuelta a DeleteImagePostHandler
, tenemos que despachar nuestro nuevo mensajeImagePostDeletedEvent
. Anteriormente, creamos un segundo servicio de bus de mensajes. Ahora tenemos un bus que utilizamos como bus de comandos llamado messenger.bus.default
y otro llamado event.bus
. Gracias a esto, cuando ejecutamos
php bin/console debug:autowiring mess
ahora podemos autoconectar cualquiera de estos servicios. Si sólo utilizamos la sugerencia de tipo MessageBusInterface
, obtendremos el bus de comandos principal. Pero si utilizamos ese type-hint y nombramos el argumento $eventBus
nos dará el otro.
Dentro de DeleteImagePostHandler
, cambia el argumento a $eventBus
. No es necesario, pero también voy a cambiar el nombre de la propiedad a $eventBus
para mayor claridad. Ah, y las variables necesitan un $
en PHP. Perfecto
... 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 | |
} |
Dentro de __invoke()
, es realmente lo mismo que antes: $this->eventBus->dispatch()
con new ImagePostDeletedEvent()
pasando que $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)); | |
} | |
} |
¡Y ya está! El resultado final de todo este trabajo... fue hacer lo mismo que antes, pero con algún cambio de nombre para que coincida con el patrón del "bus de eventos". El manejador realiza su tarea principal -eliminar el registro de la base de datos- y luego envía un evento que dice
¡Se acaba de borrar un registro de imagen! Si a alguien le interesa... ¡haz algo!
De hecho, a diferencia de lo que ocurre con los comandos, cuando enviamos un evento... en realidad no nos importa si hay algún controlador para él. Podría haber cero, 5, 10... ¡no nos importa! No vamos a utilizar ningún valor de retorno de los manejadores y, a diferencia de los comandos, no vamos a esperar que ocurra nada específico. Simplemente gritarás al espacio:
¡Oye! ¡Se ha eliminado un ImagePost!
De todos modos, la última pieza que tenemos que arreglar para que esto sea realmente idéntico a lo de antes es, en config/packages/messenger.yaml
, debajo de routing
, dirigirApp\Message\Event\ImagePostDeletedEvent
al transporte async
.
framework: | |
messenger: | |
... lines 3 - 29 | |
routing: | |
... lines 31 - 32 | |
'App\Message\Event\ImagePostDeletedEvent': async |
¡Vamos a probarlo! Busca tu trabajador y reinícialo. Toda esta refactorización giraba en torno al borrado de imágenes, así que... borremos un par de cosas, volvamos a pasar y... ¡sí! ¡Funciona de maravilla! ImagePostDeletedEvent
se está despachando y manejando.
Ah, y una nota al margen sobre el enrutamiento. Cuando enrutas una clase de comando, sabes exactamente qué manejador tiene. Y así, es súper fácil pensar en lo que hace ese manejador y determinar si puede o no manejarse de forma asíncrona.
Con los eventos, es un poco más complicado: esta clase de evento podría tener múltiples manejadores. Y, en teoría, puedes querer que algunos se gestionen inmediatamente y otros más tarde. Como Messenger está construido en torno al enrutamiento de los mensajes a los transportes -no a los manejadores-, hacer que algunos manejadores estén sincronizados y otros asincrónicos no es natural. Sin embargo, si necesitas hacerlo, es posible: puedes encaminar un mensaje a varios transportes, y luego configurar Messenger para que sólo llame a un manejador cuando se reciba del transporte A y sólo al otro manejador cuando se reciba del transporte B. Es un poco más complejo, así que no recomiendo hacerlo a menos que lo necesites. No hablaremos de cómo en este tutorial, pero está en los documentos.
De todos modos, ya he mencionado que, para los eventos, es legal a nivel filosófico no tener manejadores... aunque probablemente no lo harás en tu aplicación porque... ¿qué sentido tiene enviar un evento sin manejadores? Pero... por probar, abre RemoveFileWhenImagePostDeleted
y quita la parte deimplements MessageHandleInterface
.
... lines 1 - 8 | |
class RemoveFileWhenImagePostDeleted | |
{ | |
... lines 11 - 21 | |
} |
Lo hago temporalmente para ver qué pasa si Symfony ve cero manejadores para un evento. Vamos a... ¡descubrirlo! De nuevo en el navegador, intenta eliminar una imagen. ¡Funciona! Espera... oh, me olvidé de detener el trabajador... hagámoslo... y vuelve a intentarlo. Esta vez... funciona... pero en el registro del trabajador... ¡Error crítico!
Se ha producido una excepción al manejar
ImagePostDeletedEvent
: no hay manejador para el mensaje.
Por defecto, Messenger exige que cada mensaje tenga al menos un manejador. Eso es para ayudarnos a evitar errores tontos. Pero... para un bus de eventos... sí queremos permitir cero manejadores. De nuevo... esto es más un problema filosófico que real: es poco probable que decidas enviar eventos que no tienen manejadores. Pero, ¡vamos a ver cómo solucionarlo!
En messenger.yaml
, quita el ~
de event.bus
y añade una nueva opción debajo:default_middleware: allow_no_handlers
. La opción default_middleware
está por defecto en true
y su propósito principal es permitirte establecerla en false
si, por alguna razón, quisieras eliminar por completo el middleware por defecto, el que maneja y envía los mensajes, entre otras cosas. Pero también puedes establecerlo enallow_no_handlers
si quieres mantener el middleware normal, pero indicando a HandleMessageMiddleware
que no debe entrar en pánico si hay cero manejadores.
framework: | |
messenger: | |
... lines 3 - 4 | |
buses: | |
... lines 6 - 9 | |
event.bus: | |
default_middleware: allow_no_handlers | |
... lines 12 - 35 |
Vuelve y reinicia el trabajador. Luego, borra otra imagen... vuelve aquí y... ¡guay! Dice "No hay manejador para el mensaje" pero no se asusta y provoca un fallo.
Así que ahora nuestro bus de comandos y nuestro bus de eventos tienen una pequeña diferencia... aunque siguen siendo casi idénticos... y realmente podríamos seguir enviando tanto comandos como eventos a través del mismo bus. Vuelve a poner el MessageHandlerInterface
en la clase... y reinicia nuestro trabajador una vez más.
... lines 1 - 8 | |
class RemoveFileWhenImagePostDeleted implements MessageHandlerInterface | |
{ | |
... lines 11 - 21 | |
} |
Ahora que nos sentimos bien con los eventos... Tengo una pregunta: ¿cuál es la diferencia entre enviar un evento a Messenger y enviar un evento al EventDispatcher de Symfony?
Vamos a hablar de eso a continuación.
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