Setup for Messages from an Outside System

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

What if a queue on RabbitMQ was filled with messages that originated from an external system... but we wanted to consume and handle those from our Symfony App? For example, maybe a user can request that a photo be deleted from some totally different system... and that system needs to communicate back to our app so that it can actually do the deleting? How would that work?

Each transport in Messenger really has two jobs: one, to send messages to a message broker or queue system and two, to receive messages from that same system and handle them.

And, like we talked about in the last video, you don't need to use both features of a transport: you could choose to send to a transport, but never read and consume those messages... because some other system will. Or, you can do the opposite: create a transport that you will never send to, but that you will use to consume messages... that were probably put there by some outside system. The trick to doing this is creating a serializer that can understand the format of those outside messages.

Creating a new Message & Handler

Instead of over-explaining this, let's see it in action. First, pretend that this imaginary external system needs to be able to tell our app to do something... very... important: to log an Emoji. Ok, this may not be the most impressive type of a message... but the details of what this outside message is telling our app to do aren't important: it could be telling us to upload an image with details about where the file is located, delete an image, send an email to a registered user or, log an emoji!

Let's get this set up. Normally, if we wanted to dispatch a command to log an Emoji, we would start by creating a message class and message handler. In this case... we'll do the exact same thing. In the Command/ directory, create a new PHP class called LogEmoji.

... lines 1 - 2
namespace App\Message\Command;
class LogEmoji
{
... lines 7 - 17
}

Add a public function __construct(). In order to tell us which emoji to log, the outside system will send us an integer index of the emoji they want - our app will have a list of emojis. So, add an $emojiIndex argument and then press Alt+Enter and select "Initialize Fields" to create that property and set it.

... lines 1 - 2
namespace App\Message\Command;
class LogEmoji
{
private $emojiIndex;
public function __construct(int $emojiIndex)
{
$this->emojiIndex = $emojiIndex;
}
... lines 13 - 17
}

To make this property readable by the handler, go to the Code -> Generate menu - or Command + N on a Mac - select getters and generate getEmojiIndex().

... lines 1 - 2
namespace App\Message\Command;
class LogEmoji
{
private $emojiIndex;
public function __construct(int $emojiIndex)
{
$this->emojiIndex = $emojiIndex;
}
public function getEmojiIndex(): int
{
return $this->emojiIndex;
}
}

Brilliant! A perfectly boring, um, normal, message class. Step two: in the MessageHandler/Command/ directory, create a new LogEmojiHandler class. Make this implement our normal MessageHandlerInterface and add public function __invoke() with the type-hint for the message: LogEmoji $logEmoji.

... lines 1 - 2
namespace App\MessageHandler\Command;
use App\Message\Command\LogEmoji;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class LogEmojiHandler implements MessageHandlerInterface
{
public function __invoke(LogEmoji $logEmoji)
{
}
}

Now... we get to work! I'll paste an emoji list on top: here are the five that the outside system can choose from: cookie, dinosaur, cheese, robot, and of course, poop.

... lines 1 - 8
class LogEmojiHandler implements MessageHandlerInterface
{
private static $emojis = [
'🍪',
'🦖',
'🧀',
'🤖',
'💩'
];
... lines 18 - 33
}

And then, because we're going to be logging something, add an __construct() method with the LoggerInterface type hint. Hit Alt + Enter and select "Initialize Fields" one more time to create that property and set it.

... lines 1 - 5
use Psr\Log\LoggerInterface;
... lines 7 - 8
class LogEmojiHandler implements MessageHandlerInterface
{
private static $emojis = [
'🍪',
'🦖',
'🧀',
'🤖',
'💩'
];
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 25 - 33
}

Inside __invoke(), our job is pretty simple. To get the emoji, set an $index variable to $logEmoji->getEmojiIndex().

... lines 1 - 5
use Psr\Log\LoggerInterface;
... lines 7 - 8
class LogEmojiHandler implements MessageHandlerInterface
{
private static $emojis = [
'🍪',
'🦖',
'🧀',
'🤖',
'💩'
];
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function __invoke(LogEmoji $logEmoji)
{
$index = $logEmoji->getEmojiIndex();
... lines 30 - 32
}
}

Then $emoji = self::$emojis - to reference that static property - self::$emojis[$index] ?? self::emojis[0].

... lines 1 - 5
use Psr\Log\LoggerInterface;
... lines 7 - 8
class LogEmojiHandler implements MessageHandlerInterface
{
private static $emojis = [
'🍪',
'🦖',
'🧀',
'🤖',
'💩'
];
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function __invoke(LogEmoji $logEmoji)
{
$index = $logEmoji->getEmojiIndex();
$emoji = self::$emojis[$index] ?? self::$emojis[0];
... lines 31 - 32
}
}

In other words, if the index exists, use it. Otherwise, fallback to logging a cookie... cause... everyone loves cookies. Log with $this->logger->info('Important message! ')and then $emoji.

... lines 1 - 5
use Psr\Log\LoggerInterface;
... lines 7 - 8
class LogEmojiHandler implements MessageHandlerInterface
{
private static $emojis = [
'🍪',
'🦖',
'🧀',
'🤖',
'💩'
];
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function __invoke(LogEmoji $logEmoji)
{
$index = $logEmoji->getEmojiIndex();
$emoji = self::$emojis[$index] ?? self::$emojis[0];
$this->logger->info('Important message! '.$emoji);
}
}

The big takeaway from this new message and message handler is that it is, well, absolutely no different from any other message and message handler! Messenger does not care whether the LogEmoji object will be dispatched manually from our own app or if a worker will receive a message from an outside system that will get mapped to this class.

To prove it, go up to ImagePostController, find the create() method and, just to see make sure this is working, add: $messageBus->dispatch(new LogEmoji(2)).

... lines 1 - 7
use App\Message\Command\LogEmoji;
... lines 9 - 25
class ImagePostController extends AbstractController
{
... lines 28 - 42
public function create(Request $request, ValidatorInterface $validator, PhotoFileManager $photoManager, EntityManagerInterface $entityManager, MessageBusInterface $messageBus)
{
... lines 45 - 73
$messageBus->dispatch(new LogEmoji(2));
... lines 75 - 76
}
... lines 78 - 105
}

If this is working, we should see a message in our logs each time we upload a photo. Find your terminal: let's watch the logs with:

tail -f var/log/dev.log

That's the log file for the dev environment. I'll clear my screen, then move over, select a photo and... move back. There it is:

Important message! 🧀

I agree! That is important! This is cool... but not what we really want. What we really want to do is use a worker to consume a message from a queue - probably a JSON message - and transform that intelligently into a LogEmoji object so Messenger can handle it. How do we do that? With a dedicated transport and a customer serializer. Let's do that next!

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