Autobús turístico
Keep on Learning!
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.
Creación del bus de consultas
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 |
¿Qué es una consulta?
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.
Crear la consulta y el manejador
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.
Despachar el mensaje
¡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 | |
} | |
} |
Obtención del valor devuelto
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.
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?