# Laravel Notification Log Laravel Notification Log is a package by Spatie that automatically logs all notifications sent by your Laravel application. It creates a comprehensive history of sent notifications in the `notification_log_items` database table, capturing details such as notification type, notifiable entity, channel, fingerprint for identifying specific content, and custom extra data. The package provides powerful query methods to retrieve logged notifications and make decisions based on notification history. Key features include the ability to prevent duplicate notifications within a time window, search notifications by fingerprint or type, and add custom data to each log entry. It integrates seamlessly with Laravel's notification system through event listeners, supports on-demand notifications, and includes automatic pruning of old records via Laravel's MassPrunable trait. ## Installation Install the package via Composer and run the migrations to create the notification log table. ```bash composer require spatie/laravel-notification-log php artisan vendor:publish --tag="notification-log-migrations" php artisan migrate ``` ## Configuration Publish and customize the configuration file to control logging behavior, pruning settings, and model customization. ```php // config/notification-log.php return [ // The model used to log sent notifications 'model' => Spatie\NotificationLog\Models\NotificationLogItem::class, // Log items older than this will be automatically pruned 'prune_after_days' => 30, // Set to false to only log notifications with shouldLog() returning true 'log_all_by_default' => true, // Custom action classes for low-level customization 'actions' => [ 'convertEventToModel' => Spatie\NotificationLog\Actions\ConvertNotificationSendingEventToLogItemAction::class, ], // Event subscriber for notification events 'event_subscriber' => Spatie\NotificationLog\NotificationEventSubscriber::class, ]; ``` ## Schedule Pruning Configure Laravel's model pruning to automatically clean up old notification log entries. ```php // routes/console.php (Laravel 11+) use Illuminate\Support\Facades\Schedule; Schedule::command('model:prune', [ '--model' => [ \Spatie\NotificationLog\Models\NotificationLogItem::class, ], ])->daily(); ``` ## HasNotifiableHistory Trait Add the `HasNotifiableHistory` trait to your User model (or any notifiable) to enable convenient methods for querying notification history directly from the model. ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use Spatie\NotificationLog\Models\Concerns\HasNotifiableHistory; class User extends Model { use Notifiable; use HasNotifiableHistory; } // Usage examples: // Get all logged notifications for a user (most recent first) $allNotifications = $user->notificationLogItems; // Get the 5 most recent notifications $recentNotifications = $user->notificationLogItems()->limit(5)->get(); // Get the single most recent notification $latestNotification = $user->latestLoggedNotification(); // Get latest notification with filters $latestOrderNotification = $user->latestLoggedNotification( notificationTypes: \App\Notifications\OrderShippedNotification::class, fingerprint: 'order-123', after: now()->subDays(7), channel: 'mail' ); ``` ## NotificationLogItem::latestFor() Query the notification log to find the most recent notification sent to a specific notifiable, with optional filtering by type, fingerprint, channel, and time range. ```php use Spatie\NotificationLog\Models\NotificationLogItem; use App\Notifications\OrderShippedNotification; use App\Notifications\PaymentReceivedNotification; // Get the latest notification sent to a user $logItem = NotificationLogItem::latestFor($user); // Get the latest notification of a specific type $logItem = NotificationLogItem::latestFor( $user, notificationType: OrderShippedNotification::class ); // Search with multiple notification types (returns most recent of any type) $logItem = NotificationLogItem::latestFor( $user, notificationType: [ OrderShippedNotification::class, PaymentReceivedNotification::class, ] ); // Get notification with specific fingerprint $logItem = NotificationLogItem::latestFor( $user, fingerprint: 'order-456' ); // Get notification within a time range $logItem = NotificationLogItem::latestFor( $user, after: now()->subHours(24), before: now()->subHours(1) ); // Filter by channel $logItem = NotificationLogItem::latestFor( $user, channel: 'mail' ); // Access log item properties if ($logItem) { echo $logItem->notification_type; // App\Notifications\OrderShippedNotification echo $logItem->channel; // mail echo $logItem->fingerprint; // order-456 echo $logItem->created_at; // 2024-01-15 10:30:00 echo $logItem->confirmed_at; // 2024-01-15 10:30:01 print_r($logItem->extra); // ['order_total' => 99.99] } ``` ## HasHistory Trait - Preventing Duplicate Notifications Add the `HasHistory` trait to your notification class to use `wasSentTo()` and `wasNotSentTo()` methods for implementing rate limiting and duplicate prevention logic. ```php namespace App\Notifications; use Illuminate\Notifications\Notification; use Illuminate\Notifications\Messages\MailMessage; use Spatie\NotificationLog\Models\Concerns\HasHistory; class DailyReportNotification extends Notification { use HasHistory; // Prevent sending if this notification was sent in the last 60 minutes public function shouldSend($notifiable): bool { return $this ->wasNotSentTo($notifiable) ->inThePastMinutes(60); } public function via($notifiable): array { return ['mail']; } public function toMail($notifiable): MailMessage { return (new MailMessage) ->subject('Your Daily Report') ->line('Here is your daily summary.'); } } // With fingerprint matching - only prevent duplicates with same content class OrderUpdateNotification extends Notification { use HasHistory; public function __construct(public Order $order) {} public function shouldSend($notifiable): bool { // Only block if same notification with same fingerprint was sent return $this ->wasNotSentTo($notifiable, withSameFingerprint: true) ->inThePastMinutes(30); } public function fingerprint(): string { return "order-{$this->order->id}-status-{$this->order->status}"; } public function via($notifiable): array { return ['mail']; } } // Check if notification WAS sent (inverse logic) class ReminderNotification extends Notification { use HasHistory; public function shouldSend($notifiable): bool { // Only send reminder if original notification was sent $wasSent = $this ->wasSentTo($notifiable) ->inThePastMinutes(60); return $wasSent; } } // Check specific channel class MultiChannelNotification extends Notification { use HasHistory; public function shouldSend($notifiable): bool { return $this ->wasNotSentTo($notifiable) ->onChannel('mail') ->inThePastMinutes(120); } } // Check if ever sent (no time limit) class WelcomeNotification extends Notification { use HasHistory; public function shouldSend($notifiable): bool { return $this ->wasNotSentTo($notifiable) ->inThePast(); // No time limit - check entire history } } ``` ## Fingerprints for Content Identification Define a `fingerprint()` method on your notification to track specific notification content, enabling queries for exact matches and duplicate detection based on notification parameters. ```php namespace App\Notifications; use Illuminate\Notifications\Notification; use Spatie\NotificationLog\Models\Concerns\HasHistory; class InvoiceNotification extends Notification { use HasHistory; public function __construct( public Invoice $invoice, public string $action = 'created' ) {} // Simple fingerprint based on invoice ID public function fingerprint(): string { return "invoice-{$this->invoice->id}"; } public function via($notifiable): array { return ['mail']; } } // Complex fingerprint with multiple parameters class ShipmentUpdateNotification extends Notification { public function __construct( public Order $order, public string $trackingNumber, public string $status ) {} // Hash-based fingerprint for complex data public function fingerprint(): string { return md5("{$this->order->id}-{$this->trackingNumber}-{$this->status}"); } } // Query notifications by fingerprint use Spatie\NotificationLog\Models\NotificationLogItem; $invoiceNotification = NotificationLogItem::latestFor( $user, fingerprint: "invoice-{$invoice->id}" ); // Check if specific invoice notification was sent if ($invoiceNotification && $invoiceNotification->wasSentInPastMinutes(60)) { // Don't send duplicate } ``` ## Adding Extra Data to Logs Implement the `logExtra()` method on your notification to store additional metadata with each log entry for debugging, analytics, or audit purposes. ```php namespace App\Notifications; use Illuminate\Notifications\Notification; use Illuminate\Notifications\Events\NotificationSending; class PurchaseConfirmationNotification extends Notification { public function __construct( public Order $order, public float $discount = 0 ) {} // Store additional data with the notification log public function logExtra(NotificationSending $event): array { return [ 'order_id' => $this->order->id, 'order_total' => $this->order->total, 'discount_applied' => $this->discount, 'items_count' => $this->order->items->count(), 'shipping_method' => $this->order->shipping_method, 'user_locale' => $event->notifiable->locale ?? 'en', ]; } public function via($notifiable): array { return ['mail', 'database']; } } // Access extra data from log items use Spatie\NotificationLog\Models\NotificationLogItem; $logs = NotificationLogItem::query() ->where('notification_type', PurchaseConfirmationNotification::class) ->get(); foreach ($logs as $log) { $extra = $log->extra; echo "Order #{$extra['order_id']}: \${$extra['order_total']}"; echo "Discount: \${$extra['discount_applied']}"; } ``` ## Controlling Which Notifications Get Logged Implement the `shouldLog()` method to selectively log notifications based on conditions, or disable global logging via configuration. ```php namespace App\Notifications; use Illuminate\Notifications\Notification; use Illuminate\Notifications\Events\NotificationSending; class ImportantAlertNotification extends Notification { public function __construct( public string $severity = 'low' ) {} // Only log high-severity alerts public function shouldLog(NotificationSending $event): bool { return $this->severity === 'high' || $this->severity === 'critical'; } public function via($notifiable): array { return ['mail', 'slack']; } } // Always log this notification class AuditRequiredNotification extends Notification { public function shouldLog(NotificationSending $event): bool { return true; } } // Never log this notification class TransientNotification extends Notification { public function shouldLog(NotificationSending $event): bool { return false; } } // Conditional logging based on notifiable class UserNotification extends Notification { public function shouldLog(NotificationSending $event): bool { // Only log for premium users return $event->notifiable->is_premium ?? false; } } ``` ## Custom Notification Type Override the default notification type stored in logs by implementing the `logType()` method for cleaner, more readable type names. ```php namespace App\Notifications; use Illuminate\Notifications\Notification; class OrderStatusChangedNotification extends Notification { public function __construct( public Order $order, public string $newStatus ) {} // Use a custom type instead of the full class name public function logType(): string { return "order.status.{$this->newStatus}"; } public function via($notifiable): array { return ['mail']; } } // Query by custom type use Spatie\NotificationLog\Models\NotificationLogItem; $shippedNotifications = NotificationLogItem::query() ->where('notification_type', 'order.status.shipped') ->get(); ``` ## On-Demand Notifications Send notifications to recipients not backed by a user model. On-demand notification details are stored in the `anonymous_notifiable_properties` column. ```php use Illuminate\Support\Facades\Notification; use App\Notifications\SystemAlertNotification; // Send to an email address without a User model Notification::route('mail', 'admin@example.com') ->route('slack', '#alerts') ->notify(new SystemAlertNotification('Server CPU high')); // The log entry will have: // - notifiable_id: null // - notifiable_type: null // - anonymous_notifiable_properties: ['mail' => 'admin@example.com', 'slack' => '#alerts'] // Query on-demand notifications use Spatie\NotificationLog\Models\NotificationLogItem; $anonymousLogs = NotificationLogItem::query() ->whereNull('notifiable_id') ->whereNotNull('anonymous_notifiable_properties') ->get(); foreach ($anonymousLogs as $log) { $routes = $log->anonymous_notifiable_properties; echo "Sent to: " . ($routes['mail'] ?? 'N/A'); } ``` ## Custom Notification Log Model Extend the base `NotificationLogItem` model to add custom functionality, change the database connection, or modify pruning behavior. ```php namespace App\Models; use Spatie\NotificationLog\Models\NotificationLogItem; use Illuminate\Database\Eloquent\Builder; class CustomNotificationLogItem extends NotificationLogItem { // Use a different database connection protected $connection = 'logs'; // Custom table name protected $table = 'custom_notification_logs'; // Custom pruning logic - keep critical notifications longer public function prunable(): Builder { return static::query() ->where('created_at', '<=', now()->subDays(90)) ->where('notification_type', 'not like', '%Critical%'); } // Add custom methods public function scopeForChannel(Builder $query, string $channel): Builder { return $query->where('channel', $channel); } public function scopeSuccessful(Builder $query): Builder { return $query->whereNotNull('confirmed_at'); } // Add custom attributes public function getWasSuccessfulAttribute(): bool { return $this->confirmed_at !== null; } } // Register in config/notification-log.php return [ 'model' => \App\Models\CustomNotificationLogItem::class, // ... ]; // Use custom model $logs = CustomNotificationLogItem::query() ->forChannel('mail') ->successful() ->get(); ``` ## Custom Conversion Action Create a custom action to fully control how notification events are converted to log entries. ```php namespace App\Actions; use Spatie\NotificationLog\Actions\ConvertNotificationSendingEventToLogItemAction; use Illuminate\Notifications\Events\NotificationSending; class CustomConversionAction extends ConvertNotificationSendingEventToLogItemAction { // Use short class name instead of FQCN protected function getNotificationType(NotificationSending $event): string { return class_basename($event->notification); } // Add custom fingerprint generation protected function getFingerprint(NotificationSending $event): ?string { $notification = $event->notification; // Custom fingerprint if notification doesn't define one if (!method_exists($notification, 'fingerprint')) { return md5(serialize($notification)); } return parent::getFingerprint($event); } } // Register in config/notification-log.php return [ 'actions' => [ 'convertEventToModel' => \App\Actions\CustomConversionAction::class, ], ]; ``` ## Displaying Notification History in Views Retrieve and display notification history for users in your application's UI. ```php // In a controller namespace App\Http\Controllers; use Illuminate\Http\Request; class NotificationHistoryController extends Controller { public function index(Request $request) { $notifications = $request->user() ->notificationLogItems() ->limit(50) ->get(); return view('notifications.history', compact('notifications')); } } ``` ```blade {{-- resources/views/notifications/history.blade.php --}}
No notifications have been sent yet.
@endforelse