Laravel Mail and Notifications Reference

A Laravel mail and notifications reference: sending email with Mailables, and multi-channel alerts with Notifications.

Mailables

A Mailable is a class representing one email — its subject, view, and data — kept separate from wherever it gets triggered. This site's own account-lockout email works exactly this way.

php artisan make:mail OrderShipped
class OrderShipped extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public Order $order,
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: 'Your order has shipped');
    }

    public function content(): Content
    {
        return new Content(
            view: 'emails.orders.shipped',
            with: ['order' => $this->order],
        );
    }

    public function attachments(): array
    {
        return [
            Attachment::fromPath(storage_path('invoices/1.pdf')),
        ];
    }
}

Sending mail

use Illuminate\Support\Facades\Mail;

Mail::to($user)->send(new OrderShipped($order));

Mail::to($user)
    ->cc($manager)
    ->bcc('audit@example.com')
    ->send(new OrderShipped($order));

Queueing mail

Sending mail makes a real network call to your mail provider — queue it so the request/response cycle doesn't wait on that.

class OrderShipped extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;
    // ...
}

// or queue an already-built mailable ad hoc
Mail::to($user)->queue(new OrderShipped($order));

Notifications

Notifications generalize the idea further: one alert, sent down one or more channels (mail, database, Slack, SMS via a package) at once.

php artisan make:notification InvoicePaid
class InvoicePaid extends Notification
{
    use Queueable;

    public function __construct(
        public Invoice $invoice,
    ) {}

    public function via(object $notifiable): array
    {
        return ['mail', 'database'];
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Invoice Paid')
            ->line("Invoice #{$this->invoice->id} has been paid.")
            ->action('View Invoice', route('invoices.show', $this->invoice));
    }

    public function toArray(object $notifiable): array
    {
        return ['invoice_id' => $this->invoice->id];
    }
}
$user->notify(new InvoicePaid($invoice));

// Notify anyone, even without a User model (e.g. a raw email/Slack webhook)
Notification::route('mail', 'ops@example.com')
    ->notify(new InvoicePaid($invoice));

Reading database notifications

// Add the notifications() relation via the Notifiable trait (already on User)
$user->unreadNotifications;
$user->notifications()->latest()->take(10)->get();

$notification->markAsRead();

The database channel needs its own table — generate it once with php artisan make:notifications-table and migrate. Unlike mail, database notifications are cheap to write, which makes them a natural fit for an in-app bell icon or activity feed alongside (or instead of) an email.

Markdown mail

Rather than hand-writing HTML for every Mailable, Laravel ships a set of pre-styled Markdown components — buttons, panels, tables — so a professional-looking email can be written mostly as plain text:

php artisan make:mail OrderShipped --markdown=emails.orders.shipped

{{-- resources/views/emails/orders/shipped.blade.php --}}
@component('mail::message')
# Order Shipped

Your order #{{ $order->id }} is on its way.

@component('mail::button', ['url' => $trackingUrl])
Track Order
@endcomponent

Thanks,
{{ config('app.name') }} @endcomponent

Because it renders both HTML and plain-text automatically, Markdown mail is usually the better starting point over a plain Blade view unless the design needs something the components can't express. Run php artisan vendor:publish --tag=laravel-mail to copy the underlying templates into the app if the default styling needs changing.

Notification channels beyond mail

The via() method isn't limited to mail and database. First-party and community packages add channels such as nexmo/vonage for SMS, slack for webhook messages, and broadcast for pushing a notification over WebSockets to a listening frontend. Each channel just needs a matching to{Channel}() method on the notification:

public function via(object $notifiable): array
{
    return ['mail', 'slack', 'broadcast'];
}

public function toSlack(object $notifiable): SlackMessage
{
    return (new SlackMessage)
        ->content("Invoice #{$this->invoice->id} has been paid.");
}

public function toBroadcast(object $notifiable): BroadcastMessage
{
    return new BroadcastMessage(['invoice_id' => $this->invoice->id]);
}

Different notifiable models can also route the same notification differently — a User might prefer Slack while an external contact only has an email address on file — by defining routeNotificationForMail() etc. on the model itself.

Testing mail and notifications

Mail::fake() and Notification::fake() swap out the real senders in tests, so an assertion can confirm the right thing was sent without actually delivering anything or hitting a real mail provider:

Mail::fake();

// ... code under test that sends mail ...

Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
    return $mail->order->id === $order->id;
});

Notification::fake();

Notification::assertSentTo($user, InvoicePaid::class);

Faking is preferable to letting tests send real email even in a local/testing mail driver, since it also verifies the correct Mailable or Notification class was dispatched with the expected data, rather than just that something happened.