Sending notifications across multiple channels—email, SMS, Slack, push notifications—requires coordinating different APIs and formats. Laravel 5.3+ unifies this with a single notification class that delivers to any channel. At ZIRA Software, Laravel Notifications has simplified our notification infrastructure from dozens of scattered classes to clean, testable code.
Why Laravel Notifications?
Before: Scattered notification logic:
// Email in one place
Mail::to($user)->send(new OrderShipped($order));
// SMS elsewhere
Nexmo::message()->send([
'to' => $user->phone,
'from' => 'Company',
'text' => "Your order #{$order->id} has shipped!"
]);
// Slack in another place
SlackAPI::postMessage('#sales', "New order from {$user->name}");
Problems:
- Duplicate logic across channels
- Hard to test
- Inconsistent formatting
- Can't easily add channels
- No queuing strategy
- Scattered throughout codebase
Laravel Notifications:
- Single notification class
- Multiple channels from one place
- Consistent formatting per channel
- Easy to queue
- Simple to test
- Extensible with custom channels
Creating Notifications
php artisan make:notification OrderShipped
app/Notifications/OrderShipped.php:
<?php namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
protected $order;
public function __construct($order)
{
$this->order = $order;
}
/**
* Get the notification's delivery channels.
*/
public function via($notifiable)
{
return ['mail', 'database'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail($notifiable)
{
return (new MailMessage)
->subject('Your Order Has Shipped!')
->greeting("Hello {$notifiable->name}!")
->line("Your order #{$this->order->id} has shipped.")
->line("Tracking: {$this->order->tracking_number}")
->action('View Order', url("/orders/{$this->order->id}"))
->line('Thank you for your purchase!');
}
/**
* Get the array representation of the notification.
*/
public function toArray($notifiable)
{
return [
'order_id' => $this->order->id,
'tracking_number' => $this->order->tracking_number,
'shipped_at' => now()->toDateTimeString(),
];
}
}
Sending Notifications
Notifiable trait on User model:
<?php namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
}
Send notification:
use App\Notifications\OrderShipped;
// Send to user
$user->notify(new OrderShipped($order));
// Send to multiple users
$users = User::where('vip', true)->get();
Notification::send($users, new OrderShipped($order));
// Send anonymously (without user)
Notification::route('mail', 'admin@example.com')
->notify(new OrderShipped($order));
Email Notifications
Basic email:
public function toMail($notifiable)
{
return (new MailMessage)
->line('The introduction to the notification.')
->action('Action Text', url('/'))
->line('Thank you for using our application!');
}
Custom template:
public function toMail($notifiable)
{
return (new MailMessage)
->view('emails.order-shipped', [
'order' => $this->order,
'user' => $notifiable,
]);
}
Markdown email:
public function toMail($notifiable)
{
return (new MailMessage)
->markdown('emails.order-shipped', [
'url' => url("/orders/{$this->order->id}"),
'order' => $this->order,
]);
}
resources/views/emails/order-shipped.blade.php:
@component('mail::message')
# Order Shipped
Your order #{{ $order->id }} has shipped!
**Tracking Number:** {{ $order->tracking_number }}
@component('mail::button', ['url' => $url])
View Order
@endcomponent
Thanks,<br>
{{ config('app.name') }}
@endcomponent
Attachments:
public function toMail($notifiable)
{
return (new MailMessage)
->subject('Invoice for Order #' . $this->order->id)
->attach(storage_path("invoices/{$this->order->invoice_path}"))
->attachData($this->pdf, 'invoice.pdf', [
'mime' => 'application/pdf',
]);
}
Database Notifications
Create notifications table:
php artisan notifications:table
php artisan migrate
Store in database:
public function via($notifiable)
{
return ['database'];
}
public function toArray($notifiable)
{
return [
'message' => "Your order #{$this->order->id} has shipped",
'order_id' => $this->order->id,
'action_url' => url("/orders/{$this->order->id}"),
];
}
Retrieve notifications:
// All notifications
$notifications = $user->notifications;
// Unread only
$unread = $user->unreadNotifications;
// Mark as read
$user->unreadNotifications->markAsRead();
// Mark specific as read
$notification = $user->notifications()->find($id);
$notification->markAsRead();
// Delete notification
$notification->delete();
Display in UI:
@if($user->unreadNotifications->count())
<div class="notifications-dropdown">
<span class="badge">{{ $user->unreadNotifications->count() }}</span>
<ul>
@foreach($user->unreadNotifications as $notification)
<li>
<a href="{{ $notification->data['action_url'] }}">
{{ $notification->data['message'] }}
</a>
<small>{{ $notification->created_at->diffForHumans() }}</small>
</li>
@endforeach
</ul>
</div>
@endif
Controller:
<?php namespace App\Http\Controllers;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
public function index()
{
$notifications = auth()->user()
->notifications()
->paginate(15);
return view('notifications.index', compact('notifications'));
}
public function markAsRead($id)
{
$notification = auth()->user()
->notifications()
->findOrFail($id);
$notification->markAsRead();
return redirect($notification->data['action_url']);
}
public function markAllAsRead()
{
auth()->user()->unreadNotifications->markAsRead();
return back();
}
}
SMS Notifications (Nexmo/Vonage)
Install Nexmo:
composer require laravel/nexmo-notification-channel
.env:
NEXMO_KEY=your-api-key
NEXMO_SECRET=your-api-secret
NEXMO_SMS_FROM=YourCompany
Add phone to User model:
public function routeNotificationForNexmo($notification)
{
return $this->phone_number;
}
SMS notification:
use Illuminate\Notifications\Messages\NexmoMessage;
public function via($notifiable)
{
return ['nexmo'];
}
public function toNexmo($notifiable)
{
return (new NexmoMessage)
->content("Your order #{$this->order->id} has shipped! Track: {$this->order->tracking_number}");
}
Slack Notifications
Install Slack channel:
composer require laravel/slack-notification-channel
Add webhook URL to User:
public function routeNotificationForSlack($notification)
{
return $this->slack_webhook_url;
}
Or configure in .env:
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
Slack notification:
use Illuminate\Notifications\Messages\SlackMessage;
public function via($notifiable)
{
return ['slack'];
}
public function toSlack($notifiable)
{
return (new SlackMessage)
->from('OrderBot', ':package:')
->to('#orders')
->content('New order shipped!')
->attachment(function ($attachment) {
$attachment->title("Order #{$this->order->id}", url("/orders/{$this->order->id}"))
->fields([
'Customer' => $this->order->user->name,
'Total' => '$' . number_format($this->order->total, 2),
'Tracking' => $this->order->tracking_number,
]);
});
}
Rich formatting:
public function toSlack($notifiable)
{
return (new SlackMessage)
->success() // Green sidebar
->content('Order shipped successfully!')
->attachment(function ($attachment) {
$attachment
->title("Order #{$this->order->id}")
->content($this->order->summary)
->markdown(['text'])
->action('View Order', url("/orders/{$this->order->id}"))
->action('Track Package', $this->order->tracking_url);
});
}
Broadcast Notifications (Real-time)
Enable broadcasting channel:
public function via($notifiable)
{
return ['database', 'broadcast'];
}
public function toBroadcast($notifiable)
{
return new BroadcastMessage([
'message' => "Your order #{$this->order->id} has shipped",
'action_url' => url("/orders/{$this->order->id}"),
]);
}
Listen with Echo:
Echo.private(`App.User.${userId}`)
.notification((notification) => {
console.log(notification);
// Show toast
toastr.success(notification.message);
// Update UI
updateNotificationBadge();
});
Custom Channels
Create custom channel:
<?php namespace App\Channels;
use Illuminate\Notifications\Notification;
class TwilioChannel
{
protected $client;
public function __construct($client)
{
$this->client = $client;
}
public function send($notifiable, Notification $notification)
{
$message = $notification->toTwilio($notifiable);
$this->client->messages->create(
$notifiable->routeNotificationFor('twilio'),
[
'from' => config('services.twilio.from'),
'body' => $message->content,
]
);
}
}
Register in notification:
use App\Channels\TwilioChannel;
public function via($notifiable)
{
return [TwilioChannel::class];
}
public function toTwilio($notifiable)
{
return (new TwilioMessage)
->content("Your order #{$this->order->id} has shipped!");
}
Conditional Channels
Send email only if user prefers:
public function via($notifiable)
{
$channels = ['database'];
if ($notifiable->prefers_email) {
$channels[] = 'mail';
}
if ($notifiable->prefers_sms) {
$channels[] = 'nexmo';
}
return $channels;
}
Time-based channels:
public function via($notifiable)
{
// SMS during business hours, email otherwise
if (now()->hour >= 9 && now()->hour <= 17) {
return ['nexmo', 'database'];
}
return ['mail', 'database'];
}
Queuing Notifications
Implement ShouldQueue:
class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public $delay = 300; // Delay 5 minutes
public function via($notifiable)
{
return ['mail'];
}
}
Custom queue:
public function via($notifiable)
{
return ['mail'];
}
public function viaQueues()
{
return [
'mail' => 'emails',
'nexmo' => 'sms',
];
}
Notification Events
Listen to notification events:
<?php namespace App\Providers;
use Illuminate\Notifications\Events\NotificationSent;
use Illuminate\Notifications\Events\NotificationFailed;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
NotificationSent::class => [
'App\Listeners\LogNotificationSent',
],
NotificationFailed::class => [
'App\Listeners\LogNotificationFailure',
],
];
}
Logger:
<?php namespace App\Listeners;
use Illuminate\Notifications\Events\NotificationSent;
class LogNotificationSent
{
public function handle(NotificationSent $event)
{
\Log::info('Notification sent', [
'notification' => get_class($event->notification),
'channel' => $event->channel,
'notifiable' => get_class($event->notifiable),
]);
}
}
User Preferences
Migration:
Schema::table('users', function (Blueprint $table) {
$table->json('notification_preferences')->nullable();
});
User model:
protected $casts = [
'notification_preferences' => 'array',
];
public function prefers($channel)
{
return $this->notification_preferences[$channel] ?? true;
}
Notification:
public function via($notifiable)
{
$channels = [];
if ($notifiable->prefers('mail')) {
$channels[] = 'mail';
}
if ($notifiable->prefers('sms')) {
$channels[] = 'nexmo';
}
if ($notifiable->prefers('slack')) {
$channels[] = 'slack';
}
// Always store in database
$channels[] = 'database';
return $channels;
}
Testing Notifications
tests/Feature/NotificationTest.php:
<?php
use App\User;
use App\Notifications\OrderShipped;
use Illuminate\Support\Facades\Notification;
use Illuminate\Foundation\Testing\RefreshDatabase;
class NotificationTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_sends_notification_when_order_ships()
{
Notification::fake();
$user = factory(User::class)->create();
$order = factory(Order::class)->create(['user_id' => $user->id]);
$order->ship();
Notification::assertSentTo($user, OrderShipped::class);
}
/** @test */
public function it_sends_via_correct_channels()
{
Notification::fake();
$user = factory(User::class)->create([
'prefers_email' => true,
'prefers_sms' => false,
]);
$user->notify(new OrderShipped($order));
Notification::assertSentTo($user, OrderShipped::class, function ($notification, $channels) {
return in_array('mail', $channels) && !in_array('nexmo', $channels);
});
}
}
On-Demand Notifications
Send without user:
Notification::route('mail', 'taylor@example.com')
->route('nexmo', '5555555555')
->notify(new InvoicePaid($invoice));
Localizing Notifications
public function toMail($notifiable)
{
$locale = $notifiable->locale ?? 'en';
return (new MailMessage)
->subject(__('notifications.order_shipped.subject', [], $locale))
->greeting(__('notifications.order_shipped.greeting', ['name' => $notifiable->name], $locale))
->line(__('notifications.order_shipped.body', ['order_id' => $this->order->id], $locale));
}
Best Practices
- Queue notifications - Don't block requests
- Use database channel - Always store notifications
- Respect user preferences - Let users control channels
- Handle failures gracefully - Log and retry
- Test thoroughly - Use Notification::fake()
- Localize messages - Support multiple languages
- Rate limit - Don't spam users
Common Patterns
Admin alerts:
public function via($notifiable)
{
// Urgent issues go to multiple channels
if ($this->severity === 'critical') {
return ['mail', 'nexmo', 'slack'];
}
return ['mail', 'database'];
}
Digest notifications:
// Send daily summary instead of individual notifications
$user->notify(new DailyDigest(
$user->unreadNotifications()->where('created_at', '>=', now()->subDay())
));
Conclusion
Laravel Notifications unify multi-channel messaging into clean, testable code. From emails to SMS to Slack, one notification class handles all channels consistently. At ZIRA Software, this has simplified our notification infrastructure dramatically while improving reliability.
Start with email and database notifications. Add SMS and Slack as needed. Let users control their preferences. Your notification system will be robust and maintainable.
Building applications with complex notification requirements? Contact ZIRA Software to discuss multi-channel notification strategies, real-time messaging, and user preference management.