Symfony Mailer Service

In the previous article we learned about the Ports and Adapters architecture. One of the key ideas was to create Ports (Interfaces) and Adapters. Today we are going to apply these ideas to create a Mailer Infrastructure Service. This implementation is not strictly Ports and Adapters but can be used in any architecture you thinker of.

Why should we create a Mailer Infrastructure?

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 send.

Your infrastructure adapters should implement an interface contract provided by your application. The interface should define the public methods it require to fulfill’s its use.

By creating infrastructure services or adapters we are not dependent on these implementations. It is not important for our application that an email is send by Swift Mail, Mailchimp or whatever API. Whenever we change how we send emails. We just want emails to be send. 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 4. 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. I will be starting from the Layered Architecture Skeleton that you can find here.

We will be starting by creating a Mailer directory inside your 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 body. You could extend this with attachments or whatever 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 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. Because 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. That 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 send by some action that goes trough the domain. The business decides when a email should be send.

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

So a good example would be to send and 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 I provided you a good 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 sending of your email from the rest of your application.

But remember, you can fine tune and adapt this to your needs. I only provided you an example you can build on, or get some ideas from. And as always, think to code!