Sending Notifications with Laravel

Published on by

Sending Notifications with Laravel image

In most applications, we need to send notifications, whether in-app, email or slack - these are typically transactional notifications to alert users of some action or event in your application. Let's take a dive.

The first thing you need to think about when wanting to send notifications to a user is the delivery mechanism. Do we want to send a quick email? Or is this a system-style notification where we want to send data to a slack channel? Or maybe it is an in-app notification because somebody liked the picture of your cat?

Either way, delivery is at the foundation of any notification, and of course, with Laravel, you can deliver to multiple channels from one notification. We do this using the via method on our Notification class itself, specifying an array of channels on which we want to send our notification.

Out of the box, you can send slack, email, and in-app without any additional packages. However, there are many options these days when it comes to notification channels. Luckily, a community-maintained website shows you the options, installation instructions, and how to send notifications through many different channels.

Let's walk through a basic example of sending a Notification. We have a system where people can book meetings, think Calendly or SavvyCal. Whether this is an API, web interaction, or even CLI, we will always want the same result. If somebody books a meeting, we should notify that person, letting them know that the meeting was booked, with a calendar invite attached. We also want to let the person the meeting is booked with know so they can be aware. We might also like to update something on a third-party system to manage availability.

First off, let us generate a notification using our artisan console:

php artisan make:notification UserMeetingBookedNotification --test

Let's break down the command. We want to make a notification that part is clear and fits most artisan commands when generating code. We then provide a name for the Notification itself. You could use namespaces to further the grouping of the notification using either "\" or "/" to separate namespaces. We also tag on the option --test so that Laravel will generate a test for this specific notification; this will be a Feature test. I use pestPHP for my testing framework, so I usually publish the stubs to customize the generated test output.

Once we have generated our notification code, we should focus on what properties must be passed to the constructor (if any) and what channels we want to deliver to. So let's assume we want to accept a name and email to the notification so we can alert the user who has booked a meeting with them.

Let's start building our notification:

declare(strict_types=1);
 
namespace App\Notifications;
 
use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
 
final class UserMeetingBookedNotification extends Notification implements ShouldQueue
{
use Queueable;
 
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly CarbonInterface $datetime,
) {}
}

We can now build our notification to deliver. We need to decide on a channel. For this tutorial, I will focus on the options available by default, but Laravel Notification Channels has a lot of information you can use for alternative channels.

We will use this notification to email the user to let them know they have a meeting booked. So to do so, we need to add the via method and state that we want to use the mail channel.

declare(strict_types=1);
 
namespace App\Notifications;
 
use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
 
final class UserMeetingBookedNotification extends Notification implements ShouldQueue
{
use Queueable;
 
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly CarbonInterface $datetime,
) {}
 
public function via(mixed $notifiable): array
{
return ['mail'];
}
}

Once we have added the via method, we need to describe the Mail Message we will be sending. We do this in the toMail method. The general rule here is that each channel requires a to method, where we prefix the capitalized channel with to.

declare(strict_types=1);
 
namespace App\Notifications;
 
use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
 
final class UserMeetingBookedNotification extends Notification implements ShouldQueue
{
use Queueable;
 
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly CarbonInterface $datetime,
) {}
 
public function via(mixed $notifiable): array
{
return ['mail'];
}
 
public function toMail(mixed $notifiable): MailMessage
{
return (new MailMessage)
->subject('New Booking Received.')
->greeting('Booking received')
->line("{$this->name} has booked a meeting with you.")
->line("This has been booked for {$this->datetime->format('l jS \\of F Y h:i:s A')}")
->line("You can email them on {$this->email} if you need to organise anything.");
}
}

That is it; this will send a nice simple email to the user alerting them how they can get in touch and the fact that they received a booking.

Let us say that we now need to add this to a slack channel, perhaps for tracking reasons. We still want to send the email notification but add a channel.

To start with, we should be sure to test what we have already built. As with all aspects of your projects, you cannot guarantee things will work unless you test them. Let's first write our test around sending an email notification. Then we can make our changes.

it('can send an email notification when a booking is made', function () {
Notification::fake();
 
$user = User::factory()->create();
 
expect(
Bookings::query()->count(),
)->toEqual(0);
 
$action = app()->make(
abstract: CreateNewBookingAction::class,
);
 
$action->handle(
user: $user->id,
name: 'Test User',
email: 'test@email.com',
time: now()->addHours(12),
);
 
expect(
Bookings::query()->count(),
)->toEqual(1);
 
Notification::assertSentTo(
[$user],
UserMeetingBookedNotification::class,
);
});

We are faking the notification driver in Laravel so that the notification isn't routed to anyone. We then create a user we want to notify and resolve our logic from the container - abstracting this logic makes testing easier. We want to make sure that when we make a booking, it is saved, but then we want to ensure that the notification was sent as expected, just never delivered.

We can add another check in our test here to make sure that our test is sending to the correct channel:

it('sends the notification to the correct channels', function () {
Notification::fake();
 
$user = User::factory()->create();
 
$action = app()->make(
abstract: CreateNewBookingAction::class,
);
 
$action->handle(
user: $user->id,
name: 'Test User',
email: 'test@email.com',
time: now()->addHours(12),
);
 
Notification::assertSentTo(
[$user],
UserMeetingBookedNotification::class,
function (mixed $notification, array $channels): bool {
return in_array('mail', $channels);
},
);
});

A similar test to our initial one, but this time we are adding a third argument to our assert sent to call, which has a callable where we want to ensure that we are sending to our mail channel.

Now we are pretty happy with our test coverage. We know that when the action is handled, a notification will be sent and that when it is sent, the channels include the mail channel - which means that our email will be delivered.

Let's get back to adding the next channel: slack. I have hidden the toMail method from the code example below to make it easier to see what is being done:

declare(strict_types=1);
 
namespace App\Notifications;
 
use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
 
final class UserMeetingBookedNotification extends Notification implements ShouldQueue
{
use Queueable;
 
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly CarbonInterface $datetime,
) {}
 
public function via(mixed $notifiable): array
{
return ['mail', 'slack'];
}
 
public function toSlack(mixed $notifiable): SlackMessage
{
return (new SlackMessage)
->success()
->content("{$this->name} just booked a meeting for {$this->datetime->format('l jS \\of F Y h:i:s A')}.");
}
}

For this to work correctly, however, we need to make sure we install a first-party notification package by the laravel team:

composer require laravel/slack-notification-channel

Now that this is done, we can amend our test to ensure that we are also sending it to the slack channel:

it('sends the notification to the correct channels', function () {
Notification::fake();
 
$user = User::factory()->create();
 
$action = app()->make(
abstract: CreateNewBookingAction::class,
);
 
$action->handle(
user: $user->id,
name: 'Test User',
email: 'test@email.com',
time: now()->addHours(12),
);
 
Notification::assertSentTo(
[$user],
UserMeetingBookedNotification::class,
function (mixed $notification, array $channels): bool {
return in_array('mail', $channels)
& in_array('slack', $channels);
},
);
});

There are more tests we can do with our Notifications, but this is an excellent place to start without overcomplicating it. We know that we will send the correct Notification at the right time to the right user on the right channels.

One thing to remember is that Notifications do not have to be queued. These can be sent synchronously as well.

So now we know how to create and test a notification; let's look at sending them.

In the default Laravel application, your User Model implements the Notifiable trait, which allows you to do something like the following:

auth()->user()->notify(new UserMeetingBookedNotification(
name: $request->get('name'),
email: $request->('email'),
));

This is great, but the notifiable trait relies on the model having an email property that is accessible. What do we do when our use case does not quite fit this approach? The Notifiable trait is then not of much use to us. This is where I find the Notification facade to be your best friend. You can use the Notification facade to manually route a notification instead of leaning on the trait itself. Let's look at an example:

Notification::send(['email address one', 'email address two'], new EmailNotification($arguments));

The above example will allow you to programmatically send a notification to an array of email addresses quickly and easily. A slightly cleaner way to do so would be to use the on-demand notifications. Let's say we want to be able to send a notification as part of an artisan command programmatically. You can provide an argument for the channel and programmatically select how to send the notification.

It begins with the Notification facade. Then you tell it to route a notification with a channel and argument.

Notification::route('slack', 'slack-webhook-url')
->notify(new SlackNotification($argument));

The Notification facade is very powerful and flexible, allowing you to send notifications on demand and test them quickly.

How do you use Notifications in your Laravel applications? What is your favorite channel? Let us know on Twitter!

Steve McDougall photo

Technical writer at Laravel News, Developer Advocate at Treblle. API specialist, veteran PHP/Laravel engineer. YouTube livestreamer.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Paragraph

Manage your Laravel app as if it was a CMS – edit any text on any page or in any email without touching Blade or language files.

Visit Paragraph
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
Sort Elements with the Alpine.js Sort Plugin image

Sort Elements with the Alpine.js Sort Plugin

Read article
Anonymous Event Broadcasting in Laravel 11.5 image

Anonymous Event Broadcasting in Laravel 11.5

Read article
Microsoft Clarity Integration for Laravel image

Microsoft Clarity Integration for Laravel

Read article
Apply Dynamic Filters to Eloquent Models with the Filterable Package image

Apply Dynamic Filters to Eloquent Models with the Filterable Package

Read article
Property Hooks Get Closer to Becoming a Reality in PHP 8.4 image

Property Hooks Get Closer to Becoming a Reality in PHP 8.4

Read article
Asserting Exceptions in Laravel Tests image

Asserting Exceptions in Laravel Tests

Read article