Notifications look simple at first—until your app grows. You start by sending a quick email from a controller, but soon you need more:
- Email and in-app notifications.
- Different channels depending on user preferences.
- Queued delivery so the UI stays fast.
- Clean tests that don't actually hit your SMTP server.
That is exactly where Laravel Notifications shine. They provide a structured way to send short, event-driven messages across multiple channels using a single class.
Laravel gives you a clean, structured way to send short, event-driven messages (like Order shipped, Invoice paid, New comment, Password changed) across multiple channels with one notification class.
In this article, I’ll show a practical setup you can reuse in real projects.
Why use Notifications instead of sending mail directly ?
While you could call Mail::to()->send() everywhere, notifications offer a better architecture:
- Centralized logic: One place to define delivery channels via the
via()method. - Multi-channel support: Easily switch between or combine mail, database, Slack, and SMS.
- Built-in Queues: Native support for background processing.
- Observability: Built-in support for testing with
Notification::fake().
This makes your code easier to maintain when your app evolves.
A real example: order shipped
Let’s say you have an e-commerce app and want to notify a user when an order is shipped.
We’ll send:
- an email notification
- an in-app notification stored in the database
Step 1: Generate a notification
php artisan make:notification OrderShipped
Laravel creates a class in app/Notifications/OrderShipped.php.
Step 2: Build the notification class
Here is a practical version:
<?php
namespace App\Notifications;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public Order $order
) {}
/**
* Decide which channels to use.
*/
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
/**
* Email version.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Your order has been shipped')
->greeting('Hello ' . $notifiable->name . ' 👋')
->line("Good news! Your order #{$this->order->id} has been shipped.")
->action('Track my order', url("/orders/{$this->order->id}"))
->line('Thank you for your purchase.');
}
/**
* Database version (stored as JSON in notifications table).
*/
public function toArray(object $notifiable): array
{
return [
'order_id' => $this->order->id,
'status' => 'shipped',
'message' => "Your order #{$this->order->id} has been shipped.",
'url' => "/orders/{$this->order->id}",
];
}
}
Why this is clean
-
via()defines the channels in one place -
toMail()handles only the email message -
toArray()formats the in-app payload -
ShouldQueue+Queueablekeeps delivery async and your request fast
Step 3: Send the notification
If your User model uses Laravel’s default Notifiable trait (it usually does), sending is straightforward:
$user->notify(new OrderShipped($order));
You can also send to multiple users:
use Illuminate\Support\Facades\Notification;
Notification::send($admins, new OrderShipped($order));
This is super useful for admin alerts, moderation events, or internal ops notifications.
Step 4: Enable database notifications
If you want in-app notifications (bell icon, notification list, unread count), create the notifications table:
php artisan make:notifications-table
php artisan migrate
Laravel stores notification payloads in a notifications table (including the type, JSON data, and read_at status).
Step 5: Display notifications in your app
Because your model uses Notifiable, you get relationships out of the box:
$user->notifications$user->unreadNotifications$user->readNotifications
Example in a controller:
public function index(Request $request)
{
$user = $request->user();
return response()->json([
'unread_count' => $user->unreadNotifications()->count(),
'items' => $user->notifications()->latest()->limit(20)->get(),
]);
}
This is enough to build a simple notification center in Vue / React / Blade.
Step 6: Mark notifications as read
A common pattern: when a user opens the notification list, mark unread notifications as read.
public function markAllAsRead(Request $request)
{
$request->user()->unreadNotifications->markAsRead();
return response()->json(['message' => 'Notifications marked as read']);
}
For large volumes, you can also use a direct query update (more efficient than looping in memory).
Step 7: Keep your app fast with queues
Notifications often call external services (SMTP, Slack, SMS providers), so they should usually be queued.
We already added:
implements ShouldQueueuse Queueable
Now make sure your queue is configured and a worker is running (Redis is a great choice in production).
Example local setup:
QUEUE_CONNECTION=database
Then run:
php artisan queue:work
Why queueing matters
Without queues:
- user clicks a button
- request waits for email/SMS API
- response feels slower
With queues:
- notification job is pushed to the queue
- request returns quickly
- worker sends the notification in the background
This becomes a big win as soon as you have real traffic.
Step 8: Test notifications cleanly
One of Laravel’s best features here is Notification::fake().
It lets you test behavior without sending anything.
use App\Models\Order;
use App\Models\User;
use App\Notifications\OrderShipped;
use Illuminate\Support\Facades\Notification;
it('notifies the user when an order is shipped', function () {
Notification::fake();
$user = User::factory()->create();
$order = Order::factory()->create(['user_id' => $user->id]);
// Your application logic...
$user->notify(new OrderShipped($order));
Notification::assertSentTo($user, OrderShipped::class);
});
This keeps tests fast, reliable, and focused on your business logic.
A practical pattern I like
In real projects, I usually trigger notifications after a state change in a service/action class.
Example:
class ShipOrderAction
{
public function handle(Order $order): void
{
$order->update(['status' => 'shipped']);
$order->user->notify(new OrderShipped($order));
}
}
Why this works well:
- controller stays thin
- notification logic is tied to the domain action
- easy to test
- easy to reuse from HTTP, CLI, or jobs
Common mistakes to avoid
1) Sending notifications directly in controllers everywhere
It works at first, but it spreads your logic across the app.
2) Forgetting to queue notifications
Mail and third-party channels can slow down your requests a lot.
3) Overloading notifications with business logic
Keep notifications focused on delivery + presentation.
Your business rules should live in services/actions.
4) Mixing database and broadcast payloads without thinking
If your database payload and realtime frontend payload need to differ, define toDatabase() and toBroadcast() separately instead of relying on one toArray() for both.
Bonus ideas for production projects
Once your base setup is working, Laravel Notifications scale really well:
- Admin alerts (new signup, failed payment, suspicious login)
- User activity (comment replies, mentions, reminders)
- Subscription events (trial ending, invoice paid, renewal failed)
- Realtime UI notifications with broadcasting
- Channel preferences (email only, app only, both)
You can even make the via() method dynamic:
public function via(object $notifiable): array
{
return $notifiable->wants_email_notifications
? ['mail', 'database']
: ['database'];
}
That’s a very clean way to support user preferences.
Final thoughts
Laravel Notifications are simple to start with, but very powerful when your app grows.
They give you a consistent structure for:
- deciding where a message should be sent
- formatting it for each channel
- queueing it for performance
- testing it safely
If you’re building Laravel apps with user workflows (orders, bookings, payments, accounts, dashboards), Notifications are one of those features that quickly become foundational.
Top comments (0)