Queuing Mailables with Custom Headers in Laravel 5.4

Earlier this week, I had the need to upgrade a Laravel 5.2 application to Laravel 5.4. Using the amazing Laravel Shift service, it was relatively painless. (Highly recommend Laravel Shift!)

That is, until I encountered Mailables.

The Problem

Mailables are a great way to encapsulate logic around email messages that you intend to send. It gives the ability to represent a message as a distinct object in your application and gives a home to messages. Using the Mail façade, it’s easy to send email messages to multiple recipients by instantiating a new instance of the Mailable and passing it to the send() method of the façade.

And of course, if you want to queue those messages to send later, you can use the queue() method instead, which will then pass the job onto our queue, which can be handled by the artisan queue:listen command.

However, in Laravel 5.4, a new constraint was put into place that in order to queue messages, you had to use a Mailable object. This in itself is not a bad constraint — it makes it more straightforward in how to serialize the object to storage for processing later. But, if you have need to work directly with the generated email message — perhaps to add some custom headers — represented as a Swift_Message, you’re a bit out of luck, as you can’t access that message until the job is processed.

To handle this, the Mailable class provides a withSwiftMessage() method that allows you to pass a callback closure, executing it when the message itself is sent and giving you access to the underlying Swift_Message.

What’s the problem with this?

The problem is that you cannot serialize a closure when stored as a variable.

Consequently, the callback becomes useless when you are trying to queue messages (vs. sending them immediately).

The Context

I had the case where, for message tracking purposes, I add two custom headers to each outgoing email that identifies the source object that sent the message. This worked fine in Laravel 5.2, doing something like this within an event listener:

// Create a simple array for improved serialization!
$data = [
'email' => $recipient->email,
'name' => $recipient->name,
'source' => SocialGroup::class,
'id’ => 42,
];
$this->mailer->queue(
'announcement-posted', [
'announcement' => $announcement,
'recipient' => $recipient,
],
function ($m) use ($data) {
$m
->to($data[‘email’], $data[‘name’])
->subject(‘There’s a new announcement for you!’);
// Set some custom headers.
$m->getSwiftMessage()->getHeaders()->addTextHeader(‘X-Messagable-Source’, $data[‘source’]);
$m->getSwiftMessage()->getHeaders()->addTextHeader(‘X-Messagable-ID’, $data[‘id’]);
}
);

This worked because Laravel 5.2 included a special library for serializing closures. It has since been removed to minimize framework dependencies.

So, how do we go about making this work in a 5.4 (or later) install?

Well, we could reimplement the special library to make it work. We could also utilize dependency injection and choose a different mailing library altogether.

Or, we could turn our old friend, object-oriented programming inheritance!

Knowing When To Do The Job

The trick is in knowing when things are processed by Laravel. The general, simple order goes something like this:

  1. Create an instance of a Mailable object.
  2. Pass it to the queue() method of the Mail façade (which maps to a singleton instance of the Illuminate\Mail\Mailer class), which queues it to be sent by your job handler later — serializing it in most cases to your backend.
  3. When the job handler picks up the new job, it deserializes the Mailable instance and executes the send() method on it to send the message.

It’s step 3 when we can introduce logic to handle what we need. We’ll just move the logic we would have previously passed in as a closure to a regular method.

The Journey

So, let’s extend the default Mailable class so that we can store our intended headers, say as an array map, by passing them via the constructor:

namespace App\Mail;use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
/**
* Overrides…
*/
use Illuminate\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
abstract class AbstractMessage extends Mailable
{
use Queueable, SerializesModels;
/**
* Describes additional headers.
*
* @var array
*/
protected $headers = [];

public function __construct(array $headers = [])
{
$this->headers = $headers;
}
}

Now, say we have a concrete representation of this class, such as an AnnouncementPosted class:

namespace App\Mail;class AnnouncementPosted extends AbstractMessage
{
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->subject('There’s a new announcement for you!')
->view(‘emails.announcement-posted’);
}
}

This gives us an easy way to store headers when we create a new Mailable (for example, from a controller):

$headers = ['X-Special-Header' => 'The Cat Did It'];
Mail::to('bob@smith.com')->queue(new AnnouncementPosted($headers));

We are just passing in an array map of headers into the Mailable instance. So far, so good.

Even though our Mailable is now storing the header data, we have to actually tell the Mailable to use the headers, so that it can send them. Within our AbstractMessage class we started above, the secret is to override the send() method:

abstract class AbstractMessage extends Mailable
{
// ...
/**
* Send the message using the given mailer.
*
* @param \Illuminate\Contracts\Mail\Mailer $mailer
* @return void
*/
public function send(MailerContract $mailer)
{
Container::getInstance()->call([$this, 'build']);
$mailer->send($this->buildView(), $this->buildViewData(),
function ($message) {
$this->buildFrom($message)
->buildRecipients($message)
->buildSubject($message)
->buildAttachments($message)
->attachCustomHeaders($message) // This is new!
->runCallbacks($message);
});
}
// ...
}

All of this is straight from the Illuminate\Mail\Mailable class, but we’ve made a slight addition. You’ll see that we’ve added a new method call to the closure passed to the $mailer->send() method called attachCustomHeaders($message). This will be where we actually do the work of attaching our headers. Further on down in our AbstractMessage class, we define that method:

abstract class AbstractMessage extends Mailable
{
// ...
/**
* Add custom headers to the message.
*
* @param \Illuminate\Mail\Message $message
* @return $this
*/
protected function attachCustomHeaders($message)
{
$swift = $message->getSwiftMessage();
$headers = $swift->getHeaders();
foreach ($this->headers as $header => $value) {
$headers->addTextHeader($header, $value);
}
return $this;
}
// ...
}

When the message is sent, then, our new method will be called. We will have access to the original Mailable instance via the $this parameter, and access to the message to be sent via the $message variable within the original send() method. We will call attachCustomHeaders($message), get the underlying Swift_Message object from getSwiftMessage(), and then iterate through our header array map in order to add our custom headers to the message. In the spirit of a fluent interface, we return the instance.

Why Does This Work Again?

This works because this send() method gets executed after the Mailable is deserialized from your backend. We don’t have to worry about using the withSwiftMessage() callback that can’t be serialized anyway (again, this is because PHP can’t serialize closures when stored as variables), so we work around it by just changing how the outgoing message is built when it is handled by the queue handler.

Of course, we have to remember to change all of our Mailables to inherit from our AbstractMessage class. It’s easy to support additional things, like view variables or even subject variables, if we want to as well.

Conclusion

Laravel is an amazing framework, but don’t forget that it is an object-oriented programming framework that can be extended as needed! This should provide a nice solution for working around a PHP limitation and how Laravel queues messages.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store