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!

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
    }
}