Async Emails with Messenger

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

Sending an email - like after we complete registration - takes a little bit of time because it involves making a network request to SendGrid. Yep, sending emails is always going to be a "heavy" operation. And whenever you're doing something heavy... it means your user is waiting for the response. That's... not the end of the world... but it's not ideal.

So... when a user registers, instead of sending the email immediately, could we send it... later and return the response faster? Of course! Thanks to Symfony's Messenger component, which has first-class integration with Mailer.

Installing & Configuring Messenger

First: in our editor, open .env.local and, for simplicity. let's change the MAILER_DSN back to use Mailtrap. To install Messenger... you can kinda guess the command. In your terminal, run:

composer require messenger

Messenger is super cool and we have an entire tutorial about it. But, it's also simple to get set up and running. Let's see how.

The recipe for Messenger just did a few things: it created a new messenger.yaml configuration file and also added a section in .env. Let's go find that.

62 lines .env
... lines 1 - 55
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=doctrine://default
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages

Here's the 30 second description of how to get Messenger set up. In order to do some work "later" - like sending an email - you need to configure a "queueing" system where details about that work - called "messages" - will be sent. Messenger calls these transports. Because we're already using Doctrine, the easiest "queueing" system is a database table. Uncomment that MESSENGER_TRANSPORT_DSN to use it.

Next, open config/packages/messenger.yaml - that's the new config file:

# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
# failure_transport: failed
# failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
# Route your messages to the transports
# 'App\Message\YourMessage': async

and uncomment the transport called async.

... lines 3 - 5
... line 7
... lines 9 - 16

Making Emails Async

Great. As soon as you install Messenger, when Mailer sends an email, internally, it will automatically start doing that by dispatching a message through Messenger. Hit Shift + Shift to open a class called SendEmailMessage.

Specifically, Mailer will create this object, put our Email message inside, and dispatch it through Messenger.

Now, if we only installed messenger, the fact that this is being dispatched through the message bus would make... absolutely no difference. The emails would still be handled immediately - or synchronously.

But now we can tell Messenger to "send" instances of SendEmailMessage to our async transport instead of "handling" them - meaning delivering the email - right now. We do that via the routing section. Go copy the namespace of the SendEmailMessage class and, under routing, I'll clear out the comments and say Symfony\Component\Mailer\Messenger\, copy the class name, and paste: SendEmailMessage. Set this to async.

... lines 3 - 11
# Route your messages to the transports
# 'App\Message\YourMessage': async
'Symfony\Component\Mailer\Messenger\SendEmailMessage': async

Hey! We just made all emails async! Woo! Let's try it: find the registration page.... register as "Fox", email, any password, agree to the terms and register!

You may not have noticed, but if you compared the response times of submitting the form before and after that change... this was way, way faster.

Checking out the Queue

Over in Mailtrap... there are no new messages. I can refresh and... nothing. The email was not delivered. Yay! Where is it? Sitting & waiting inside our queue... which is a database table. You can see it by running:

php bin/console doctrine:query:sql 'SELECT * FROM messenger_messages'

That table was automatically created when we sent our first message. It has one row with our one Email inside. If you look closely... you can see the details: the subject, and the email template that will be rendered when it's delivered.

Running the Worker

How do we actually send the email? In Messenger, you process any waiting messages in the queue by running:

php bin/console messenger:consume -vv

The -vv adds extra debugging info... it's more fun. This process is called a "worker" - and you'll have at least one of these commands running at all times on production. Check out our Messenger tutorial for details about that.

Cool! The message was "received" from the queue and "handled"... which is a fancy way in this case to say that the email was actually delivered! Go check out Mailtrap! Ah! There it is! The full correct email... in all its glory.

By the way, in order for your emails to be rendered correctly when being sent via Messenger, you need to make sure that you have the route context parameters set up correctly. That's a topic we covered earlier in this tutorial.

So... congrats on your new shiny async emails! Next, let's make sure that the "author weekly report" email still works... because... honestly... there's going to be a gotcha. Also, how does sending to a transport affect our functional tests?

Leave a comment!

This tutorial is built on Symfony 4.3, but will work well with Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.110.11
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-snappy-bundle": "^1.6", // v1.6.0
        "knplabs/knp-time-bundle": "^1.8", // v1.9.1
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.8.2
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.1,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.1.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.4.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.3.4
        "symfony/console": "^4.0", // v4.3.4
        "symfony/flex": "^1.0", // v1.6.2
        "symfony/form": "^4.0", // v4.3.4
        "symfony/framework-bundle": "^4.0", // v4.3.4
        "symfony/mailer": "4.3.*", // v4.3.4
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.3.4
        "symfony/sendgrid-mailer": "4.3.*", // v4.3.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.3.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "^4.0", // v4.3.4
        "symfony/web-server-bundle": "^4.0", // v4.3.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.6.2
        "symfony/yaml": "^4.0", // v4.3.4
        "twig/cssinliner-extra": "^2.12", // v2.12.0
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.0
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.2.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/browser-kit": "4.3.*", // v4.3.5
        "symfony/debug-bundle": "^3.3|^4.0", // v4.3.4
        "symfony/dotenv": "^4.0", // v4.3.4
        "symfony/maker-bundle": "^1.0", // v1.13.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.3.4