Sending emails with Symfony: Swift Mailer or the Mailer component?
For many years Swift Mailer has been the de facto solution for sending automated emails with Symfony. However in March 2019, Fabien Potencier introduced two new experimental components for Symfony 4.3: Mime and Mailer.
You might be thinking:
What’s the difference? Can I keep using Swift Mailer or will I have to upgrade?
In this post we’ll explore the differences between the two, and what you should use in your next Symfony application.
In the beginning there was mail()
In the early days of PHP, developers would use the built-in mail() function to send an email.
It relies on a mail transfer agent (e.g. sendmail
or postfix
) existing on your server, and can often fail in weird and unexpected ways, only returning false
.
Not particularly helpful for debugging!
It also lacks ‘dry-run’ functionality for development and testing.
Swift Mailer emerged in the early 2000s as an object-oriented library to create and send email messages using a variety of transports. These transports are swappable, letting you change how emails are sent in a way that’s transparent to your application. For example, you might want to use the NullTransport while developing, and multiple EsmtpTransport instances combined with the FailoverTransport in production.
Swift Mailer also includes easy ways to:
- Send multipart / HTML emails;
- Add attachments with low-memory usage;
- Connect to servers that requires a username, password, or encryption.
Swift Mailer is well-tested and has been used in production for a long time. The earliest release on Github - a beta of version 4 - was published in 2009.
However, in that time it has been somewhat left behind by modern PHP features:
- It uses underscores in class names instead of proper namespacing (
\Swift_Mailer
vsSwift\Mailer
); - Version 6 is the first release to require PHP 7. Version 5 still supports PHP 5.3;
- No use of scalar types and other PHP 7 features.
A few people have suggested cleaning up the codebase, but instead Fabien chose to replace Swift Mailer with the Mailer component.
It aims to do for Swift Mailer what Swift Mailer did for mail()
- cleanup, modernise, and add new features.
Usage
From a high level, both libraries look similar:
<?php
// Sending an email with Swift Mailer
$message = (new Swift_Message('RE: FW: Check this out!'))
->setFrom('glynn@example.com')
->setTo('customer@example.org')
->setBody(
'<h1>HTML email</h1><p>Email with HTML tags if the client supports it.</p>',
'text/html'
)
->addPart(
'Plain text email',
'text/plain'
);
/**
* @var Swift_Mailer $mailer
*/
$mailer->send($message);
<?php
// Sending an email with the Symfony Mailer
$email = (new Symfony\Component\Mime\Email())
->from('glynn@example.com')
->to('customer@example.org')
->subject("What's the point of the Symfony Mailer, anyway?")
->html('<h1>HTML email</h1><p>Email with HTML tags if the client supports it.</p>')
->text('Plain text email');
/**
* @var Symfony\Component\Mailer\MailerInterface $mailer
*/
$mailer->send($email);
For both libraries, you create an email object, customise it with setter functions, and send it using a mailer object.
Benefits of Mailer
Beyond basic usage, we get to see where the Mailer component really shines.
Modern codebase
As a brand new library in 2019, Symfony Mailer fixes all of Swift Mailer’s legacy problems:
- Proper PSR-4 namespacing;
- Strict scalar type hints in function arguments;
- Requires at least PHP 7.1, allowing it to incorporate new language features and syntax.
Simpler class structure
As Fabien mentions in his slides, each Mime message is smaller and simpler than the Swift Mailer equivalent:
- A
Swift_Message
instance is made up of 38 objects and is around 16kB in length when serialised. - A
Symfony\Component\Mime\Email
instance is made up of only 7 objects and is around 2kB in length when serialised.
First class Twig integration
While Swift Mailer doesn’t stop you using Twig to create the email body, the Symfony Mailer has built-in support with the TemplatedEmail
class:
<?php
$email = (new TemplatedEmail())
->from('glynn@example.com')
->to('customer@example.org')
->subject('Order confirmation: 300 Easter Eggs')
->textTemplate('order_confirmation.txt.twig')
->htmlTemplate('order_confirmation.html.twig');
Even better, you can use a single template for HTML, plain text, subject line, and even attachments:
<?php
$email = (new TemplatedEmail())
->from('glynn@example.com')
->to('customer@example.org')
->template('order_confirmation.email.twig')
{% block html %}
<h1>Confirmation</h1>
<p>Your order is confirmed!</p>
{% endblock %}
{% block text %}
Your order is confirmed!
{% endblock %}
{% block subject %}Order confirmation: 300 Easter Eggs{% endblock %}
{% block config %}
{% do email.attach('@images/company_logo.png') %}
{% endblock %}
Embedding files and images
Embedding images into HTML is also straightforward using the cid:<embed_name>
syntax:
<?php
$email = (new Email())
->embed(fopen('company_logo.png', 'r'), 'logo')
->embedFromPath('company_logo_banner.png', 'logo-banner')
->html('<img src="cid:logo"><img src="cid:logo-banner">');
You can also embed them directly using twig templates:
<img src="{{ email.image('company_logo.png') }}">
<h1>Confirmation</h1>
<p>Your order is confirmed!</p>
<img src="{{ email.image('company_logo_banner.png') }}">
And much more
Despite being a much smaller library than Swift Mailer, the Mailer component packs in a lot of features. See https://symfony.com/doc/current/mailer.html for the complete list.
Sending emails in the background
A common problem in web applications is sending emails in the main web process. Doing so can slow down your application, especially if the target email server is slow to respond. Have you ever filled out a contact form on a website and find the page takes a while to load? It could be because an email is being sent to the site owner and the page can’t respond until it’s finished.
Instead, it’s a good practice to use another process to send emails in the background. Symfony has developed a few ways to accomplish this over the years:
Swift Mailer spools
Swift Mailer has the concept of spools.
$mailer->send($email)
will place the message onto a spool, waiting until the spool is flushed to actually send the email.
Like transports, there are different spool implementations:
Swift_MemorySpool
- store messages in memory. To actually send the email you’d have to flush the spool in the same process.Swift_FileSpool
- store messages in files. This allows another process to load and flush the spool later in a different process from the web server.
In practical terms, this often means using the file spool to store emails on the filesystem, and have a cron job or background worker periodically load and flush the stored messages. You could also create a custom spool implementation using a database, in-memory cache, or message queue to share the spool across multiple servers.
Queues
While the spool concept is somewhat successful for emails, it doesn’t work for other background tasks, such as resizing an image or processing a video. For these tasks, developers can use a queuing system like RabbitMQ, Redis, and vendor-specific solutions like Amazon’s Simple Queue Service combined with background workers.
Whenever a long running task is required, the application will post the job to the queue. A worker process will then pull the job from the queue and run the given task.
To work with Swift Mailer, you might decide to use the memory spool combined with a background worker:
- Instead of calling
$mailer->send($email)
when your application needs to send an email, put$email
into a job and place it on the queue. - A background worker takes the job from the queue and reads the original
$email
object. - Configured with the memory spool, the worker calls
$mailer->send($email)
and flushes the spool immediately, sending the email.
Messenger component
Instead of combining spools and queues together, the Symfony Mailer focuses on a single concept for sending background emails: the Messenger component, another recent addition to Symfony in version 4.1. Similar to a queue, the Messenger component allows you to run a background worker process and dispatch jobs to it over a message queue.
The implementation is amazingly simple:
<?php
class Mailer implements MailerInterface
{
private $transport;
private $bus;
public function __construct(TransportInterface $transport, MessageBusInterface $bus = null)
{
$this->transport = $transport;
$this->bus = $bus;
}
public function send(RawMessage $message, SmtpEnvelope $envelope = null): void
{
if (null === $this->bus) {
$this->transport->send($message, $envelope);
return;
}
$this->bus->dispatch(new SendEmailMessage($message, $envelope));
}
}
If the mailer has the $bus
property set (a message bus from Messenger component), it will wrap the email in a SendEmailMessage
object.
If the bus isn’t available, it will send the message in the current process (like the memory spool).
This design makes it easy to go from in-process sending (don’t use the Messenger component, or use a synchronous Messenger transport) to background sending (use the Messenger component with an asynchronous transport).
kernel.terminate
There’s another option that’s somewhat less fashionable than it used to be - sending emails during the kernel.terminate
event.
When run in a PHP environment that supports it, this event fires after the HTTP response has been sent to the user.
This allows a user to submit the contact form and get a response back immediately, while the server actually spends 5 seconds at the end of the request waiting to send the email.
It has downsides however - kernel.terminate
occurs at the latest possible stage in the Symfony lifecycle, making it difficult to handle exceptions cleanly and track errors (nothing will show up in the profiler, for instance).
For this reason, proposals to add a kernel.terminate
mechanism to Messenger and Mailer have been rejected due to fears of debugging difficulties.
Swift Mailer can be configured to flush its memory spool on kernel.terminate
, however.
Development experience
Swift Mailer and the companion Swift Mailer Bundle have been around for a long time, so include lots of nice features for development such as:
- A well-integrated profiler panel;
- Enable / disable delivery via configuration;
- Send all emails to a particular address (with exceptions via pattern matching).
The Mailer lacks most of these at the moment. Some will be coming in future versions (the profiler panel), while others are deliberately not included in the name of simplicity.
However, due to the simplicity and flexibility of the component, these missing features can usually be added with a bit of manual work. For instance, to send all emails to a single address in development, you can configure the EnvelopeListener in your services configuration:
# config/services_dev.yaml
services:
mailer.dev.set_recipients:
class: Symfony\Component\Mailer\EventListener\EnvelopeListener
tags: ['kernel.event_subscriber']
arguments:
$sender: null
$recipients: ['developer@example.com']
Fabien’s tagline when he introduced the component was “back to basics”. I like this approach a lot. In today’s world of smaller, self-contained services, minimalist solutions that include only what they need are extremely attractive.
You might miss the development options of the Swift Mailer, but perhaps there’s a better way? Maybe it’s better to debug emails by sending to a fake inbox instead? This avoids any trickery with email recipient settings that could accidentally leak into a production configuration.
You could use MailHog as a test inbox and configure Symfony to send to it:
docker run --rm -ti -p 8025:8025 -p 1025:1025 mailhog/mailhog
# .env
MAILER_DSN=smtp://127.0.0.1:1025
Then go to http://localhost:8025 to see the emails arrive in the test inbox:
Easy!
Conclusion
With Swift Mailer development all but suspended, the Symfony Mailer is clearly the future. Its new features and ‘back to basics’ approach will make email sending with Symfony even easier. There may be some rough edges while the component is marked as experimental, but by the release of Symfony 5 we can expect to see a fully featured and capable email solution.
But what should I use today?
In my opinion, you should use Swift Mailer if:
- You have an existing application that uses it, or you’re building an application that will be released soon;
- You’ve come to rely on the features provided by the bundle;
- You’re stuck on an older version of PHP;
- You’re more concerned about stability than new features.
You should use the Symfony Mailer if:
- You’re working on a brand new application and will be for a while;
- You’re not afraid of breaking changes between minor versions;
- You’re using the Messenger component already in your application;
- You value new features over stability.