Query Bus
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 SubscribeThe last type of bus that you'll hear about is... the double-decker tourist bus! I mean... the query bus! Full disclosure... while I am a fan of waving like an idiot on the top-level of a tourist bus, I'm not a huge fan of query buses: I think they make your code a bit more complex... for not much benefit. That being said, I want you to at least understand what it is and how it fits into the message bus methodology.
Creating the Query Bus
In config/packages/messenger.yaml
we have command.bus
and event.bus
. Let's add query.bus
. I'll keep things simple and just set this to ~
to get the default settings.
framework: | |
messenger: | |
// ... lines 3 - 4 | |
buses: | |
// ... lines 6 - 14 | |
query.bus: ~ | |
// ... lines 16 - 39 |
What is a Query?
Ok: so what is the point of a "query bus"? We understand the purpose of commands: we dispatch messages that sound like commands: AddPonkaToImage
or DeleteImagePost
. Each command then has exactly one handler that performs that work... but doesn't return anything. I haven't really mentioned that yet: commands just do work, but they don't communicate anything back. Because of this, it's ok to process commands synchronously or asynchronously - our code isn't waiting to get information back from the handler.
A query bus is the opposite. Instead of commanding the bus to do work, the point of a query is to get information back from the handler. For example, let's pretend that, on our homepage, we want to print the number of photos that have been uploaded. This is a question or query that we want to ask our system:
How many photos are in the database?
If you're using the query bus pattern, instead of getting that info directly, you'll dispatch a query.
Creating the Query & Handler
Inside the Message/
directory, create a new Query/
subdirectory. And inside of that, create a new PHP class called GetTotalImageCount
.
Even that name sounds like a query instead of a command: I want to get the total image count. And... in this case, we can leave the query class blank: we won't need to pass any extra data to the handler.
namespace App\Message\Query; | |
class GetTotalImageCount | |
{ | |
} |
Next, inside of MessageHandler/
, do the same thing: add a Query/
subdirectory and then a new class called GetTotalImageCountHandler
. And like with everything else, make this implement MessageHandlerInterface
and create public function __invoke()
with an argument type-hinted with the message class: 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 | |
} | |
} |
What do we do inside of here? Find the image count! Probably by injecting the ImagePostRepository
, executing a query and then returning that value. I'll leave the querying part to you and just 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; | |
} | |
} |
But hold on a second... cause we just did something totally new! We're returning a value from our handler! This is not something that we've done anywhere else. Commands do work but don't return any value. A query doesn't really do any work, its only point is to return a value.
Before we dispatch the query, open up config/services.yaml
so we can do our same trick of binding each handler to the correct bus. Copy the Event\
section, paste, change Event
to Query
in both places... then set the bus to 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 |
Love it! Let's check our work by running:
php bin/console debug:messenger
Yep! query.bus
has one handler, event.bus
has one handler and command.bus
has two.
Dispatching the Message
Let's do this! Open up src/Controller/MainController.php
. This renders the homepage and so this is where we need to know how many photos have been uploaded. To get the query bus, we need to know which type-hint & argument name combination to use. We get that info from running:
php bin/console debug:autowiring mess
We can get the main command.bus
by using the MessageBusInterface
type-hint with any argument name. To get the query bus, we need to use that type-hint and name the argument: $queryBus
.
Do that: MessageBusInterface $queryBus
. Inside the function, say $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 | |
} | |
} |
Fetching the Returned Value
We haven't used it too much, but the dispatch()
method returns the final Envelope object, which will have a number of different stamps on it. One of the properties of a query bus is that every query will always be handled synchronously. Why? Simple: we need the answer to our query... right now! And so, our handler must be run immediately. In Messenger, there's nothing that enforces this on a query bus... it's just that we won't ever route our queries to a transport, so they'll always be handled right now.
Anyways, once a message is handled, Messenger automatically adds a stamp called HandledStamp
. Let's get that: $handled = $envelope->last()
with HandledStamp::class
. I'll add some inline documentation above that to tell my editor that this will be a HandledStamp
instance.
// ... 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 | |
} | |
} |
So... why did we get this stamp? Well, we need to know what the return value of our handler was. And, conveniently, Messenger stores that on this stamp! Get it with $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 | |
} | |
} |
Let's pass that into the template as an imageCount
variable....
// ... 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 | |
]); | |
} | |
} |
and then in the template - templates/main/homepage.html.twig
- because our entire frontend is built in Vue.js, let's override the title
block on the page and use it there: Ponka'd {{ imageCount }} Photos
.
// ... lines 1 - 2 | |
{% block title %}Ponka'd {{ imageCount }} Photos{% endblock %} | |
// ... lines 4 - 10 |
Let's check it out! Move over, refresh and... it works! We've Ponka's 50 photos... at least according to our hardcoded logic.
So... that's a query bus! It's not my favorite because we're not guaranteed what type it returns - the imageCount
could really be a string... or an object of any class. Because we're not calling a direct method, the data we get back feels a little fuzzy. Plus, because queries need to be handled synchronously, you're not saving any performance by leveraging a query bus: it's purely a programming pattern.
But, my opinion is totally subjective, and a lot of people love query buses. In fact, we've been talking mostly about the tools themselves: command, event & query buses. But there are some deeper patterns like CQRS or event sourcing that these tools can unlock. This is not something we currently use here on SymfonyCasts... but if you're interested, you can read more about this topic - Matthias Noback's blog is my favorite source.
Oh, and before I forget, if you look back on the Symfony docs... back on the main messenger page... all the way at the bottom... there's a spot here about getting results from your handler. It shows some shortcuts that you can use to more easily get the value from the bus.
Next, let's talk about message handler subscribers: an alternate way to configure a message handler that has a few extra options.
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?