Mail Logging in Laravel 5.3: Extending the Mail Driver
This article was peer reviewed by Viraj Khatavkar. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!
One of the many goodies Laravel offers is mailing. You can easily configure and send emails through multiple popular services, and it even includes a logging helper for development.
Mail::send('emails.welcome', ['user' => $user], function ($m) use ($user) {
$m->to($user->email, $user->name)->subject('Welcome to the website');
});
This will send an email to a new registered user on the website using the emails.welcome
view. It got even simpler with Laravel 5.3 using mailables (but the old syntax is still valid).
Here’s an example:
# Generate a new mailable class
php artisan make:mail WelcomeMail
// app/Mail/WelcomeMail.php
class WelcomeUser extends Mailable
{
use Queueable, SerializesModels;
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function build()
{
return $this->view('emails.welcome');
}
}
// routes/web.php
Route::get('/', function () {
$user = User::find(2);
\Mail::to($user->email)->send(new WelcomeUser($user));
return "done";
});
Laravel also provides a good starting point for sending mails during the development phase using the log
driver, and in production using smtp
, sparkpost
, mailgun
, etc. This seems fine in most cases, but it can’t cover all the available services! In this tutorial, we’re going to learn how to extend the existing mail driver system to add our own.
To keep our example simple and straightforward, we’re going to log our emails to a DB table.
Creating a Service Provider
The preferred way to accomplish this is by creating a service provider to interact with our application at boot time and register our services. Let’s start by generating a new service provider using the artisan command line helper.
php artisan make:provider DBMailProvider
This will create a new class inside our app/Providers
folder. If you’re familiar with Laravel service providers, you’ll know that we extend the ServiceProvider
class and define the boot
and register
methods. You can read more about providers in the documentation.
Using the Mail Provider
Instead of using the parent service provider class, we can take a shortcut and extend the existing Illuminate\Mail\MailServiceProvider
one. This means that the register
method is already implemented.
// vendor/Illuminate/Mail/MailServiceProvider.php
public function register()
{
$this->registerSwiftMailer();
// ...
}
The registerSwiftMailer
method will return the appropriate transport driver based on the mail.driver
config value. What we can do here is perform the check before calling the registerSwiftMailer
parent method and return our own transport manager.
// app/Providers/DBMailProvider.php
function registerSwiftMailer()
{
if ($this->app['config']['mail.driver'] == 'db') {
$this->registerDBSwiftMailer();
} else {
parent::registerSwiftMailer();
}
}
Using the Transport Manager
Laravel resolves the swift.mailer
instance from the IOC, which should return a Swift_Mailer
instance of SwiftMailer. We need to bind our Swift mailer instance to the container.
private function registerDBSwiftMailer()
{
$this->app['swift.mailer'] = $this->app->share(function ($app) {
return new \Swift_Mailer(new DBTransport());
});
}
You can think of the transport object as the actual driver. If you check the Illuminate\Mail\Transport
namespace, you’ll find the different transport classes for every driver (like: LogTransport
, SparkPostTransport
, etc.).
The Swift_Mailer
class requires a Swift_Transport
instance, which we can satisfy by extending the Illuminate\Mail\Transport\Transport
class. It should look something like this.
namespace App\Mail\Transport;
use Illuminate\Mail\Transport\Transport;
use Illuminate\Support\Facades\Log;
use Swift_Mime_Message;
use App\Emails;
class DBTransport extends Transport
{
public function __construct()
{
}
public function send(Swift_Mime_Message $message, &$failedRecipients = null)
{
Emails::create([
'body' => $message->getBody(),
'to' => implode(',', array_keys($message->getTo())),
'subject' => $message->getSubject()
]);
}
}
The only method we should implement here is the send
one. It’s responsible for the mail-sending logic, and in this case, it should log our emails to the database. As for our constructor, we can leave it empty for the moment because we don’t need any external dependencies.
The $message->getTo()
method always returns an associative array of recipients’ emails and names. We get the list of emails using the array_keys
function and then implode them to get a string.
Log Email to DB
The next step is to create the necessary migration for our database table.
php artisan make:migration create_emails_table --create="emails"
class CreateEmailsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('emails', function (Blueprint $table) {
$table->increments('id');
$table->text('body');
$table->string('to');
$table->string('subject');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('emails');
}
}
Our migration only contains the email body, subject and recipient email, but you can add as many details as you want. Check the Swift_Mime_Message
class definition to see the list of available fields.
Now, we need to create a new model to interact with our table, and add the necessary fields to the fillable array.
php artisan make:model Emails
class Emails extends Model
{
protected $fillable = [
'body',
'to',
'subject'
];
}
Sending Emails
Ok, it’s time to test what we’ve got so far. We start by adding our provider to the list of providers inside the config/app.php
file.
return [
// ...
'providers' => [
// ...
App\Providers\DBMailProvider::class,
// ...
],
// ...
];
Then we register our mail driver to db
inside the config/mail.php
file.
return [
'driver' => 'db',
// ...
];
The only remaining part is to send a test email and check if it gets logged to the database. I’m going to send an email when the home URL is hit. Here’s the code.
// routes/web.php
Route::get('/', function () {
$user = User::find(2);
Mail::send('emails.welcome', ['user' => $user], function ($m) use ($user) {
$m->to($user->email, $user->name)->subject('Welcome to the website');
});
return "done";
});
After hitting the home route, we can run the php artisan tinker
command to check the emails table records.
Conclusion
In this article, we saw how to extend the mail driver system to intercept emails for debugging purposes. One of the things I admire in Laravel is its unparalleled extensibility: you can alter or extend almost any functionality from router and IOC, to Mail and beyond.
If you have any questions or comments, be sure to post them below and I’ll do my best to answer them!