Workflows

Notifications

Notifications

Notifications keep users informed about workflow events, approvals, and system changes. Laravolt v7 uses Laravel's notification system with email, database, and in-app channels.

Notification channels

Laravel supports multiple channels:

  • Database — store notifications in database for in-app display
  • Mail — send email notifications
  • Broadcast — real-time notifications via WebSockets
  • Slack — send to Slack channels
  • Custom — build your own channels

Basic notification

PHP
// app/Notifications/RequisitionSubmitted.php
namespace App\Notifications;
use App\Models\PurchaseRequisition;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class RequisitionSubmitted extends Notification
{
use Queueable;
public function __construct(
public PurchaseRequisition $requisition
) {}
public function via($notifiable): array
{
return ['mail', 'database'];
}
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->subject('New requisition requires approval')
->line('A new purchase requisition has been submitted.')
->line('Requester: ' . $this->requisition->requester->name)
->line('Total: $' . number_format($this->requisition->total, 2))
->action('Review requisition', route('requisitions.show', $this->requisition))
->line('Please review and approve or reject this requisition.');
}
public function toDatabase($notifiable): array
{
return [
'requisition_id' => $this->requisition->id,
'requester_name' => $this->requisition->requester->name,
'total' => $this->requisition->total,
'action_url' => route('requisitions.show', $this->requisition),
];
}
}

Sending notifications

PHP
// app/Models/PurchaseRequisition.php
use App\Notifications\RequisitionSubmitted;
use App\Notifications\RequisitionApproved;
use App\Notifications\RequisitionRejected;
class PurchaseRequisition extends Model
{
protected function notifyApprovers()
{
$approvers = User::role('approver')->get();
foreach ($approvers as $approver) {
$approver->notify(new RequisitionSubmitted($this));
}
}
protected function notifyRequester(string $action)
{
$notification = match($action) {
'approved' => new RequisitionApproved($this),
'rejected' => new RequisitionRejected($this),
};
$this->requester->notify($notification);
}
}

Email templates

Customize email appearance:

PHP
// app/Notifications/RequisitionSubmitted.php
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->subject('New requisition requires approval')
->greeting('Hello ' . $notifiable->name . ',')
->line('A new purchase requisition has been submitted and requires your approval.')
->line('**Requisition details:**')
->line('- Requester: ' . $this->requisition->requester->name)
->line('- Date: ' . $this->requisition->created_at->format('M d, Y'))
->line('- Total: $' . number_format($this->requisition->total, 2))
->line('- Items: ' . $this->requisition->items->count())
->action('Review requisition', route('requisitions.show', $this->requisition))
->line('Please review this requisition at your earliest convenience.')
->salutation('Best regards, ' . config('app.name'));
}

In-app notifications

Display notifications in the UI:

1. Database migration

PHP
// database/migrations/xxxx_create_notifications_table.php
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});

2. Notification component

Blade
{{-- resources/views/components/notifications-dropdown.blade.php --}}
<div x-data="{ open: false }" class="relative">
<button
@click="open = !open"
type="button"
class="relative inline-flex items-center p-2 text-sm font-medium text-center text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
@if($unreadCount = auth()->user()->unreadNotifications->count())
<span class="absolute top-0 right-0 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full">
{{ $unreadCount }}
</span>
@endif
</button>
<div
x-show="open"
@click.away="open = false"
x-cloak
class="absolute right-0 z-50 mt-2 w-80 bg-white divide-y divide-gray-100 rounded-lg shadow-lg dark:bg-neutral-800 dark:divide-neutral-700"
>
<div class="px-4 py-3 text-sm text-gray-900 dark:text-white">
<div class="font-medium">Notifications</div>
</div>
<div class="max-h-96 overflow-y-auto">
@forelse(auth()->user()->notifications->take(10) as $notification)
<a
href="{{ $notification->data['action_url'] ?? '#' }}"
@if(!$notification->read_at)
@click="$wire.markAsRead('{{ $notification->id }}')"
@endif
class="flex px-4 py-3 hover:bg-gray-100 dark:hover:bg-neutral-700 {{ $notification->read_at ? 'opacity-60' : '' }}"
>
<div class="flex-1">
<div class="text-sm font-medium text-gray-900 dark:text-white">
{{ $notification->data['title'] ?? 'Notification' }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ $notification->data['message'] ?? '' }}
</div>
<div class="text-xs text-gray-400 dark:text-gray-500 mt-1">
{{ $notification->created_at->diffForHumans() }}
</div>
</div>
@if(!$notification->read_at)
<div class="flex-shrink-0">
<span class="inline-block w-2 h-2 bg-blue-600 rounded-full"></span>
</div>
@endif
</a>
@empty
<div class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
No notifications
</div>
@endforelse
</div>
<a href="{{ route('notifications.index') }}" class="block px-4 py-2 text-sm text-center text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700">
View all notifications
</a>
</div>
</div>

3. Mark as read

PHP
// app/Http/Controllers/NotificationController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
public function index()
{
$notifications = auth()->user()->notifications()->paginate(20);
return view('notifications.index', compact('notifications'));
}
public function markAsRead(string $id)
{
auth()->user()->notifications()->findOrFail($id)->markAsRead();
return back();
}
public function markAllAsRead()
{
auth()->user()->unreadNotifications->markAsRead();
return back()->with('success', 'All notifications marked as read');
}
}

Notification preferences

Let users control notification channels:

PHP
// app/Models/User.php
public function notificationPreferences()
{
return $this->hasOne(NotificationPreference::class);
}
public function shouldReceiveNotification(string $type, string $channel): bool
{
$preferences = $this->notificationPreferences;
if (!$preferences) {
return true; // Default: receive all
}
return $preferences->isEnabled($type, $channel);
}
PHP
// app/Notifications/RequisitionSubmitted.php
public function via($notifiable): array
{
$channels = [];
if ($notifiable->shouldReceiveNotification('requisition_submitted', 'mail')) {
$channels[] = 'mail';
}
if ($notifiable->shouldReceiveNotification('requisition_submitted', 'database')) {
$channels[] = 'database';
}
return $channels;
}

Queued notifications

Send notifications asynchronously:

PHP
// app/Notifications/RequisitionSubmitted.php
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
class RequisitionSubmitted extends Notification implements ShouldQueue
{
use Queueable;
// Notification will be queued automatically
}

Notification groups

Group related notifications:

PHP
// app/Notifications/RequisitionSubmitted.php
public function toDatabase($notifiable): array
{
return [
'group' => 'requisitions',
'requisition_id' => $this->requisition->id,
'requester_name' => $this->requisition->requester->name,
'total' => $this->requisition->total,
'action_url' => route('requisitions.show', $this->requisition),
];
}

Display grouped notifications:

Blade
@php
$grouped = auth()->user()->notifications->groupBy('data.group');
@endphp
@foreach($grouped as $group => $notifications)
<div class="mb-4">
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
{{ ucfirst($group) }}
</h3>
@foreach($notifications as $notification)
{{-- Display notification --}}
@endforeach
</div>
@endforeach

Integration with workflows

Trigger notifications at workflow transitions:

PHP
// app/Models/PurchaseRequisition.php
use App\Notifications\RequisitionSubmitted;
use App\Notifications\RequisitionApproved;
use App\Notifications\RequisitionRejected;
class PurchaseRequisition extends Model
{
public function submit()
{
$this->state->transitionTo(PendingApproval::class);
$this->save();
// Notify approvers
$this->notifyApprovers();
// Log activity
activity()
->performedOn($this)
->causedBy(auth()->user())
->log('Requisition submitted');
}
public function approve(User $approver)
{
$this->state->transitionTo(Approved::class);
$this->approved_by = $approver->id;
$this->approved_at = now();
$this->save();
// Notify requester
$this->requester->notify(new RequisitionApproved($this));
// Create purchase order
$this->createPurchaseOrder();
}
public function reject(User $approver, string $reason)
{
$this->state->transitionTo(Rejected::class);
$this->rejected_by = $approver->id;
$this->rejected_at = now();
$this->rejection_reason = $reason;
$this->save();
// Notify requester
$this->requester->notify(new RequisitionRejected($this, $reason));
}
protected function notifyApprovers()
{
$approvers = $this->determineApprovers();
foreach ($approvers as $approver) {
$approver->notify(new RequisitionSubmitted($this));
}
}
}

Verification commands

Test notifications:

Bash
# Send test notification
php artisan tinker
>>> $user = User::first()
>>> $pr = PurchaseRequisition::first()
>>> $user->notify(new RequisitionSubmitted($pr))
# Check database notifications
>>> $user->notifications
>>> $user->unreadNotifications
# Mark as read
>>> $user->unreadNotifications->markAsRead()
# Test email
php artisan queue:work
# Submit requisition → check email inbox
# Test notification preferences
>>> $user->notificationPreferences->disable('requisition_submitted', 'mail')
>>> $user->notify(new RequisitionSubmitted($pr))
# Should only send database notification

Common pitfalls

Missing queue worker

Queued notifications require a running queue worker:

Bash
# Development
php artisan queue:work
# Production (use Supervisor)
[program:laravel-worker]
command=php /path/to/artisan queue:work --sleep=3 --tries=3

Notification spam

Batch notifications to avoid overwhelming users:

PHP
// Instead of notifying immediately
public function submit()
{
$this->state->transitionTo(PendingApproval::class);
$this->save();
// Queue notification for later
NotifyApprovers::dispatch($this)->delay(now()->addMinutes(5));
}

Missing notification data

Always include action URLs and context:

PHP
public function toDatabase($notifiable): array
{
return [
'title' => 'New requisition requires approval',
'message' => 'Requisition #' . $this->requisition->id . ' from ' . $this->requisition->requester->name,
'action_url' => route('requisitions.show', $this->requisition),
'requisition_id' => $this->requisition->id,
];
}
Previous
Approval flows