Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Middleware

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

Internally, when you dispatch a message onto the bus... what happens? What does the code look like inside the bus? The answer is... there basically is no code inside the bus! Everything is done via middleware.

Middleware Basics

The bus is nothing more than a collection of "middleware". And each middleware is just a function that receives the message and can do something with it.

The process looks like this. We pass a message to the dispatch() method, then the bus passes that to the first middleware. The middleware then runs some code and eventually calls the second middleware. It runs some code and eventually calls the third middleware... until finally the last middleware - let's say it's the fourth middleware - has no one else to call. At that moment, the fourth middleware function finishes, then the third middleware function finishes, then the second, then the first. Thanks to this design, each middleware can run code before calling the next middleware or after.

This "middleware" concept isn't unique to Messenger or even PHP - it's a pattern. It can be both super useful... and a bit confusing... as it's a big circle. The point is this: with Messenger, if you want to hook into the dispatch process - like to log what's happening - you'll do that with a middleware. Heck, even the core functionality of messenger - executing handlers and sending messages to transports - is done with middleware! Those are called HandleMessageMiddleware and SendMessageMiddleware if you want to geek out and see how they work.

So here's our goal: each time we dispatch a message... from anywhere, I want to attach a unique id to that message and then use that to log what's happening over time to the message: when it's initially dispatched, when it's sent to the transport, and when it's received from the transport and handled. Heck, you could even use this to track how long an individual message took before it was processed or how many times it was retried.

Creating a Middleware

Creating a middleware is actually fairly simple. Create a new directory inside src/ called Messenger/... though... like with pretty much everything in Symfony, this directory could be called anything. Inside, add a class called, how about, AuditMiddleware.

... lines 1 - 2
namespace App\Messenger;
... lines 4 - 5
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
... lines 7 - 8
class AuditMiddleware implements MiddlewareInterface
{
... lines 11 - 14
}

The only rule for middleware is that they must implement - surprise! - MiddlewareInterface. I'll go to "Code -> Generate" - or Command+N on a Mac - and select "Implement Methods". This interface requires just one: handle(). We'll talk about the "stack" thing in a second... but mostly... the signature of this method makes sense: we receive the Envelope and return an Envelope.

... lines 1 - 2
namespace App\Messenger;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
class AuditMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
// TODO
}
}

The one line that your middleware will almost definitely need is this: return $stack->next()->handle($envelope, $stack).

... lines 1 - 7
class AuditMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
... line 13
return $stack->next()->handle($envelope, $stack);
}
}

This is the line that basically says:

I want to execute the next middleware and then return its value.

Without this line, any middleware after us would never be called... which isn't usually what you want.

Registering the Middleware

And... to start... that's enough: this class is already a functional middleware! But, unlike a lot of stuff in Symfony, Messenger won't find and start using this middleware automatically. Find your open terminal and, once again, run:

php bin/console debug:config framework messenger

Let's see... somewhere in here is a key called buses. This defines all of the message bus services you have in your system. Right now, we have one: the default bus called messenger.bus.default. That name could be anything and becomes the service id. Below this, we can use the middleware key to define whatever new middleware we want to add, in addition to the core ones that are added by default.

Let's copy that config. Then, open config/packages/messenger.yaml and, under framework:, messenger:, paste this right on top... and make sure it's indented correctly. Below, add middleware: a new line, then our new middleware service: App\Messenger\AuditMiddleware.

framework:
messenger:
buses:
messenger.bus.default:
middleware:
- App\Messenger\AuditMiddleware
... lines 7 - 25

Order of Middleware

And just like that, our middleware should be called... along with all the core middleware. What... um... are the core middleware? And what order is everything called in? Well, there's not a great way to see that yet, but you can find this information by running:

php bin/console debug:container --show-arguments messenger.bus.default.inner

... which is a super low-level way to get information about the message bus. Anyways, there are a few core middleware at the start that get some basic things set up, then our middleware, and finally, SendMessageMiddleware and HandleMessageMiddleware are called at the end. Knowing the exact order of this stuff isn't that important - but hopefully it'll help demystify things as we keep going.

Next, let's get to work by using our middleware to attach a unique id to each message. How? Via our very own stamp!

Leave a comment!

4
Login or Register to join the conversation
Default user avatar
Default user avatar John Esteban | posted 1 year ago

Hi! I'm working with the Symfony framework, and I'm using Messenger. I want to know if there is a way to ignore some messages when I'm using middlewares without finish with a error or exception or exit. For example, if I have already consumed a particular message and I want to ignore it.

Reply

Hi John Esteban!

Hmm. I've never done this before, but yes, I think this is definitely doable. One of the things you need to have in any middleware is this line:


return $stack->next()->handle($envelope, $stack);

Example: https://github.com/symfony/symfony/blob/8e8207bb72d7f2cb8be355994ad2fcfa97c00f74/src/Symfony/Component/Messenger/Middleware/AddBusNameStampMiddleware.php#L37

This is literally what calls the NEXT item in the middleware. If you changed this, instead to:


return $envelope.

Then I'm pretty sure that the rest of the middleware change would not execute. So as long as your middleware is put before the "handle message" middleware (and this will happen automatically), then this should do the trick.

Let me know if you're successful.

Cheers!

Reply
Default user avatar
Default user avatar Jeff Groves | posted 3 years ago

I'm trying to figure out how to using the Symfony Messenger component in a non-Symfony application. I've got it working fine in Symfony to send AMQP messages to RabbitMQ, but I need to send AMQP messages from a legacy PHP application. Basically, I need to figure out how to configure my own SendMessageMiddleware to work the same way as Symfony. The current documentation at https://symfony.com/doc/cur... is not very helpful. All it says is "...with Symfony’s FrameworkBundle, the following middleware are configured for you: SendMessageMiddleware...".

I need an example of how to do it myself without the Symfony magic.

Reply

Hey Jeff Groves !

I'm trying to figure out how to using the Symfony Messenger component in a non-Symfony application

Ah yes :). This is (as you correctly noticed) not the "first class citizen use case" in the docs - it's tricky, we have to write the docs primarily for the main audience. That, indeed, makes the "standalone component" usage trickier!

So let's talk about SendMessageMiddleware. This class itself is quite simple - the tricky part is the SendersLocatorInterface constructor argument, which normally Symfony handles creating for you. This is the concrete class that Symfony uses: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php

But, let's stop right there :). The full Symfony Framework Messenger is super feature rich and complex. And you may not need to hook up the same level of complexity and features in your legacy app. The simplest thing to do would be to create your own implementation of SendersLocatorInterface - it's a beautifully simple interface: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Messenger/Transport/Sender/SendersLocatorInterface.php

Inside, getSenders(), you could call $envelop->getMessage() and, if you want, just have a big map of which message class should go to which "SenderInterface". Or... maybe you only have one Sender/Transport. In that case, you could literally just return it:


public function getSenders(Envelope $envelope): iterable
{
    return [$this->myOneSender];
}

Let me know if that makes sense - or if there is something else tripping you up in this process :).

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