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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The 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.

<?php
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.

<?php
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.

<?php
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.

Leave a comment!

This tutorial is built with Symfony 4.3, but will work well on Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "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.1", // v1.4.4
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/serializer-pack": "^1.0", // v1.0.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": {
        "symfony/debug-pack": "^1.0", // v1.0.7
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/test-pack": "^1.0" // v1.0.6
    }
}