Mailing
Table of contents
Loading...Introduction
Mailing is the main way the application can communicate with users outside its own interface.
This is a guide on how to configure and test Symfony Mailer in a Slim 4 project.
Choosing the library
There are many PHP libraries for sending emails. Some of the most popular ones are:
The PHPMailer library is probably the most widely used one and still well maintained with a large community.
It has been around for a very long time and may not be as optimized for performance as newer,
more modern libraries such as Symfony Mailer.
Symfony Mailer replaced its predecessor SwiftMailer and
is only a few years old. It is performant, modern has a
large community and comes with lots of
built-in mailer assertions
which makes it a good choice.
Laminas mail feels a bit more lightweight, which is great, but it is not as popular as the other two.
It seems to require a bit more configuration and knowledge about transport and mailing.
When choosing a mailer, I wanted one that is intuitive and just works without asking too much.
This is why symfony/mailer
is the choice for the slim-example-project.
Symfony Mailer
Configuration
Mailers can use different "transports" which are methods or protocols used to send emails.
The most common one is SMTP.
File: config/defaults.php
$settings['smtp'] = [
// use type 'null' for the null adapter
'type' => 'smtp',
'host' => 'smtp.mailtrap.io',
'port' => '587', // TLS: 587; SSL: 465
'username' => 'my-username',
'password' => 'my-secret-password',
];
The real host, secret username and password are stored in the environment-specific file config/env/env.php
:
$settings['smtp']['host'] = 'smtp.host.com';
$settings['smtp']['username'] = 'username';
$settings['smtp']['password'] = 'password';
Setup
The mailer has to be instantiated with the configuration in the DI container.
The EventDispatcher
is also added
to the container and passed to the mailer.
This allows asserting email content for testing.
File: config/container.php
use Psr\Container\ContainerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Mailer\EventListener\EnvelopeListener;
use Symfony\Component\Mailer\EventListener\MessageListener;
use Symfony\Component\Mailer\EventListener\MessageLoggerListener;
return [
// ...
MailerInterface::class => function (ContainerInterface $container) {
$settings = $container->get('settings')['smtp'];
// smtp://user:pass@smtp.example.com:25
$dsn = sprintf(
'%s://%s:%s@%s:%s',
$settings['type'],
$settings['username'],
$settings['password'],
$settings['host'],
$settings['port']
);
$eventDispatcher = $container->get(EventDispatcherInterface::class);
return new Mailer(Transport::fromDsn($dsn, $eventDispatcher));
},
// Event dispatcher for mailer. Required to retrieve emails when testing.
EventDispatcherInterface::class => function () {
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addSubscriber(new MessageListener());
$eventDispatcher->addSubscriber(new EnvelopeListener());
$eventDispatcher->addSubscriber(new MessageLoggerListener());
return $eventDispatcher;
},
];
Creating and sending emails
To create a new email, an instance of the class Symfony\Component\Mime\Email
has to be created.
It is a data object that holds the email content and metadata as well as methods
for setting and getting these properties.
The Email body can be plain text or HTML. The PHP template renderer can be used to render the HTML for the email body from a template.
To get the HTML from a template and to send the email, the App\Infrastructure\Service\Mailer
helper service class can be used.
Example
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use App\Infrastructure\Service\Mailer;
// Create email object
$email = new Email();
// Set sender and reply-to address
$email->from(new Address('sender@email.com', 'Sender Name'))
->replyTo(new Address('reply-to@email.com', 'Reply To Name'));
// Set subject
$email->subject('Subject');
// Get body HTML from template password-reset.email.php
$body = $this->mailer->getContentFromTemplate(
'authentication/email/password-reset.email.php',
['user' => $userData, 'queryParams' => $queryParamsWithToken]
);
// Set body
$email->html($body);
// Add recipients and priority
$email->to(new Address($userData->email, $userData->getFullName()))
->priority(Email::PRIORITY_HIGH);
// Send email
$this->mailer->send($email);
Mailer helper service
This mailer helper provides the send()
function which can send an email using the Symfony
mailer and logs the email request.
The logging is part of a security feature
that limits the number of emails a user can send within a certain time frame.
The Mailer
also contains a function to get the rendered HTML from a template.
File: src/Infrastructure/Service/Mailer.php
namespace App\Infrastructure\Service;
use App\Application\Data\UserNetworkSessionData;
use App\Domain\Security\Repository\EmailLoggerRepository;
use Slim\Views\PhpRenderer;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
// Test sender score: https://www.mail-tester.com/
final readonly class Mailer
{
private ?int $loggedInUserId;
public function __construct(
private MailerInterface $mailer,
private PhpRenderer $phpRenderer,
private EmailLoggerRepository $emailLoggerRepository,
private UserNetworkSessionData $userNetworkSessionData
) {
$this->loggedInUserId = $this->userNetworkSessionData->userId ?? null;
}
/**
* Returns rendered HTML of given template path.
* Using PHP-View template parser allows access to the attributes from PhpViewExtensionMiddleware
* like uri and route.
*
* @param string $templatePath PHP-View path relative to template path defined in config
* @param array $templateData ['varName' => 'data', 'otherVarName' => 'otherData', ]
* @return string html email content
*/
public function getContentFromTemplate(string $templatePath, array $templateData): string
{
// Prepare and fetch template
$this->phpRenderer->setLayout(''); // Making sure there is no default layout
foreach ($templateData as $key => $data) {
$this->phpRenderer->addAttribute($key, $data);
}
return $this->phpRenderer->fetch($templatePath);
}
/**
* Send and log email
*
* @param Email $email
* @return void
*/
public function send(Email $email): void
{
$this->mailer->send($email);
$cc = $email->getCc();
$bcc = $email->getBcc();
// Log email request
$this->emailLoggerRepository->logEmailRequest(
$email->getFrom()[0]->getAddress(),
$email->getTo()[0]->getAddress(),
$email->getSubject() ?? '',
$this->loggedInUserId
);
}
}
Error handling
The MailerInterface
's send()
function throws a TransportExceptionInterface
if the email
could not be sent.
If this error should be caught, it can be done so by catching the TransportExceptionInterface
in the Action
class or service that sends the email.
try {
// Send email
$this->mailer->send($email);
} catch (TransportExceptionInterface $transportException) {
// Handle error
}
Testing
Configuration
To prevent the mailer from sending emails during testing, the smtp
adapter
has to be changed to null
in the testing environment config file.
File: config/env.test.php
// ...
// Using the null adapter to prevent emails from actually being sent
$settings['smtp']['type'] = 'null';
Assertions
The Symfony Framework provides a set of mailer assertions, but they're not available outside the framework.
The samuelgfeller/test-traits
MailerTestTrait
provides the same assertions for the Symfony Mailer.
After adding the library as
dev dependency,
the MailerTestTrait
can be used in the test class.
The full list of available assertions can be found in the
vendor/samuelgfeller/test-traits/src/Trait/MailerTestTrait.php
file.
File: tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php
namespace App\Test\Integration\Authentication;
use PHPUnit\Framework\TestCase;
use TestTraits\Trait\MailerTestTrait;
class PasswordForgottenEmailSubmitActionTest extends TestCase
{
// ...
use MailerTestTrait;
public function testPasswordForgottenEmailSubmit(): void
{
// ... Request being sent ...
// Assert that email was sent
$this->assertEmailCount(1);
// Get email RawMessage
$mailerMessage = $this->getMailerMessage();
// Assert email content
$this->assertEmailHtmlBodyContains(
$mailerMessage,
'If you recently requested to reset your password, click the link below to do so.'
);
// Assert email recipient
$this->assertEmailHeaderSame(
$mailerMessage, 'To', $userRow['first_name'] . ' ' . $userRow['surname'] . ' <' . $email . '>'
);
// ...
}