DEV Community

A0mineTV
A0mineTV

Posted on

Laravel Notifications in Practice: Mail, Database, Queues, and Clean Testing

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
Enter fullscreen mode Exit fullscreen mode

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}",
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this is clean

  • via() defines the channels in one place
  • toMail() handles only the email message
  • toArray() formats the in-app payload
  • ShouldQueue + Queueable keeps 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));
Enter fullscreen mode Exit fullscreen mode

You can also send to multiple users:

use Illuminate\Support\Facades\Notification;

Notification::send($admins, new OrderShipped($order));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
    ]);
}
Enter fullscreen mode Exit fullscreen mode

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']);
}
Enter fullscreen mode Exit fullscreen mode

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 ShouldQueue
  • use 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
Enter fullscreen mode Exit fullscreen mode

Then run:

php artisan queue:work
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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'];
}
Enter fullscreen mode Exit fullscreen mode

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)