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 SubscribeLa eliminación de una imagen se sigue haciendo de forma sincrónica. Puedes verlo: como lo he hecho extra lento para conseguir un efecto dramático, tarda un par de segundos en procesarse antes de desaparecer. Por supuesto, podríamos evitarlo haciendo que nuestro JavaScript elimine la imagen visualmente antes de que termine la llamada AJAX. Pero hacer que las cosas pesadas sean asíncronas es una buena práctica y podría permitirnos poner menos carga en el servidor web.
Veamos el estado actual de las cosas: hemos actualizado todo esto para que lo maneje nuestro bus de comandos: tenemos un comando DeleteImagePost
yDeleteImagePostHandler
. Pero dentro de config/packages/messenger.yaml
, no estamos enrutando esta clase a ninguna parte, lo que significa que se está manejando inmediatamente.
Ah, y fíjate: seguimos pasando todo el objeto entidad al mensaje. En los dos últimos capítulos, hablamos de evitar esto como mejor práctica y porque puede causar cosas raras si manejas esto de forma asíncrona.
Pero... si piensas mantener DeleteImagePost
de forma sincrónica... depende de ti: pasar el objeto entidad completo no perjudica nada. Y... realmente... ¡necesitamos que este mensaje se gestione de forma sincrónica! Necesitamos que el ImagePost
se borre de la base de datos inmediatamente para que, si el usuario actualiza, la imagen desaparezca.
Pero, fíjate bien: el borrado implica dos pasos: eliminar una fila en la base de datos y eliminar el archivo de imagen subyacente. Y... sólo es necesario que se produzca el primer paso en este momento. Si eliminamos el archivo en el sistema de archivos más tarde... ¡no pasa nada!
Para hacer parte del trabajo de forma sincronizada y la otra parte de forma asincrónica, mi enfoque preferido es dividir esto en dos comandos.
Crea una nueva clase de comando llamada DeletePhotoFile
. Dentro, añade un constructor para que podamos pasar la información que necesitemos. Esta clase de comando se utilizará para eliminar físicamente el archivo del sistema de archivos. Y si te fijas en el manejador, para hacerlo, sólo necesitamos el servicio PhotoFileManager
y la cadena nombre de archivo.
Así que esta vez, la menor cantidad de información que podemos poner en la clase de comando esstring $filename
... lines 1 - 2 | |
namespace App\Message; | |
class DeletePhotoFile | |
{ | |
... lines 7 - 8 | |
public function __construct(string $filename) | |
{ | |
... line 11 | |
} | |
... lines 13 - 17 | |
} |
Pulsaré Alt + intro e iré a "Inicializar campos" para crear esa propiedad y establecerla
... lines 1 - 2 | |
namespace App\Message; | |
class DeletePhotoFile | |
{ | |
private $filename; | |
public function __construct(string $filename) | |
{ | |
$this->filename = $filename; | |
} | |
... lines 13 - 17 | |
} |
Ahora iré a Código -> Generar -o Cmd+N en un Mac- para generar el getter.
... lines 1 - 2 | |
namespace App\Message; | |
class DeletePhotoFile | |
{ | |
private $filename; | |
public function __construct(string $filename) | |
{ | |
$this->filename = $filename; | |
} | |
public function getFilename(): string | |
{ | |
return $this->filename; | |
} | |
} |
¡Genial! Paso 2: añade el manejador DeletePhotoFileHandler
. Haz que éste siga las dos reglas de los manejadores: implementa MessageHandlerInterface
y crea un método __invoke()
con un argumento que sea de tipo con la clase de mensaje:DeletePhotoFile $deletePhotoFile
.
... lines 1 - 2 | |
namespace App\MessageHandler; | |
use App\Message\DeletePhotoFile; | |
... line 6 | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class DeletePhotoFileHandler implements MessageHandlerInterface | |
{ | |
... lines 11 - 17 | |
public function __invoke(DeletePhotoFile $deletePhotoFile) | |
{ | |
... line 20 | |
} | |
} |
Perfecto Lo único que tenemos que hacer aquí es... esta línea:$this->photoManager->deleteImage()
. Cópiala y pégala en nuestro manejador. Para el argumento, podemos utilizar nuestra clase de mensaje: $deletePhotoFile->getFilename()
.
... lines 1 - 2 | |
namespace App\MessageHandler; | |
use App\Message\DeletePhotoFile; | |
... line 6 | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class DeletePhotoFileHandler implements MessageHandlerInterface | |
{ | |
... lines 11 - 17 | |
public function __invoke(DeletePhotoFile $deletePhotoFile) | |
{ | |
$this->photoManager->deleteImage($deletePhotoFile->getFilename()); | |
} | |
} |
Y por último, necesitamos el servicio PhotoFileManager
: añade un constructor con un argumento: PhotoFileManager $photoManager
. Utilizaré mi truco de Alt+Enter -> Inicializar campos para crear esa propiedad como siempre.
... lines 1 - 2 | |
namespace App\MessageHandler; | |
use App\Message\DeletePhotoFile; | |
use App\Photo\PhotoFileManager; | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class DeletePhotoFileHandler implements MessageHandlerInterface | |
{ | |
private $photoManager; | |
public function __construct(PhotoFileManager $photoManager) | |
{ | |
$this->photoManager = $photoManager; | |
} | |
public function __invoke(DeletePhotoFile $deletePhotoFile) | |
{ | |
$this->photoManager->deleteImage($deletePhotoFile->getFilename()); | |
} | |
} |
Ya está Ahora tenemos una clase de comando funcional que requiere la cadena nombre de archivo, y un manejador que lee ese nombre de archivo y... ¡hace el trabajo!
Todo lo que tenemos que hacer ahora es despachar el nuevo comando. Y... técnicamente podríamos hacerlo en dos lugares diferentes. En primer lugar, podrías pensar que, enImagePostController
, podríamos enviar dos comandos diferentes aquí mismo.
Pero... Eso no me gusta. El controlador ya está diciendo DeleteImagePost
. No debería necesitar emitir ningún otro comando. Si decidimos dividir esa lógica en trozos más pequeños, eso depende del controlador. En otras palabras, vamos a despachar este nuevo comando desde el manejador de comandos. ¡Inicio!
En lugar de llamar directamente a $this->photoManager->deleteImage()
, cambia la sugerencia de tipo de ese argumento a autowire MessageBusInterface $messageBus
. Actualiza el código en el constructor... y el nombre de la propiedad.
... lines 1 - 9 | |
use Symfony\Component\Messenger\MessageBusInterface; | |
... line 11 | |
class DeleteImagePostHandler implements MessageHandlerInterface | |
{ | |
private $messageBus; | |
... lines 15 - 16 | |
public function __construct(MessageBusInterface $messageBus, EntityManagerInterface $entityManager) | |
{ | |
$this->messageBus = $messageBus; | |
$this->entityManager = $entityManager; | |
} | |
... lines 22 - 32 | |
} |
Ahora, fácil: elimina el código antiguo y empieza con:$filename = $imagePost->getFilename()
. Luego, eliminémoslo de la base de datos y, al final, $this->messageBus->dispatch(new DeletePhotoFile($filename))
.
... lines 1 - 5 | |
use App\Message\DeletePhotoFile; | |
... lines 7 - 11 | |
class DeleteImagePostHandler implements MessageHandlerInterface | |
{ | |
... lines 14 - 22 | |
public function __invoke(DeleteImagePost $deleteImagePost) | |
{ | |
$imagePost = $deleteImagePost->getImagePost(); | |
$filename = $imagePost->getFilename(); | |
$this->entityManager->remove($imagePost); | |
$this->entityManager->flush(); | |
$this->messageBus->dispatch(new DeletePhotoFile($filename)); | |
} | |
} |
Y... esto debería... funcionar: todo se sigue gestionando de forma sincrónica.
Probemos a continuación, pensemos un poco en lo que ocurre si falla parte de un manejador, y hagamos que la mitad del proceso de borrado sea asíncrono.
Hey Kris,
That's unexpected, the DelayStamp should just work (and actually, it does work for me). How do you start the worker? I'm wondering if you use any special config for it
Also, remember that the delay time it's in milliseconds
Cheers!
Hi Ryan, I have a question. from what i understand the idea of taking the load off the web server would be to make the service asynchronous so that the response is returned as quickly as possible to the client and finish the request faster, with that the (same server) can process the rest of the work as delete
the photo file without this process being linked to a web request, am I right? I believed that we would have to separate our code on different servers to perform each step as if they were microservices, but it was already clear to me how things should work.
Hey Renato,
Yes, you're right. We don't need out users to wait for the actual photo deleted, we can say that it's deleted by kind of "scheduled" it to be deleted in the system. Then later, the messenger will handle that scheduled delete request eventually deleting the file, but it will be a background process, and we don't care how long it will last, the user will get the response almost immediately. Moreover, if something when wrong - we could retry the deleting (or any other process we scheduled).
Also, it's important to understand that with this strategy you don't care about how many simultaneous requests you get the same second because all scheduled deletions will be performed one by one by the Messenger's worker, i.e. no any pick highloads on the server. Without the Messenger, if we know that the process (like deleting a photo from the server) is resource consuming - you may down the whole website when you suddently got a high pick of users requesting file deletions.
So, Messenger clearly has some benefits handling time or resource consuming operations.
And yes, you can have a separate server that will handle all those tasks in the queue if you need, but as I said, thanks to the fact that messenger's worker will handle each queue one by one you can safely do this on the same server as well without causing server overloading. But yes, it depends on your specific case, on how time/resource consuming your operations are, and how many queues you have.
I hope it's clearer to you now.
Cheers!
Great tutorial and followed it step by step using Symfony 6. The "versions" tab explicitly says it will work upto Symfony 5. I'm facing problems with Symfony 6 when changing it to async. Synchronously it works perfectly (dispatch to 1, then dispatch to 2 and on success dispaches to 3 and 4), but in async only the first message is handled succesfully. The message of the 2nd dispatch does not even endup on the queue (Doctrine). Doesn't this work on Symfony 6 ? Should I change to a workflow setup?
// 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
}
}
Is there any reason why DelayStamp for a message dispatched in the handler doesn't work for me.
When I dispatch Message from the controller DelayStamp works.