-
-
Notifications
You must be signed in to change notification settings - Fork 3
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 the slim-example-project.
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';
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:[email protected]: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 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.
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('[email protected]', 'Sender Name'))
->replyTo(new Address('[email protected]', '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 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
);
}
}
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
}
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';
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 . '>'
);
// ...
}
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