New laravel dynamic emails blog post

Closes: #6
This commit is contained in:
Aloïs Micard 2021-11-09 19:31:40 +01:00
parent c0c642c2cf
commit 713e7d276d
2 changed files with 280 additions and 0 deletions

View file

@ -0,0 +1,280 @@
+++
title = "Laravel dynamic SMTP mail configuration"
date = "2021-11-09"
author = "Aloïs Micard"
authorTwitter = "" #do not include @ cover = ""
tags = ["Laravel", "PHP", "Tutorial"]
keywords = ["Laravel", "SMTP", "Dynamic", "PHP", "Tutorial"]
description = "How to use dynamic SMTP credentials with Laravel."
showFullContent = false
+++
Hello friend...
It has been a while.
I have been very busy lately with work, open source and *life* that I didn't find the energy to write a blog post.
Despite having some good ideas, I wasn't really in the mood.
Hopefully, I now have the energy and the subject to make a good blog post: let's talk
about [Laravel](https://laravel.com/) and emails!
# 1. Laravel and SMTP
## 1.1. Configuration
Laravel SMTP [Mail](https://laravel.com/docs/8.x/mail) support is truly awesome and work out-of-the-box without
requiring anything more than a few env variables:
```text
MAIL_MAILER=smtp
MAIL_HOST=mail1.example.org
MAIL_PORT=587
MAIL_USERNAME=demo@example.org
MAIL_PASSWORD=foo
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@example.org
MAIL_FROM_NAME=Demo App
```
> Note: The initial setup only requires these environment variables because the smtp mailer is already configured in `config/mail.php`.
## 1.2. Creating an email
Once everything is configured, creating an email is as simple as running this command:
```shell
php artisan make:mail Greetings
```
This command will generate a sample email in `app/Mail/Greetings` and you'll just need to design it afterwards.
## 1.3. Sending an email
Sending an email to a user with Laravel can be either done:
- using `\Illuminate\Notifications\RoutesNotifications::notify`:
```php
$user->notify(new Greetings());
```
- or using `\Illuminate\Support\Facades\Notification::send`:
```php
Notification::send($users, new Greetings());
```
> Note: the later syntax is especially useful when bulk sending emails.
See how simple it is? ~~I wonder what would be the limitations.~~
Eh! What if we need to 'whitelabelize' our application. :-)
---
# 2. Dynamic SMTP configuration
In our scenario, we have the need to [whitelabelize](https://en.wikipedia.org/wiki/White-label_product) our application:
each `User` will belongs to a `Provider` that will have custom SMTP settings. So when sending email to a user we need to
configure dynamically the SMTP credentials to use depending on the `$user->provider`.
**Can Laravel help us doing so?**
After a bit of googling and reading the official documentation, there's no out-of-the-box support for dynamic SMTP
configuration, certainly because there would be 100x way of doing it, each way depending on your exact use-case.
**So, we're screwed?**
Not yet, because Laravel allows us to tweak almost **anything**, so we just need to find our way.
## 2.1. Designing the models
Here's a quick visualization of our models:
```php
/**
* @property Provider $provider
*/
class User extends Model
{
public function provider(): BelongsTo
{
return $this->belongsTo(Provider::class);
}
}
```
For the `User` model there's nothing special: we only link the user to a `Provider`.
```php
/**
* @property array $mail_configuration
* @property Provider $provider
*/
class Provider
{
protected $casts = [
'mail_configuration' => 'encrypted:array'
];
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}
```
The `Provider` model has many `Users` and has a `mail_configuration` field which is encrypted and that will contain all
the SMTP credentials.
## 2.2. Digging down the internals
Now that our models are ready, we must find a way to use the provider configuration to send the email. Let's dig down
in Laravel source code to understand how emails works:
Remember the two-ways of sending emails?
- `\Illuminate\Notifications\RoutesNotifications::notify`
- `\Illuminate\Support\Facades\Notification::send`
What we need to do is find the common path between these two methods, and see if we can override some
behavior in there.
![](/img/laravel-mail-internals.png)
As you can see here, the methods share the same execution path that end up
calling `\Illuminate\Notifications\Channels\MailChannel::send`. So how can we hook up into this path?
The answer lies in `\Illuminate\Notifications\NotificationSender::sendToNotifiable`:
```php
protected function sendToNotifiable($notifiable, $id, $notification, $channel)
{
if (! $notification->id) {
$notification->id = $id;
}
if (! $this->shouldSendNotification($notifiable, $notification, $channel)) {
return;
}
$response = $this->manager->driver($channel)->send($notifiable, $notification);
$this->events->dispatch(
new NotificationSent($notifiable, $notification, $channel, $response)
);
}
```
As you can see this method is looking for a driver to use with the `$channel` and finally calls the `send()`
method. So what we can do is registering a special SMTP driver that will use dynamic SMTP settings. Fortunately
registering a custom driver is a common use-case and there's a straightforward way.
## 2.3. Creating a custom MailChannel
```php
class ProviderMailChannel extends MailChannel
{
public function send($notifiable, Notification $notification)
{
// TODO: override the SMTP configuration
parent::send($notifiable, $notification);
}
}
```
## 2.4. Registering the ProviderMailChannel
All we need to know is extend (i.e: register a custom driver creator) for the `mail` channel.
This way when an email is sent it will be sent using the `ProviderMailChannel`.
```php
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
/** @var ChannelManager $channelManager */
$channelManager = $this->app->get(ChannelManager::class);
$channelManager->extend('mail', function (Application $application) {
return new ProviderMailChannel($application->get('mail.manager'), $application->get(Markdown::class));
});
}
}
```
## 2.5. Creating a custom Mailer
Now that we are hooked up into the mail sending flow, we need to actually send the email. For doing so we need to instantiate
custom `\Illuminate\Mail\Mailer` instance that will be configured using the provider settings. To register such dynamic
configurable service we will use the power of
the [Service container](https://laravel.com/docs/8.x/container#binding-basics).
```php
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// When running in testing environment then return the mocked mail implementation
if (env('APP_ENV') === 'testing') {
$this->app->singleton('custom.mailer', MailFake::class);
return;
}
// Register a custom mailer named `custom.mailer` that will receive his configuration dynamically
$this->app->bind('custom.mailer', function ($app, $parameters) {
$transport = new Swift_SmtpTransport($parameters['host'], $parameters['port']);
$transport->setUsername($parameters['username']);
$transport->setPassword($parameters['password']);
$transport->setEncryption($parameters['encryption']);
$mailer = new Mailer('', $app->get('view'), new Swift_Mailer($transport), $app->get('events'));
$mailer->alwaysFrom($from_address, $from_name);
return $mailer;
});
}
public function boot()
{
/** @var ChannelManager $channelManager */
$channelManager = $this->app->get(ChannelManager::class);
$channelManager->extend('mail', function (Application $application) {
return new ProviderMailChannel($application->get('mail.manager'), $application->get(Markdown::class));
});
}
}
```
Now we can instantiate this custom Mailer by doing the following:
```php
$mailer = app()->make('custom.mailer', $configuration);
// do something with $mailer
```
Where `$configuration` is the custom SMTP configuration.
## 2.6. Plug the custom Mailer into the ProviderMailChannel
Finally, all we need to do is to read the `Provider` mail configuration
and use it to instantiate our `custom.mailer` and then, use it to send the actual email.
```php
class ProviderMailChannel extends MailChannel
{
public function send($notifiable, Notification $notification)
{
$message = $notification->toMail($notifiable);
$mailer = app()->make('custom.mailer', $notifiable->provider->mail_configuration);
$message->send($mailer);
}
}
```
# 3. Conclusion
That all folks. You are now capable of sending email using dynamic SMTP credentials based on the use-case.
Happy hacking!

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB