Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

6
Login or Register to join the conversation

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:


$envelope = $queryBus->dispatch(new GetTotalImageCount());

// Insert some important logic here //

$handled = $envelope->last(HandledStamp::class);
while(!$handled instanceof HandledStamp::class) {
    sleep 1;
}
$imageCount = $handled->getResult();

Would this work?

Reply

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!

Reply

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!

Reply

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!

Reply
Shiraats Avatar
Shiraats Avatar Shiraats | posted 3 years ago

HI.
Any reason why this video has not been released yet

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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": "*",
        "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
    }
}
userVoice