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 SubscribeEntonces... ¿qué diablos es un evento? Deja que te ponga un ejemplo. Supón que un usuario se registra en tu sitio web. Cuando eso ocurre, haces tres cosas: guardar al usuario en la base de datos, enviarle un correo electrónico y añadirlo a un sistema de CRM. El código para hacer todo esto podría vivir en un controlador, un servicio o un SaveRegisteredUserHandler
si tuvieras un comando SaveRegisteredUser
.
Esto significa que tu servicio -o quizás tu controlador de comandos- está haciendo tres cosas distintas. Eso... no es un gran problema. Pero si de repente necesitas hacer una cuarta cosa, tendrás que añadir aún más código. Tu servicio -o manejador- viola el principio de responsabilidad única que dice que cada función sólo debe realizar una única tarea.
Esto no es el fin del mundo: a menudo escribo código así... y no suele molestarme. Pero este problema de organización del código es exactamente la razón por la que existen los eventos.
La idea es la siguiente: si tienes un manejador de comandos como SaveRegisteredUser
, se supone que sólo debe realizar su tarea principal: debe guardar el usuario registrado en la base de datos. Si sigues esta práctica, no debería realizar tareas "secundarias", como enviar un correo electrónico al usuario o configurarlo en un sistema CRM. En su lugar, debe realizar la tarea principal y luego enviar un evento, como UserWasRegistered
. Entonces, tendríamos dos manejadores para ese evento: uno que envía el correo electrónico y otro que configura el usuario en el CRM. El manejador de comandos realiza la "acción" principal y el evento ayuda a otras partes del sistema a "reaccionar" a esa acción.
En lo que respecta a Messenger, los comandos y los eventos son idénticos. La diferencia se reduce a que cada uno soporta un patrón de diseño diferente.
Y... ¡ya tenemos una situación así! Mira DeleteImagePost
y luegoDeleteImagePostHandler
. La tarea "principal" de este manejador es eliminar esteImagePost
de la base de datos. Pero también tiene una segunda tarea: eliminar el archivo subyacente del sistema de archivos.
Para ello, enviamos un segundo comando - DeletePhotoFile
- y su manejador elimina el archivo. Adivina qué... ¡este es el patrón de eventos! Bueno, es casi el patrón de eventos. La única diferencia es la denominación: DeletePhotoFile
suena como un "comando". En lugar de "ordenar" al sistema que haga algo, un evento es más bien un "anuncio" de que algo ha ocurrido.
Para entenderlo bien, retrocedamos y volvamos a implementar todo esto de nuevo. Comenta la llamada a $messageBus->dispatch()
y luego elimina la declaración de uso de DeletePhotoFile
en la parte superior
... lines 1 - 10 | |
class DeleteImagePostHandler implements MessageHandlerInterface | |
{ | |
... lines 13 - 21 | |
public function __invoke(DeleteImagePost $deleteImagePost) | |
{ | |
... lines 24 - 29 | |
//$this->messageBus->dispatch(new DeletePhotoFile($filename)); | |
} | |
} |
A continuación, para tener un comienzo limpio: elimina la propia clase de comando DeletePhotoFile
y DeletePhotoFileHandler
. Por último, en config/packages/messenger.yaml
, dirigimos el comando que acabamos de eliminar. Comenta eso.
framework: | |
messenger: | |
... lines 3 - 29 | |
routing: | |
... lines 31 - 32 | |
#'App\Message\DeletePhotoFile': async |
Veamos esto con ojos nuevos. Hemos conseguido que DeleteImagePostHandler
realice únicamente su tarea principal: borrar el ImagePost
. Y ahora nos preguntamos: ¿dónde debo poner el código para realizar la tarea secundaria de borrar el archivo físico? Podríamos poner esa lógica aquí mismo, o aprovechar un evento.
Los comandos, los eventos y sus manejadores son idénticos. En el directorio src/Message
, para empezar a organizar las cosas un poco mejor, vamos a crear un subdirectorio Event/
. Dentro, añade una nueva clase: ImagePostDeletedEvent
.
namespace App\Message\Event; | |
class ImagePostDeletedEvent | |
{ | |
... lines 7 - 17 | |
} |
Fíjate en el nombre de esta clase: es fundamental. Hasta ahora todo ha sonado como una orden: estamos recorriendo nuestra base de código gritando: ¡ AddPonkaToImage
! y ¡ DeleteImagePost
! Parecemos mandones.
Pero con los eventos, no estás utilizando un comando estricto, sino que estás notificando al sistema algo que acaba de ocurrir: vamos a eliminar completamente el puesto de la imagen y luego diremos:
¡Oye! ¡Acabo de borrar un post de imagen! Si te interesa... eh... ahora es tu oportunidad de... eh... ¡hacer algo! Pero no me importa si lo haces o no.
El evento en sí podría ser manejado por... nadie... o podría tener múltiples manejadores. Dentro de la clase, almacenaremos los datos que creamos que pueden ser útiles. Añade un constructor con un string $filename
- saber el nombre de archivo delImagePost
eliminado podría ser útil. Pulsaré Alt + Enter e iré a "Inicializar campos" para crear esa propiedad y establecerla. Luego, en la parte inferior, iré a "Código -> Generar" -o Comando + N en un Mac- y seleccionaré "Obtenedores" para generar este único getter.
namespace App\Message\Event; | |
class ImagePostDeletedEvent | |
{ | |
private $filename; | |
public function __construct(string $filename) | |
{ | |
$this->filename = $filename; | |
} | |
public function getFilename(): string | |
{ | |
return $this->filename; | |
} | |
} |
Te habrás dado cuenta de que, aparte de su nombre, esta clase "evento" es exactamente igual que el comando que acabamos de eliminar
La creación de un "manejador" de eventos también es idéntica a la de los manejadores de comandos. En el directorio MessageHandler
, vamos a crear otro subdirectorio llamadoEvent/
para organizarnos. A continuación, añade una nueva clase PHP. LlamémoslaRemoveFileWhenImagePostDeleted
. Pero asegúrate de que lo escribes correctamente.
namespace App\MessageHandler\Event; | |
... lines 5 - 6 | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class RemoveFileWhenImagePostDeleted implements MessageHandlerInterface | |
{ | |
... lines 11 - 21 | |
} |
Esto también sigue una convención de nomenclatura diferente. En el caso de los comandos, si un comando se llamaba AddPonkaToImage
, llamábamos al manejador AddPonkaToImageHandler
. La gran diferencia entre los comandos y los eventos es que, mientras que cada comando tiene exactamente un manejador -por lo que usar la convención "nombre del comando Manejador" tiene sentido-, cada evento puede tener varios manejadores.
Pero el interior de un manejador es el mismo: implementa MessageHandlerInterface
y luego crea nuestro querido public function __invoke()
con el tipo-indicación de la clase de evento: 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 | |
} | |
} |
Ahora... haremos el trabajo... y esto será idéntico al manejador que acabamos de eliminar. Añade un constructor con el único servicio que necesitamos para eliminar archivos:PhotoFileManager
. Inicializaré los campos para crear esa propiedad y luego, más abajo, terminaré las cosas con $this->photoFileManager->deleteImage()
pasando ese$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()); | |
} | |
} |
Espero que esto te haya resultado deliciosamente aburrido. Hemos eliminado un comando y un controlador de comandos... y los hemos sustituido por un evento y un controlador de eventos que son... aparte del nombre... ¡idénticos!
A continuación, vamos a enviar este nuevo evento... pero a nuestro bus de eventos. Luego, ajustaremos un poco ese bus para asegurarnos de que funciona perfectamente.
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?