Laravel Notifications in Practice: Mail, Database, Queues, and Clean Testing
Source: Dev.to
Laravel Notifications – A Practical Guide
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 (e.g., Order shipped, Invoice paid, New comment, Password changed) across multiple channels using a single class.
Why use Notifications instead of sending mail directly?
While you could call Mail::to()->send() everywhere, notifications offer a better architecture:
- Centralized logic – define delivery channels in one place via the
via()method. - Multi‑channel support – easily switch between or combine mail, database, Slack, SMS, etc.
- Built‑in queues – native support for background processing.
- Observability – test with
Notification::fake()without sending real messages.
This makes your code easier to maintain as 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
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 the 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+Queueablekeep delivery asynchronous 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));
Great for admin alerts, moderation events, or internal‑ops notifications.
Step 4: Enable database notifications
Create the notifications table and run the migration:
php artisan make:notifications-table
php artisan migrate
Laravel stores notification payloads in a notifications table (including 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 controller method
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 centre in Vue, React, or 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 perform a direct query update for better efficiency.
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 for production).
php artisan queue:work
With these steps you have a reusable, clean notification system that scales as your Laravel application grows. Happy coding!
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.
class ShipOrderAction
{
public function handle(Order $order): void
{
$order->update(['status' => 'shipped']);
$order->user->notify(new OrderShipped($order));
}
}
Why This Works Well
- Controllers stay thin
- Notification logic is tied to the domain action
- Easy to test
- Easy to reuse from HTTP, CLI, or jobs
Common Mistakes to Avoid
-
Sending notifications directly in controllers everywhere
It works at first, but it spreads your logic across the app. -
Forgetting to queue notifications
Mail and third‑party channels can slow down your requests a lot. -
Overloading notifications with business logic
Keep notifications focused on delivery + presentation.
Your business rules should live in services/actions. -
Mixing database and broadcast payloads without thinking
If your database payload and realtime frontend payload need to differ, definetoDatabase()andtoBroadcast()separately instead of relying on onetoArray()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 quickly become foundational.