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.

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

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

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
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant 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
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

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

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article