cult3

Sending template emails through Mandrill in Laravel

Oct 12, 2015

Table of contents:

  1. How does the Laravel Email service work?
  2. Creating the Message class
  3. The Transport Interface
  4. The Mailer Service
  5. The Log Transport implementation
  6. The Mandrill implementation
  7. Adding the Transport Manager
  8. Adding the Service Provider
  9. Conclusion

Sending emails in web applications is a necessary evil. On one hand, emails are extremely important for engagement and to ensure your customers get the most out of your application. On the other hand, they can be a pain in the arse to get right.

Email development is still stuck in 1995, and so, you need to write everything as a horrible mess of HTML tables. Sending email requires dealing with an Email server, and every time the marketing department wants to make a change to an email, engineering has to deploy a new release.

Of course, there is a much better way. There are a number of tools that can be combined as part of a build process to turn regular HTML into email-compatible HTML.

There are also a number of services that provide an API for sending emails so you don’t have to host the email server yourself.

And the final piece of the puzzle is abstracting the actual HTML template to the email provider so you don’t have to generate the HTML as part of the application. This means the marketing department can tweak the HTML as much as they want.

Mandrill (from the creators of MailChimp) is an email provider that ticks all of these boxes!

Unfortunately, Laravel does not support using hosted email templates out of the box.

However, it’s not too difficult to set up our own version of Laravel’s email service, and so in today’s tutorial we will be looking at doing just that.

How does the Laravel Email service work?

Before we get into building the Mandril implementation, first we will explore how Laravel’s Email service works.

Laravel allows you to switch out the email provider using a simple configuration option. This makes it easy to switch providers (for example, going from Mandrill to SendGrid).

But also, it allows you to simply log that an email was sent in development, rather than actually sending the email.

This is good because it will save you money and stop you from accidentally emailing your customers.

As a side note, Mandrill also offers a test environment that you can use to send emails and then review the contents of what was sent within the Mandrill dashboard. This is really good because there is no risk of accidentally emailing a customer, and you can see the email in Mandrill, rather than having to actually send the email to a real email address.

In order to use the correct Transport implementation for the given config, we can use a Manager class. We saw how to implement Manager classes for the IoC container in How to resolve environment specific implementations from Laravel’s IoC Container.

So to make this work, we’re basically going to scrap the Laravel email service and instead build our own using the same principles.

Creating the Message class

The first thing I’m going to do is to create a Message class. This object will hold the details of the message we are attempting to send, including the template to use, as well as the dynamic content variables we need to send to mandrill, the to addresses, and the subject.

class Message
{
    /**
     * @var string
     */
    private $template;

    /**
     * @var array
     */
    private $content;

    /**
     * @var array
     */
    private $to = [];

    /**
     * @var string
     */
    private $subject;

    /**
     * @var bool
     */
    private $sent = false;

    /**
     * @param string $template
     * @param array $content
     * @return void
     */
    public function __construct($template, array $content)
    {
        $this->template = $template;
        $this->content = $content;
    }

    /**
     * Set the `to` field
     *
     * @param string $name
     * @param string $email
     * @return void
     */
    public function to($name, $email)
    {
        $this->to[] = array_merge(compact("name", "email"), ["type" => "to"]);
    }

    /**
     * Set the subject
     *
     * @param string $subject
     * @return void
     */
    public function subject($subject)
    {
        $this->subject = $subject;
    }

    /**
     * Mark the Message as sent
     *
     * @return void
     */
    public function sent()
    {
        $this->sent = true;
    }

    /**
     * Check to see if the Message has been sent
     *
     * @return bool
     */
    public function isSent()
    {
        return $this->sent;
    }

    /**
     * Return the Message as an array
     *
     * @return array
     */
    public function toArray()
    {
        return [
            "template" => $this->template,
            "subject" => $this->subject,
            "to" => $this->to,
            "content" => $this->content,
        ];
    }

    /**
     * Retrieve private attributes
     *
     * @param string $key
     * @return mixed
     */
    public function __get($key)
    {
        if (property_exists($this, $key)) {
            return $this->$key;
        }
    }
}

I’m also including a simple boolean value to record if the message has been marked as sent. This will make testing easier.

And here are the tests. As you can see, they’re pretty straight forward:

class MessageTest extends \TestCase
{
    /** @test */
    public function should_mark_message_as_sent()
    {
        $message = new Message("template", ["name" => "John"]);

        $message->sent();

        $this->assertTrue($message->isSent());
    }

    /** @test */
    public function should_add_subject()
    {
        $message = new Message("template", ["name" => "John"]);

        $message->subject("subject");

        $this->assertEquals("subject", $message->subject);
    }

    /** @test */
    public function should_add_to_address()
    {
        $message = new Message("template", ["name" => "John"]);

        $message->to("John Smith", "john@smith.com");

        $this->assertEquals(
            [
                [
                    "name" => "John Smith",
                    "email" => "john@smith.com",
                    "type" => "to",
                ],
            ],
            $message->to
        );
    }

    /** @test */
    public function should_return_message_as_array()
    {
        $message = new Message("template", ["name" => "John"]);

        $message->subject("subject");

        $message->to("John Smith", "john@smith.com");

        $this->assertEquals(
            [
                "template" => "template",
                "subject" => "subject",
                "to" => [
                    [
                        "name" => "John Smith",
                        "email" => "john@smith.com",
                        "type" => "to",
                    ],
                ],
                "content" => ["name" => "John"],
            ],
            $message->toArray()
        );
    }
}

The Transport Interface

Next I’m going to write a Transport Interface. I’m going to have two different Transport implementations initially. One for Mandrill, and one for Logging messages in development.

At some point in the future I could potentially add more, but in either case, it’s a good practice to define the interface.

interface Transport
{
    /**
     * Send the Message
     *
     * @param Message $message
     * @return Message
     */
    public function send(Message $message);
}

The interface only requires a single send() method that should accept an instance of Message.

The Mailer Service

Next I will create the Mailer Service class. This class will be the public API to this service.

The Mailer service should accept an object that implements the Transport interface through the constructor.

class Mailer
{
    /**
     * @var Transport
     */
    private $transport;

    /**
     * @param Transport $transport
     * @return void
     */
    public function __construct(Transport $transport)
    {
        $this->transport = $transport;
    }

    /**
     * Send a Message through the Mailer
     *
     * @param Message $message
     * @return Message
     */
    public function send(Message $message)
    {
        return $this->transport->send($message);
    }
}

The class only requires a single send() method, which will just delegate to the Transport instance. In the future we can add additional functionality to this class.

Here is the test for the Mailer class:

use Mockery as m;
use Cribbb\Mail\Mailer;
use Cribbb\Mail\Message;

class MailerTest extends \TestCase
{
    /** @test */
    public function should_send_message()
    {
        $transport = m::mock("Cribbb\Mail\Transport");
        $transport->shouldReceive("send")->once();

        $mailer = new Mailer($transport);
        $mailer->send(new Message("template", []));
    }
}

In this test I’m asserting that the message is delegated to the injected Transport instance.

The Log Transport implementation

Next I will create the first implementation for logging requests in the development environment:

use Cribbb\Mail\Message;
use Cribbb\Mail\Transport;
use Psr\Log\LoggerInterface;

class LogTransport implements Transport
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @param LoggerInterface $logger
     * @return void
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * Send the Message
     *
     * @param Message $message
     * @return Message
     */
    public function send(Message $message)
    {
        $this->logger->debug("Mailer:", $message->toArray());

        $message->sent();

        return $message;
    }
}

First I will inject an object that complies to the LoggerInterface interface. I’m going to be using Laravel’s native logging functionality, so this will work fine.

Next I will implement the send method. First I add the message contents to the log by calling the toArray() method on the message. Next I mark the message as sent and return it from the method.

The test for this class is fairly simple:

use Mockery as m;
use Papertrail\Mail\Message;
use Papertrail\Mail\Transport\LogTransport;

class LogTransportTest extends \TestCase
{
    /** @test */
    public function should_send_message()
    {
        $logger = m::mock("Psr\Log\LoggerInterface");
        $logger->shouldReceive("debug")->once();

        $transport = new LogTransport($logger);
        $message = $transport->send(new Message("template", []));

        $this->assertTrue($message->isSent());
    }
}

First I mock the LoggerInterface and set the expectation that the debug() method should be called once.

Next I create the LogTransport instance and inject the mocked $logger. Finally, I pass a new instance of Message to the send() method, accept it as the returned value and then assert that is has been sent from the isSent() method.

The Mandrill implementation

Next I will create the Mandrill implementation. To make the HTTP request I will need to inject an instance of Guzzle’s Http Client. I will also need inject the Mandril API key, and an array of options. Finally I can add the API endpoint to the class:

use Cribbb\Mail\Message;
use Cribbb\Mail\Transport;
use GuzzleHttp\ClientInterface;

class MandrillTransport implements Transport
{
    /**
     * @var string
     */
    private $key;

    /**
     * @var ClientInterface
     */
    private $client;

    /**
     * @var array
     */
    private $options;

    /**
     * @var string
     */
    private static $endpoint = "https://mandrillapp.com/api/1.0/messages/send-template.json";

    /**
     * @param string $key
     * @param ClientInterface $client
     * @param array $options
     * @return void
     */
    public function __construct($key, ClientInterface $client, array $options)
    {
        $this->key = $key;
        $this->client = $client;
        $this->options = $options;
    }
}

Next I need to write a method to create the request. Mandril expects the data in a certain format, so we can deal with that here:

/**
 * Create the request
 *
 * @param Message $message
 * @return array
 */
public function request(Message $message)
{
    $data = [
        'key' => $this->key,
        'template_name' => $message->template,
        'template_content' => []
    ];

    $message = array_merge($this->options, [
        'to' => $message->to,
        'global_merge_vars' => array_map(function ($content, $name) {
            return compact('name', 'content');
        }, $message->content, array_keys($message->content))
    ]);

    $json = array_merge($data, compact('message'));

    return compact('json');
}

Finally I can add the send method to make the HTTP request:

/**
 * Send the Message
 *
 * @param Message $message
 * @return Message
 */
public function send(Message $message)
{
    $response = $this->client->post(self::$endpoint, $this->request($message));

    $message->sent();

    return $message;
}

In this method I’m making a POST request by passing the endpoint listed as a class property along with the return value of the request() method. Once again I can mark the message as sent and return it from the method.

As with the LogTransport test, the MandrillTransport test is fairly simple:

use Mockery as m;
use Cribbb\Mail\Message;
use Cribbb\Mail\Transport\MandrillTransport;

class MandrillTransportTest extends \TestCase
{
    /** @test */
    public function should_sent_message()
    {
        $client = m::mock("GuzzleHttp\ClientInterface");
        $client->shouldReceive("post")->once();

        $transport = new MandrillTransport("key", $client, []);
        $message = $transport->send(new Message("template", []));

        $this->assertTrue($message->isSent());
    }
}

In this test I just need to mock the ClientInterface interface and set the expectation that the post() method will be called once.

Next I set up the MandrillTransport implementation, pass a new instance of Message to the send() method, and then asset that the returned message has been sent.

Adding the Transport Manager

As we saw in How to resolve environment specific implementations from Laravel’s IoC Container, adding the functionality to switch implementations in Laravel via a config option is really easy.

First we create a class that extends the Manager class:

class TransportManager extends Manager
{
}

Next we add the default driver method to get the default driver from the configuration:

/**
 * Get the default driver
 *
 * @return string
 */
public function getDefaultDriver()
{
    return $this->app['config']['mail.driver'];
}

Next I will add a Log driver method:

/**
 * Create an instance of the Log Transport driver
 *
 * @return LogTransport
 */
protected function createLogDriver()
{
    return new LogTransport($this->app->make('Psr\Log\LoggerInterface'));
}

And finally a Mandrill driver method:

/**
 * Create an instance of the Mandrill Transport driver
 *
 * @return MandrillTransport
 */
protected function createMandrillDriver()
{
    $client = new HttpClient;

    $config = $this->app['config']->get('services.mandrill', []);

    return new MandrillTransport($config['key'], $client, [
        'track_opens' => true,
        'track_clicks' => true,
        'async' => false,
        'merge_language' => 'handlebars'
    ]);
}

In this method I first create a new instance of Guzzle’s HttpClient. Next I pull the config details from the services.php config file for Mandrill.

Finally I can create the MandrillTransport implementation and pass it the key, the $client and an array of options.

Adding the Service Provider

Finally we can create a Service Provider to resolve the Mailer service from the IoC container:

class MailServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services
     *
     * @return void
     */
    public function boot()
    {
        //
    }
    /**
     * Register any Blueprint services
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton("Cribbb\Mail\Mailer", function ($app) {
            $manager = new TransportManager($app);

            return new Mailer($manager->driver());
        });
    }
}

First I will create an instance of the TransportManager. Next I will resolve the correct driver and finally I will create a new instance of the Mailer and return it from the Closure.

Optionally you could also add a Facade to replace Laravel’s Mail facade.

Conclusion

In today’s tutorial we looked at creating our own custom implementation of Laravel’s Mailer service. Laravel provides really nice implementation agnostic services, but sometimes we need to change these services to meet our needs. It would be impractical for Laravel to support every single type of implementation.

Hopefully today’s tutorial has given you a sneak preview behind the curtain of how Laravel’s services work. As you can see, it’s really quite simple, and so we can very easily adopt the same foundation for our own application’s services.

This will make your custom services feel right at home within a Laravel application.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.