PHP - - By Younes Rafie

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).

Laravel Logo

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.

Emails 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!

Sponsors
Login or Create Account to Comment
Login Create Account