Tracking Messages with Middleware & a Stamp

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

We somehow want to attach a unique id - just some string - that stays with the message forever: whether it's handled immediately, sent to a transport, or even retried multiple times.

Creating a Stamp

How can we attach extra... "stuff" to a message? By giving it our very-own stamp! In the Messenger/ directory, create a new PHP class called UniqueIdStamp. Stamps also have just one rule: they implement MessengerEnvelopeMetadataAwareContainerReaderInterface. Nah I'm kidding - that would be a silly name. They just need to implement StampInterface.

... lines 1 - 2
namespace App\Messenger;
use Symfony\Component\Messenger\Stamp\StampInterface;
class UniqueIdStamp implements StampInterface
{
... lines 9 - 19
}

And... that's it! This is an empty interface that just serves to "mark" objects as stamps. Inside... we get to do whatever we want... as long as PHP can serialize this message... which basically means: as long as it holds simple data. Let's add a private $uniqueId property, then a constructor with no arguments. Inside, say $this->uniqueId = uniqid(). At the bottom, go to Code -> Generate - or Command+N on a Mac - and generate the getter... which will return a string.

... lines 1 - 2
namespace App\Messenger;
use Symfony\Component\Messenger\Stamp\StampInterface;
class UniqueIdStamp implements StampInterface
{
private $uniqueId;
public function __construct()
{
$this->uniqueId = uniqid();
}
public function getUniqueId(): string
{
return $this->uniqueId;
}
}

Stamp, done!

Stamping... um... Attaching the Stamp

Next, inside AuditMiddleware, before we call the next middleware - which will call the rest of the middleware and ultimately handle or send the message - let's add the stamp.

But, be careful: we need to make sure that we only attach the stamp once. As we'll see in a minute, a message may be passed to the bus - and so, to the middleware - many times! Once when it's initially dispatched and again when it's received from the transport and handled. If handling that message fails and is retried, it would go through the bus even more times.

So, start by checking if null === $envelope->last(UniqueIdStamp::class), then $envelope = $envelope->with(new UniqueIdStamp()).

... lines 1 - 8
class AuditMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (null === $envelope->last(UniqueIdStamp::class)) {
$envelope = $envelope->with(new UniqueIdStamp());
}
... lines 16 - 21
}
}

Envelopes are Immutable

There are a few interesting things here. First, each Envelope is "immutable", which means that, just due to the way that class was written, you can't change any data on it. When you call $envelope->with() to add a new stamp, it doesn't actually modify the Envelope. Nope, internally, it makes a clone of itself plus the new stamp.

That's... not very important except that you need to remember to say $envelope = $envelope->with() so that $envelope becomes the newly stamped object.

Fetching Stamps

Also, when it comes to stamps, an Envelope could, in theory, hold multiple stamps of the same class. The $envelope->last() method says:

Give me the most recently added UniqueIdStamp or null if there are none.

Dumping the Unique Id

Thanks to our work, below the if statement - regardless of whether this message was just dispatched... or just received from a transport... or is being retried - our Envelope should have exactly one UniqueIdStamp. Fetch it off with $stamp = $envelope->last(UniqueIdStamp::class). I'm also going to add a little hint to my editor so that it knows that this is specifically a UniqueIdStamp.

... lines 1 - 8
class AuditMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (null === $envelope->last(UniqueIdStamp::class)) {
$envelope = $envelope->with(new UniqueIdStamp());
}
/** @var UniqueIdStamp $stamp */
$stamp = $envelope->last(UniqueIdStamp::class);
... lines 19 - 21
}
}

To see if this is working, let's dump($stamp->getUniqueId()).

... lines 1 - 8
class AuditMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if (null === $envelope->last(UniqueIdStamp::class)) {
$envelope = $envelope->with(new UniqueIdStamp());
}
/** @var UniqueIdStamp $stamp */
$stamp = $envelope->last(UniqueIdStamp::class);
dump($stamp->getUniqueId());
return $stack->next()->handle($envelope, $stack);
}
}

Let's try it! If we've done our job well, for an asynchronous message, that dump() will be executed once when the message is dispatched and again inside of the worker when it's received from the transport and handled.

Refresh the page just to be safe, then upload an image. To see if our dump() was hit, I'll use the link on the web debug toolbar to open up the profiler for that request. Click "Debug" on the left and... there it is! Our unique id! In a few minutes, we'll make sure that this code is also executed in the worker.

And because middleware are executed for every message, we should also be able to see this when deleting a message. Click that, then open up the profiler for the DELETE request and click "Debug". Ha! This time there are two distinct unique ids because deleting dispatches two different messages.

Next, let's actually do something useful with this! Inside of our middleware, we're going to log the entire lifecycle of a single message: when it's originally dispatched, when it's sent to a transport and when it's received from a transport and handled. To figure out which part of the process the message is currently in, we're going to once again use stamps. But instead of creating new stamps, we'll read the core stamps.

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