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 SubscribeEl último tipo de autobús del que oirás hablar es... ¡el autobús turístico de dos pisos! Es decir... ¡el autobús de la consulta! Para que lo sepas... aunque soy un fanático de saludar como un idiota en el nivel superior de un autobús turístico, no soy un gran fanático de los autobuses de consulta: creo que hacen que tu código sea un poco más complejo... para no obtener mucho beneficio. Dicho esto, quiero que al menos entiendas qué es y cómo encaja en la metodología del bus de mensajes.
En config/packages/messenger.yaml
tenemos command.bus
y event.bus
. Añadamos query.bus
. Mantendré las cosas sencillas y sólo pondré esto en ~
para obtener la configuración por defecto.
framework: | |
messenger: | |
... lines 3 - 4 | |
buses: | |
... lines 6 - 14 | |
query.bus: ~ | |
... lines 16 - 39 |
Vale: ¿para qué sirve un "bus de consulta"? Entendemos el propósito de los comandos: enviamos mensajes que suenan como comandos: AddPonkaToImage
oDeleteImagePost
. Cada comando tiene entonces exactamente un manejador que realiza ese trabajo... pero no devuelve nada. En realidad, aún no lo he mencionado: los comandos sólo realizan un trabajo, pero no comunican nada de vuelta. Por ello, no hay problema en procesar los comandos de forma sincrónica o asincrónica: nuestro código no espera recibir información de vuelta del manejador.
Un bus de consulta es lo contrario. En lugar de ordenar al bus que haga su trabajo, el objetivo de una consulta es obtener información del manipulador. Por ejemplo, supongamos que, en nuestra página web, queremos imprimir el número de fotos que se han subido. Esta es una pregunta o consulta que queremos hacer a nuestro sistema
¿Cuántas fotos hay en la base de datos?
Si utilizas el patrón del bus de consulta, en lugar de obtener esa información directamente, enviarás una consulta.
Dentro del directorio Message/
, crea un nuevo subdirectorio Query/
. Y dentro de él, crea una nueva clase PHP llamada GetTotalImageCount
.
Incluso ese nombre parece una consulta en lugar de un comando: Quiero obtener el número total de imágenes. Y... en este caso, podemos dejar la clase de consulta en blanco: no necesitaremos pasar ningún dato extra al manejador.
namespace App\Message\Query; | |
class GetTotalImageCount | |
{ | |
} |
A continuación, dentro de MessageHandler/
, haz lo mismo: añade un subdirectorio Query/
y luego una nueva clase llamada GetTotalImageCountHandler
. Y, como con todo lo demás, haz que ésta implemente MessageHandlerInterface
y creapublic function __invoke()
con un argumento de tipo-indicado con la clase de mensaje:GetTotalImageCount $getTotalImageCount
.
namespace App\MessageHandler\Query; | |
use App\Message\Query\GetTotalImageCount; | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class GetTotalImageCountHandler implements MessageHandlerInterface | |
{ | |
public function __invoke(GetTotalImageCount $getTotalImageCount) | |
{ | |
... line 12 | |
} | |
} |
¿Qué hacemos aquí dentro? ¡Encontrar el recuento de imágenes! Probablemente inyectando elImagePostRepository
, ejecutando una consulta y devolviendo ese valor. Dejaré la parte de la consulta para ti y sólo return 50
.
namespace App\MessageHandler\Query; | |
use App\Message\Query\GetTotalImageCount; | |
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | |
class GetTotalImageCountHandler implements MessageHandlerInterface | |
{ | |
public function __invoke(GetTotalImageCount $getTotalImageCount) | |
{ | |
return 50; | |
} | |
} |
Pero espera un segundo... ¡porque acabamos de hacer algo totalmente nuevo! ¡Estamos devolviendo un valor de nuestro manejador! Esto no es algo que hayamos hecho en ningún otro sitio. Los comandos funcionan pero no devuelven ningún valor. Una consulta no hace realmente ningún trabajo, su único objetivo es devolver un valor.
Antes de enviar la consulta, abre config/services.yaml
para que podamos hacer nuestro mismo truco de vincular cada manejador al bus correcto. Copia la sección Event\
, pégala, cambia Event
por Query
en ambos sitios... y luego establece el bus a query.bus
.
... lines 1 - 7 | |
services: | |
... lines 9 - 39 | |
App\MessageHandler\Query\: | |
resource: '../src/MessageHandler/Query' | |
autoconfigure: false | |
tags: [{ name: messenger.message_handler, bus: query.bus }] | |
... lines 44 - 49 |
¡Me encanta! Comprobemos nuestro trabajo ejecutando:
php bin/console debug:messenger
¡Sí! query.bus
tiene un manejador, event.bus
tiene un manejador y command.bus
tiene dos.
¡Hagamos esto! Abre src/Controller/MainController.php
. Para obtener el bus de consulta, necesitamos saber qué combinación de tipo-indicación y nombre de argumento debemos utilizar. Obtenemos esa información ejecutando:
php bin/console debug:autowiring mess
Podemos obtener el command.bus
principal utilizando la sugerencia de tipo MessageBusInterface
con cualquier nombre de argumento. Para obtener el bus de consulta, tenemos que utilizar esa sugerencia de tipo y nombrar el argumento: $queryBus
.
Hazlo: MessageBusInterface $queryBus
. Dentro de la función, di$envelope = $queryBus->dispatch(new GetTotalImageCount())
.
... lines 1 - 6 | |
use Symfony\Component\Messenger\MessageBusInterface; | |
... lines 8 - 9 | |
class MainController extends AbstractController | |
{ | |
... lines 12 - 14 | |
public function homepage(MessageBusInterface $queryBus) | |
{ | |
$envelope = $queryBus->dispatch(new GetTotalImageCount()); | |
... lines 18 - 19 | |
} | |
} |
No lo hemos utilizado demasiado, pero el método dispatch()
devuelve el objeto final Envelope, que tendrá una serie de sellos diferentes. Una de las propiedades de un bus de consultas es que cada consulta se gestionará siempre de forma sincrónica, ¿por qué? Sencillo: necesitamos la respuesta a nuestra consulta... ¡ahora mismo! Y, por tanto, nuestro manejador debe ejecutarse inmediatamente. En Messenger, no hay nada que imponga esto en un bus de consultas... es que nunca dirigiremos nuestras consultas a un transporte, por lo que siempre se manejarán ahora mismo.
De todos modos, una vez que se maneja un mensaje, Messenger añade automáticamente un sello llamadoHandledStamp
. Vamos a conseguirlo: $handled = $envelope->last()
conHandledStamp::class
. Añadiré algo de documentación inline encima de eso para decirle a mi editor que esto será una instancia de HandledStamp
.
... lines 1 - 7 | |
use Symfony\Component\Messenger\Stamp\HandledStamp; | |
... lines 9 - 10 | |
class MainController extends AbstractController | |
{ | |
... lines 13 - 15 | |
public function homepage(MessageBusInterface $queryBus) | |
{ | |
... lines 18 - 19 | |
/** @var HandledStamp $handled */ | |
$handled = $envelope->last(HandledStamp::class); | |
... lines 22 - 26 | |
} | |
} |
Entonces... ¿por qué conseguimos este sello? Bueno, necesitamos saber cuál era el valor de retorno de nuestro manejador. Y, convenientemente, Messenger lo almacena en este sello Consíguelo con $imageCount = $handled->getResult()
.
... lines 1 - 7 | |
use Symfony\Component\Messenger\Stamp\HandledStamp; | |
... lines 9 - 10 | |
class MainController extends AbstractController | |
{ | |
... lines 13 - 15 | |
public function homepage(MessageBusInterface $queryBus) | |
{ | |
... lines 18 - 19 | |
/** @var HandledStamp $handled */ | |
$handled = $envelope->last(HandledStamp::class); | |
$imageCount = $handled->getResult(); | |
... lines 23 - 26 | |
} | |
} |
Pasémoslo a la plantilla como una variable imageCount
...
... lines 1 - 7 | |
use Symfony\Component\Messenger\Stamp\HandledStamp; | |
... lines 9 - 10 | |
class MainController extends AbstractController | |
{ | |
... lines 13 - 15 | |
public function homepage(MessageBusInterface $queryBus) | |
{ | |
... lines 18 - 19 | |
/** @var HandledStamp $handled */ | |
$handled = $envelope->last(HandledStamp::class); | |
$imageCount = $handled->getResult(); | |
return $this->render('main/homepage.html.twig', [ | |
'imageCount' => $imageCount | |
]); | |
} | |
} |
y luego en la plantilla - templates/main/homepage.html.twig
- ya que todo nuestro frontend está construido en Vue.js, anulemos el bloque title
en la página y usémoslo allí: Ponka'd {{ imageCount }} Photos
.
... lines 1 - 2 | |
{% block title %}Ponka'd {{ imageCount }} Photos{% endblock %} | |
... lines 4 - 10 |
¡Vamos a comprobarlo! Muévete, actualiza y... ¡funciona! Tenemos las 50 fotos de Ponka... al menos según nuestra lógica codificada.
Así que... ¡es un bus de consulta! No es mi favorito porque no se nos garantiza qué tipo devuelve: el imageCount
podría ser realmente una cadena... o un objeto de cualquier clase. Como no estamos llamando a un método directo, los datos que obtenemos de vuelta parecen un poco confusos. Además, como las consultas tienen que gestionarse de forma sincrónica, no estás ahorrando ningún rendimiento al aprovechar un bus de consultas: es puramente un patrón de programación.
Pero mi opinión es totalmente subjetiva, y a mucha gente le encantan los buses de consulta. De hecho, hemos hablado sobre todo de las herramientas en sí: buses de comandos, eventos y consultas. Pero hay algunos patrones más profundos, como CQRS o event sourcing, que estas herramientas pueden desbloquear. Esto no es algo que utilicemos actualmente aquí en SymfonyCasts... pero si te interesa, puedes leer más sobre este tema - el blog de Matthias Noback es mi fuente favorita.
Ah, y antes de que se me olvide, si miras atrás en los documentos de Symfony... en la página principal de Messenger... hasta el final... hay un punto aquí sobre cómo obtener resultados de tu manejador. Muestra algunos atajos que puedes utilizar para obtener más fácilmente el valor del bus.
A continuación, vamos a hablar de los suscriptores de los manejadores de mensajes: una forma alternativa de configurar un manejador de mensajes que tiene algunas opciones adicionales.
Hey Adeoweb!
Hmm, interesting. On a high level, I think it *would* work... but you'd have a few challenges and problems. The biggest one is if your workers are busy... like maybe it takes 30 seconds to find the time to handle your message. That's... not too much fun. Even a quick response could take 1/2 a second, which in "web request" time is really long. So... it's tricky to get this to happen in practice, in large part because the philosophy of works is that the work is done "as quickly as possible"... but some delays are no huge problem, since nobody is waiting for the info.
You would also need to implement some extra logic in Messenger to make it work, but due to the above problems, you won't see that functionality. I'm sure *someone* has done this *somewhere* and gotten decent results, I just don't know an example. Maybe a microservice architecture might be the best example. Imagine you dispatch a message *synchronously*, but the handler actually sends an API request to some other service which (again, synchronously) does some work and sense back the answer. Then, once all of that finishes, in your controller, you can read the result. The end result is actually quite close to what you're proposing. You send the message async, but doe to the "sleep", in reality, everything happens sync anyways.
Let me know if that made any sense ;)
Cheers!
Well, my idea was running multiple tasks in parallel at the same time and then synchronizing the main thread with all those threads. For example in javascript you can create one (or more) promises so they all could be handled in the background at the same time, in parallel. Then, while all the promises are being handled, the main thread could do some other work (at the same time, in parallel to promises) and then you can wait for all the promises to finish. You can do similar things in C# using tasks - you can create multiple tasks, so they can run in parallel at the same time, and when you need to synchronize your main thread with these tasks, you can await them. So microservice approach is not really fitting here.
I've got your point in the first paragraph. To solve the problem, one could implement some extra logic that would spawn PHP message handler process for every message dispatched. That way we would have a true multithreading system in PHP just like in C#. Maybe I will test my idea when I will have more free time. I will post the results here in a reply.
Thank you!
Hey Adeoweb!
Ah, I understand better now! There is a new feature (that I have not looked into yet) in PHP 8.1 called Fibers - https://betterprogramming.p... - that might (or might not) be somewhat related to what you're saying :).
Anyways, let me know if you do some experimenting!
Cheers!
Hey!
Good question. The thing here is that this is not the last video of the course. Ryan is preparing more content but at the same time we are actively releasing the "ApiPlatform Security" tutorial. We just need some more time :)
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. Just watched this tutorial and an idea popped into my head. What if we dispatched query message from controller through a transport (to handle it asynchronously), then executed some logic in controller and then wait for a message to be handled? That way we could run some code in parallel to query handling. For example:
Would this work?