-
-
Notifications
You must be signed in to change notification settings - Fork 6
Mailing
Mailing is the main way the application can communicate with users outside its own interface. It is used to send information or sensitive data such as password reset or email confirmation links after registration.
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 this project.
Mailers can use different "transports" which are methods or protocols used to send emails.
This project uses the most common one "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';
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;
},
];
To create a new email, an object of the class Email
has to be instantiated.
It is a data object that holds the email's 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 Mailer
helper service class is used.
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);
This mailer helper is meant to offer functionalities from the Symfony Mailer
such as the send()
function
but extending it with additional functionalities such as logging the email request and
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/
class Mailer
{
private ?int $loggedInUserId;
public function __construct(
private readonly MailerInterface $mailer,
private readonly PhpRenderer $phpRenderer,
private readonly EmailLoggerRepository $emailLoggerRepository,
private readonly 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
);
}
}
The MailerInterface
's send()
function throws a TransportExceptionInterface
if the email could not be sent.
try {
// Send email
$this->mailer->send($email);
} catch (TransportExceptionInterface $transportException) {
// Handle error
}
The mailer assertions Symfony Mailer
provides are made accessible by the
selective/test-traits
library.
The MailerTestTrait
has to be included to the test class to be able to access the assertions.
File: tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php
namespace App\Test\Integration\Authentication;
use PHPUnit\Framework\TestCase;
use Selective\TestTrait\Traits\MailerTestTrait;
class PasswordForgottenEmailSubmitActionTest extends TestCase
{
// ...
use MailerTestTrait;
public function testPasswordForgottenEmailSubmit(): void
{
// ...
// Assert 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 . '>'
);
// ...
}
Slim app basics
- Composer
- Web Server config and Bootstrapping
- Dependency Injection
- Configuration
- Routing
- Middleware
- Architecture
- Single Responsibility Principle
- Action
- Domain
- Repository and Query Builder
Features
- Logging
- Validation
- Session and Flash
- Authentication
- Authorization
- Translations
- Mailing
- Console commands
- Database migrations
- Error handling
- Security
- API endpoint
- GitHub Actions
- Scrutinizer
- Coding standards fixer
- PHPStan static code analysis
Testing
Frontend
Other