Symfony Mailer Service

Whenever you work with big and complex applications. It might be a good idea to abstract pure infrastructure-focused tasks outside the application parts of your code. One such infrastructure task is sending emails.

Sending emails should be as easy and reusable as possible. For this reason, we should create a Mailer Infrastructure.

Why should we create a Mailer Infrastructure?

Because the infrastructure layer is used to abstract technical concerns and implementations. It is not the concern of the application or the domain to know how emails are sent.

Your infrastructure adapters should implement an interface contract provided by your application. The interface should define the public methods it requires to fulfil’s its uses.

By creating infrastructure services or adapters we are not dependent on these implementations. It is not important for our application that an email is sent by Swift Mail, Mailchimp or whatever API. Whenever we change how we send emails. We just want emails to be sent. By using infrastructure services or adapters we are free from these changes and would not require rewrites when something changes.

Creating the Sender Service

Before we start you need to have some basic framework setup. In this example, I will be using Symfony. But the code that we create here is universal and could be used in any PHP framework.

I also assume that you will be using some kind of layered architecture. Or at least separate infrastructure from the rest of your application.

We will be starting by creating a Mailer directory inside our Infrastructure directory.

Next, we will create our SenderInterface and Sender implementation.

<?php
namespace App\Infrastructure\Mailer\Sender;

interface SenderInterface
{
    public function send(string $from, array $recipients, string $subject, string $body): void;
}
<?php
namespace App\Infrastructure\Mailer\Sender;

final class Sender implements SenderInterface
{
    public function send(string $from, array $recipients, string $subject, string $body): void
    {
        
    }
}

As you can see we will be having a from address, an array of one or more recipients, a subject and a body. You could extend this with attachments or any other functionality you require. This is something you should fine-tune to your needs.

The SenderInterface is your port. This should be decided by your application and thus could be placed in the ports directory of your Ports and Adapter application.

The Mailer Adapter

The Mailer adapter will be used to actually send the email. For this, we first create a MailerInterface. We could then create multiple implementations to send an email. In this example, we will be using Swift Mailer, but you could create an adapter for any Mailer API that you want. Switching to another Mailer API will be as easy as creating a new Adapter that implements the MailerInterface.

<?php
namespace App\Infrastructure\Mailer\Sender\Adapter;

interface MailerInterface
{
    public function send(string $from, array $recipients, string $subject, string $body): void;
}
<?php
namespace App\Infrastructure\Mailer\Sender\Adapter;

final class SwiftMailer implements MailerInterface
{
    /**
     * @var \Swift_Mailer
     */
    private $mailer;

    public function __construct(\Swift_Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send(string $from, array $recipients, string $subject, string $body): void
    {
        $swiftMessage = (new \Swift_Message($subject))
            ->setFrom($from)
            ->setTo($recipients)
            ->setBody($body);
        $this->mailer->send($swiftMessage);
    }
}

As you can see we have decoupled Swift Mailer from the rest of our application.

You could argue now. Why did we create a SenderInterface and a MailerInterface? Now you are just passing data from the one to the other?

<?php
namespace App\Infrastructure\Mailer\Sender;

use App\Infrastructure\Mailer\Sender\Adapter\MailerInterface;

final class Sender implements SenderInterface
{
    /**
     * @var MailerInterface
     */
    private $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send(string $from, array $recipients, string $subject, string $body): void
    {
        $this->mailer->send($from, $recipients, $subject, $body);
    }
}

Well, that is correct. But we don’t want to send plain text emails. We want to send fully-fledged HTML emails. And for this, we need to create a Renderer Adapter to render a template instead of a subject and body.

Creating the Renderer Adapter

In Symfony, we use Twig for creating our views. So it is also a good idea to use Twig to create email templates. But this can sometimes get kinda messy. Tha’st why we should create a Renderer Adapter to hide the code in our Infrastructure. This also allows us to switch to other Template Renderers if we so desire.

First, we create our RenderedEmail object. This object represents our Email that has been rendered.

<?php
namespace App\Infrastructure\Mailer\Renderer;

final class RenderedEmail
{
    /**
     * @var string
     */
    private $subject;

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

    public function __construct(string $subject, string $body)
    {
        $this->subject = $subject;
        $this->body = $body;
    }

    public function subject(): string
    {
        return $this->subject;
    }

    public function body(): string
    {
        return $this->body;
    }
}

As you can see. This is a simple immutable data transfer object. Our rendered adapter will create a rendered email, and then we can use this object to send trough one of our Mailer adapters.

The next step is to create our EmailRendererInterface. This is the contract that any adapter needs to comply to.

<?php
namespace App\Infrastructure\Mailer\Renderer\Adapter;

use App\Infrastructure\Mailer\Renderer\RenderedEmail;

interface EmailRendererInterface
{
    public function render(string $template, array $data): RenderedEmail;
}

As you can see. We need a template name and an array of data that can be used to personalize the rendered email.

Now we can create our Twig adapter to render a given template with the provided data.

<?php
namespace App\Infrastructure\Mailer\Renderer\Adapter;

use App\Infrastructure\Mailer\Renderer\RenderedEmail;

final class TwigEmailEmailRenderer implements EmailRendererInterface
{
    /**
     * @var \Twig_Environment
     */
    private $twig;

    public function __construct(\Twig_Environment $twig)
    {
        $this->twig = $twig;
    }

    public function render(string $template, array $data): RenderedEmail
    {
        $data = $this->twig->mergeGlobals($data);
        $template = $this->twig->loadTemplate($template);
        $subject = $template->renderBlock('subject', $data);
        $body = $template->renderBlock('body', $data);
        return new RenderedEmail($subject, $body);
    }
}

An example of a Twig template that can be rendered:

{% block subject %}
    A new order {reference} has been created
{% endblock %}

{% block body %}
    A new order with reference {reference} was created.
{% endblock %}

And then in the Sender implementation, we can use the renderer and send the rendered template trough the Mailer Adapter.

<?php
namespace App\Infrastructure\Mailer\Sender;

interface SenderInterface
{
    public function send(string $from, array $recipients, string $template, array $data): void;
}
<?php
namespace App\Infrastructure\Mailer\Sender;

use App\Infrastructure\Mailer\Renderer\Adapter\EmailRendererInterface;
use App\Infrastructure\Mailer\Sender\Adapter\MailerInterface;

final class Sender implements SenderInterface
{
    /**
     * @var EmailRendererInterface
     */
    private $emailRenderer;

    /**
     * @var MailerInterface
     */
    private $mailer;

    public function __construct(EmailRendererInterface $emailRenderer, MailerInterface $mailer)
    {
        $this->emailRenderer = $emailRenderer;
        $this->mailer = $mailer;
    }

    public function send(string $from, array $recipients, string $template, array $data): void
    {
        $renderedEmail = $this->emailRenderer->render($template, $data);
        $this->mailer->send($from, $recipients, $renderedEmail->subject(), $renderedEmail->body());
    }
}

An implementation example

When you send an email, I hope you don’t send it directly from a controller. An email is always sent by some business action. The business decides when an email should be sent.

But the actual sending of the email is not a domain but an application concern. This means we should send emails after a domain event has been dispatched.

So a good example would be to send an email in your subscriber.

<?php
namespace App\Application\EventSubscriber;

use App\Domain\Event\SalesOrderCreatedEvent;
use App\Infrastructure\Mailer\Sender\SenderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Class MailOnSalesOrderCreated
 * @package App\Application\EventSubscriber
 */
final class MailOnSalesOrderCreated implements EventSubscriberInterface
{
    /**
     * @var SenderInterface
     */
    private $sender;

    public function __construct(SenderInterface $sender) {
        $this->sender = $sender;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            SalesOrderCreatedEvent::class => ['sendEmail']
        ];
    }

    public function sendEmail(SalesOrderCreatedEvent $salesOrderCreatedEvent): void
    {
        $reference = $salesOrderCreatedEvent->orderReference();
        $this->sender->send('noreply@example.com', ['jeffrey.verreckt@gmail.com'], '@BackOffice/emails/sales_order_created.html.twig', [
            'reference' => $reference
        ]);
    }
}

Now you can see, we are using the event subscriber from Symfony. And yes, we should also our own interfaces and adapters for events and subscribers, so that we are also not dependent on the framework.
But that’s something for another day.

Conclusion

I hope you will appreciate my example of how to create a Mailer Service in your Infrastructure layer. In my opinion, you should always create some kind of Mailer Interface to separate the actual email sending from the rest of your application. But remember, you can fine-tune and adapt this to your needs.

Feel free to use my example to build on or get some ideas from it. And, as always, think to code!

Also, you should take a look at this article about How to Send Emails in Symfony. It contains working examples as well. If you’re looking for more information about sending emails, check out the Mailtrap blog.