Email in Laravel with Mailable & Notification classes (and how to test them in Dusk!)

mailslurp

MailSlurp

Posted on June 14, 2023

Email in Laravel with Mailable & Notification classes (and how to test them in Dusk!)

Laravel is a very popular PHP framework with the built-in ability to send email. This tutorial demonstrates how to use two different email techniques in Laravel to send mail: Mailable and Notification. It then shows how to test that emails are sent correctly using Dusk and MailSlurp fake mail catchers.

Tutorial basics

To illustrate email sending in Laravel let us create a demo app that allows users to sign up for a newsletter using a form and select to be emailed using Mailable or Notification techniques. We will then write Dusk browser tests using Chrome and MailSlurp disposable email addresses to capture the outbound email and verify the results. This is what our project will look like:

email form

When the user visits our site they will be given the choice to sign up to receive an email newsletter. For demonstration purposes we'll make two implementations, one using Mailable and another using Notifications. Full code can be found on the github examples repository.

Notifcations vs Mailable

Laravel provides two primary ways to send notifications to users: Mailables and Notifications. Here's a brief comparison:

Aspect Mailables Notifications
Purpose Designed specifically for email communication Designed for quick, simple messages that can be sent via various channels
Customization Provides flexibility for complex email layouts and designs Offers customization focused on the message content rather than design
Delivery channels Restricted to the email channel Supports multiple channels, including mail, database, broadcast, SMS (via Nexmo), and Slack
Queue Can be queued for background sending Can also be queued for background sending
Complexity Might be a bit more complex to set up initially, especially with complex designs Typically simpler to set up, particularly if you're using multiple channels

Notifications and Mailables both have their uses. In this post we will show to how use and test both of them.

Setting up a project

Let's assume you have an existing Laravel project with composer. If not create one with:

php composer.phar create-project laravel/laravel php-laravel-phpunit
Enter fullscreen mode Exit fullscreen mode

Then add the MailSlurp library so we can use email addresses. You need an API Key so create a free account to obtain one:

php composer.phar require --dev mailslurp/mailslurp-client-php
Enter fullscreen mode Exit fullscreen mode

Then configure dusk to enable browser testing:

php composer.phar require --dev laravel/dusk
php artisan dusk:install
php artisan dusk:chrome-driver --detect
Enter fullscreen mode Exit fullscreen mode

We can add a basic welcome page with a link to our two different sign up methods (Mailable and Notification):

welcome page

The buttons link to /newsletter and /notification respectively.

Configure mail settings

Before we can send emails we need to configure PHP mail settings. Laravel sends email using an external SMTP mailserver. We can configure this inside .env or config/mail.php.

Using .env

If you have created an inbox in MailSlurp then you can use the SMTP access details provided in the MailSlurp dashboard to configure the SMTP credentials inside .env

MAIL_MAILER=smtp
MAIL_HOST=mailslurp.mx
MAIL_PORT=2587
MAIL_USERNAME=your-smtp-username
MAIL_PASSWORD=your-smtp-password
MAIL_FROM_ADDRESS="your-inbox@mailslurp.com"
Enter fullscreen mode Exit fullscreen mode

The disadvantage of this approach is that the settings are static. For this example we will use config/mail.php instead which allows dynamic configuration.

Using config/mail.php

For this example we want to use create and use a new MailSlurp inbox for sending within the app. We can use the MailSlurp API client inside mail.php to configure these settings:

// configure mailslurp client
$config = MailSlurp\Configuration::getDefaultConfiguration()->setApiKey('x-api-key', $MAILSLURP_API_KEY);

// create an inbox to send emails from
$inboxController = new MailSlurp\Apis\InboxControllerApi(null, $config);
$senderInbox = $inboxController->createInboxWithOptions(new \MailSlurp\Models\CreateInboxDto(['inbox_type' => 'SMTP_INBOX', 'name' => 'Newsletters']));
$accessOptions = $inboxController->getImapSmtpAccess($senderInbox->getId());

// get access to the inbox
$host = $accessOptions->getSecureSmtpServerHost();
$port = $accessOptions->getSecureSmtpServerPort();
$username = $accessOptions->getSecureSmtpUsername();
$password = $accessOptions->getSecureSmtpPassword();

// configure laravel mailer settings to use our sender inbox
// for production apps set this in .env instead with static values
// make sure you run `API_KEY=$(API_KEY) php artisan config:cache` after setting
return [
    'default' => 'smtp',
    'mailers' => [
        'smtp' => [
            'transport' => 'smtp',
            'url' => env('MAIL_URL'),
            'host' => $host,
            'port' => $port,
            'encryption' => 'tls',
            'username' => $username,
            'password' => $password,
            'timeout' => null,
            'local_domain' => env('MAIL_EHLO_DOMAIN'),
        ],
    ],
    'from' => [
        'address' => $senderInbox->getEmailAddress(),
        'name' => $senderInbox->getName(),
    ],
    'markdown' => [
        'theme' => 'default',
        'paths' => [
            resource_path('views/vendor/mail'),
        ],
    ],
];
Enter fullscreen mode Exit fullscreen mode

Note that each time you change the config you need to run:

php artisan config:cache
Enter fullscreen mode Exit fullscreen mode

Create a Mailable

To send email using Mailables we can scaffold a class:

php artisan make:mail Newsletter
Enter fullscreen mode Exit fullscreen mode

Inside the class we define which view we will use:

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class Newsletter extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Get the message envelope.
     */
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Newsletter',
        );
    }

    /**
     * Get the message content definition.
     */
    public function content(): Content
    {
        return new Content(
            view: 'emails.newsletter',
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

For the view we used emails.newsletter - this means we need to define a blade template view in resources/views/emails/newsletter:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Welcome Email</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body>
<h1>Welcome to our newsletter!</h1>
<p>We are glad you have decided to join us.</p>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

This view will be used as the email body when we send using the Mailable class.

Define a Notification

For the Notification approach we can do something similar using Notification classes

php artisan make:notification NewsletterNotification
Enter fullscreen mode Exit fullscreen mode

The class looks like so:

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class NewsletterNotification extends Notification
{
    use Queueable;

    public function __construct()
    {
    }

    public function via(object $notifiable): array
    {
        // use mail to send
        return ['mail'];
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->line('Welcome to our notifications!')
            ->line('We are glad you have decided to use notifications.');
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice here we don't use a view. This is because laravel will style our notifications for us. This is a major difference between Mailables and Notifications.

Create controller routes

We want users to sign up with email addresses for a fake newsletter. Let's use artisan to create controllers and routes for each method:

php artisan make:controller NewsLetter
php artisan make:controller Notification
Enter fullscreen mode Exit fullscreen mode

We will show a form on each page so let's wire up the routes:

Route::get('/newsletter', [NewsletterController::class, 'create']);
Route::post('/newsletter', [NewsletterController::class, 'store']);
Route::get('/notification', [NotificationController::class, 'create']);
Route::post('/notification', [NotificationController::class, 'store']);
Enter fullscreen mode Exit fullscreen mode

Define controllers

So we have defined our routes and mail config. Now let's create a controller for each method.

Mailable controller

For the mailable use we need a form with an email input like this:

email form

Let's define the view:

@extends('layout', ['logo' => '/newsletter.svg'])
@section('content')
<div class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent
dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
    <div>
        <h2 class="text-xl font-semibold text-gray-900 dark:text-white">Enter your newsletter details</h2>

        <p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
            We will email you every Sunday with the latest Laravel news.
        </p>
        <div class="mt-4">
            <form class="flex flex-col gap-4" method="POST" action="/newsletter" data-test-id="newsletter-form">
                @csrf
                <label>
                    <input type="email" id="email" name="email" required placeholder="Your email address..." class="appearance-none rounded p-2 w-full">
                </label>
                <button type="submit" id="submit" class="rounded bg-red-500 text-white p-2 px-4">Sign up</button>
            </form>
        </div>
    </div>
</div>
@endsection
Enter fullscreen mode Exit fullscreen mode

The important part here is the form and input. We also need a view for when the form is successfully submitted:

@extends('layout', ['logo' => '/success.svg'])
@section('content')
<div class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent
dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
    <div>
        <h2 class="text-xl font-semibold text-gray-900 dark:text-white">Thanks!</h2>

        <p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed" data-test-id="newsletter-success">
            We have saved your email address <span class="font-semibold" data-test-id="email-result">{{$email}}</span>
        </p>
    </div>
</div>
@endsection
Enter fullscreen mode Exit fullscreen mode

This will thank the user after submission. Next we need to wire up the views with our controller and configure the Mailable call:

namespace App\Http\Controllers;

use App\Mail\Newsletter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;

class NewsletterController extends Controller
{
    public function create()
    {
        return view('newsletter');
    }

    public function store(Request $request)
    {
        // get the email from the form submission
        $email = $request->validate(['email' => 'required|email']);

        // send an email to the user using the Newsletter Mailable
        Mail::to($email['email'])->send(new Newsletter($email['email']));

        return view('newsletter-success', ['email' => $email['email']]);
    }
}
Enter fullscreen mode Exit fullscreen mode

This configuration allows the user to submit the newsletter form and be emailed using a Mailable. After that we render the success form.

email form

Notification controller

We can also do the same using Notifications. Here we need a similar form with views and controller:

@extends('layout', ['logo' => '/notification.svg'])
@section('content')
<div class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent
dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
    <div>
        <h2 class="text-xl font-semibold text-gray-900 dark:text-white">Enter your notification details</h2>

        <p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
            We will notify you whenever something exciting happens
        </p>
        <div class="mt-4">
            <form class="flex flex-col gap-4" method="POST" action="/notification" data-test-id="notification-form">
                @csrf
                <label>
                    <input type="email" id="email" name="email" required placeholder="Your email address..." class="appearance-none rounded p-2 w-full">
                </label>
                <button type="submit" id="submit" class="rounded bg-red-500 text-white p-2 px-4">Enable notifications</button>
            </form>
        </div>
    </div>
</div>
@endsection
Enter fullscreen mode Exit fullscreen mode

The success page will look like this:

@extends('layout', ['logo' => '/success.svg'])
@section('content')
<div class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent
dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
    <div>
        <h2 class="text-xl font-semibold text-gray-900 dark:text-white">Thanks!</h2>

        <p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed" data-test-id="notification-success">
            We have saved your email address <span class="font-semibold" data-test-id="email-result">{{$email}}</span>
        </p>
    </div>
</div>
@endsection
Enter fullscreen mode Exit fullscreen mode

Next we configure the views inside the Notification controller and use the NewsletterNotification class to email the user:

namespace App\Http\Controllers;

use App\Notifications\NewsletterNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;

class NotificationController extends Controller
{
    public function create()
    {
        return view('notification');
    }

    public function store(Request $request)
    {

        // get the email from the form submission
        $email = $request->validate(['email' => 'required|email']);

        // send an email to the user using the Newsletter Notification
        Notification::route('mail', $email['email'])
            ->notify(new NewsletterNotification());

        return view('notification-success', ['email' => $email['email']]);
    }
}
Enter fullscreen mode Exit fullscreen mode

This configures views for submission and success:

email form

Testing the application with Dusk

So now we have created routes, views, and controllers for Mailable and Notification methods. Next we want to define two browser tests that will:

  • Load the welcome page in a browser
  • Click on each link
  • Create a disposable email address
  • Submit the email address for newsletter sign-up
  • Verify that the email is sent and received

What is Dusk?

The most common way to test Laravel applications end-to-end with real browsers is Dusk. Dusk uses Chromedriver to instantiate a headless chrome browser and control it remotely. You can define tests in PHP and assert that our application is functioning correctly.

How does MailSlurp integrate?

The reason for using MailSlurp is that it lets us create throwaway email accounts during tests and use them to sign up and receive emails. We can also use the MailSlurp PHP client to wait for the sent emails and fetch them for verification.

Create Dusk tests

public function testNewsletterMailable(): void
{

    $this->browse(function (Browser $browser) {
        $MAILSLURP_API_KEY = env('API_KEY');
        // configure mailslurp client
        $config = MailSlurp\Configuration::getDefaultConfiguration()->setApiKey('x-api-key', $MAILSLURP_API_KEY);
        $inboxController = new MailSlurp\Apis\InboxControllerApi(null, $config);

        // create a disposable email address
        $inbox = $inboxController->createInboxWithDefaults();

        // load the app in the browser
        $browser->resize(1440, 900);
        $browser->visit('http://localhost:8000/')
            ->assertSee('Sign up for our newsletter');
        $browser->screenshot('welcome');

        // click the newsletter link
        $browser->click('[data-test-id="newsletter-link"]');
        $browser->waitFor('[data-test-id="newsletter-form"]');
        $browser->screenshot('newsletter-form');

        // fill the newsletter sign up form with the disposable email address
        $browser->type('#email', $inbox->getEmailAddress());
        $browser->screenshot('newsletter-form-filled');
        $browser->click('#submit');

        // submit the form and see a success message
        $browser->waitFor('[data-test-id="newsletter-success"]');
        $browser->screenshot('newsletter-success');

        // now use MailSlurp to await the email sent by our NewsletterController
        $waitForController = new MailSlurp\Apis\WaitForControllerApi(null, $config);
        $email = $waitForController->waitForLatestEmail($inbox->getId(), 60_000, true);
        assert($email->getSubject() === 'Welcome to our newsletter');

        // get email content
        $browser->resize(800, 400);
        $emailController = new MailSlurp\Apis\EmailControllerApi(null, $config);
        $previewUrls = $emailController->getEmailPreviewURLs($email->getId());
        $browser->visit($previewUrls->getPlainHtmlBodyUrl());
        $browser->screenshot('newsletter-mailable-preview');
    });
}
Enter fullscreen mode Exit fullscreen mode

Run integration tests

To run the tests we first need to run the Laravel application php artisan serve in a separate terminal. Then execute API_KEY=$(API_KEY) php artisan dusk.

The test will load the application, create an email address, fill out the form, then wait for the email to arrive:

notification form filled

We then use the getEmailPreviewURLs method to fetch the render email and view it in our browser:

notification rendered

Notice how the notification is rendered using a Laravel built-in template. The Mailable on the other hand uses only our own blade view:

mailable rendered

Conclusion

Woah! That was a lot of code but we also achieved something remarkable: we sent emails using both Mailable and Notification classes plus we tested it using Dusk integration tests and MailSlurp disposable email addresses. You can do the same and test your application end to end with real email (and SMS).

Links and more

💖 💪 🙅 🚩
mailslurp
MailSlurp

Posted on June 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related