offload-project / laravel-notification-preferences
Manage and display user notification preferences in Laravel
Package info
github.com/offload-project/laravel-notification-preferences
pkg:composer/offload-project/laravel-notification-preferences
Requires
- php: ^8.3
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/notifications: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- captainhook/captainhook-phar: ^5.27
- larastan/larastan: ^3.8.1
- laravel/pint: ^1.26.0
- orchestra/testbench: ^9.15|^10.8|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- ramsey/conventional-commits: ^1.6
This package is auto-updated.
Last update: 2026-03-31 03:29:21 UTC
README
Laravel Notification Preferences
A Laravel package for managing user notification preferences with support for multiple channels, notification groups, and automatic channel filtering — perfect for building notification settings UIs.
Features
- Automatic Filtering — All notifications respect user preferences without code changes
- Multiple Channels — Support for mail, database, broadcast, SMS, or custom channels
- Notification Grouping — Organize notifications into logical groups (system, marketing, etc.)
- Forced Channels — Critical notifications that users cannot disable
- Bulk Operations — Disable all emails, mute a group, or toggle notification types
- Structured Output — UI-ready table structure for building preference pages
- Opt-in/Opt-out Defaults — Configure default behavior at global, group, or notification level
- Event Dispatching — Listen for preference changes for audit logging or sync
- Email Unsubscribe Links — Signed URLs for one-click unsubscribe with
List-Unsubscribeheader support - Input Validation — Prevents setting preferences for unregistered notifications/channels
Requirements
- PHP 8.3+
- Laravel 11/12/13
Installation
composer require offload-project/laravel-notification-preferences
Publish the config and migrations:
php artisan vendor:publish --tag=notification-preferences-config php artisan vendor:publish --tag=notification-preferences-migrations php artisan migrate
Quick Start
1. Add the trait to your User model:
use OffloadProject\NotificationPreferences\Concerns\HasNotificationPreferences; class User extends Authenticatable { use HasNotificationPreferences; }
2. Register your notifications in config/notification-preferences.php:
return [ 'channels' => [ 'mail' => ['label' => 'Email', 'enabled' => true], 'database' => ['label' => 'In-App', 'enabled' => true], ], 'groups' => [ 'system' => [ 'label' => 'System Notifications', 'description' => 'Important system updates', 'default_preference' => 'opt_in', 'order' => 1, ], ], 'notifications' => [ \App\Notifications\OrderShipped::class => [ 'group' => 'system', 'label' => 'Order Shipped', 'description' => 'When your order ships', 'order' => 1, ], ], ];
3. Send notifications normally — preferences are applied automatically:
$user->notify(new OrderShipped($order));
Managing Preferences
// Set a preference $user->setNotificationPreference(OrderShipped::class, 'mail', false); // Check a preference $enabled = $user->getNotificationPreference(OrderShipped::class, 'mail'); // Get all preferences $preferences = $user->getNotificationPreferences(); // Get structured table for UI $table = $user->getNotificationPreferencesTable();
Bulk Operations
Convenient methods for "disable all emails" or "mute marketing" features:
// Disable all emails $user->setChannelPreferences('mail', false); // Mute all marketing notifications for email $user->setGroupPreferences('marketing', 'mail', false); // Disable all channels for a notification type $user->setNotificationChannelPreferences(OrderShipped::class, false); // Reset all preferences to defaults $user->resetNotificationPreferences();
All bulk methods return the count of updated preferences and automatically skip forced channels.
Explicit Control with Trait
For granular control, use the ChecksNotificationPreferences trait in your notification:
use OffloadProject\NotificationPreferences\Concerns\ChecksNotificationPreferences; class OrderShipped extends Notification { use ChecksNotificationPreferences; public function via($notifiable) { return $this->allowedChannels($notifiable, ['mail', 'database', 'broadcast']); } }
Forced Channels
Prevent users from disabling critical notifications:
'notifications' => [ SecurityAlert::class => [ 'group' => 'security', 'label' => 'Security Alerts', 'force_channels' => ['mail', 'database'], ], ],
Per-Channel Defaults
Set specific channels enabled by default:
'notifications' => [ OrderShipped::class => [ 'group' => 'system', 'label' => 'Order Shipped', 'default_channels' => ['mail', 'database'], // Only these enabled by default ], ],
Events
The package dispatches events when preferences change:
use OffloadProject\NotificationPreferences\Events\NotificationPreferenceChanged; Event::listen(NotificationPreferenceChanged::class, function ($event) { // $event->preference - The NotificationPreference model // $event->user - The user who changed the preference // $event->wasCreated - Whether this was a new preference or update });
Using the Facade
For quick access without dependency injection:
use OffloadProject\NotificationPreferences\Facades\NotificationPreferences; // Check if a channel is enabled NotificationPreferences::isChannelEnabled($user, OrderShipped::class, 'mail'); // Set a preference NotificationPreferences::setPreference($user, OrderShipped::class, 'mail', false); // Get structured table for UI NotificationPreferences::getPreferencesTable($user); // Discover registered configuration NotificationPreferences::getRegisteredChannels(); // ['mail', 'database'] NotificationPreferences::getRegisteredGroups(); // ['system', 'marketing'] NotificationPreferences::getRegisteredNotifications(); // [OrderShipped::class, ...]
Using the Interface
For dependency injection and testing, use the interface:
use OffloadProject\NotificationPreferences\Contracts\NotificationPreferenceManagerInterface; class NotificationPreferenceController { public function __construct( private NotificationPreferenceManagerInterface $manager ) {} public function update(Request $request) { $this->manager->setPreference( $request->user(), $request->notification_type, $request->channel, $request->enabled ); } }
Table Structure Output
The getNotificationPreferencesTable() method returns UI-ready data:
[
[
'group' => 'system',
'label' => 'System Notifications',
'description' => 'Important system updates',
'notifications' => [
[
'type' => 'App\Notifications\OrderShipped',
'label' => 'Order Shipped',
'description' => 'When your order ships',
'channels' => [
'mail' => ['enabled' => true, 'forced' => false],
'database' => ['enabled' => true, 'forced' => false],
],
],
],
],
]
Inertia.js Integration
Share preferences via middleware:
// app/Http/Middleware/HandleInertiaRequests.php public function share(Request $request): array { return [ ...parent::share($request), 'notificationPreferences' => fn () => $request->user()?->getNotificationPreferencesTable(), ]; }
Cache Management
Preferences are cached for performance (default: 24 hours). Configure the TTL in your config:
// config/notification-preferences.php 'cache_ttl' => 1440, // minutes (default: 24 hours)
Clear caches when needed:
use OffloadProject\NotificationPreferences\Contracts\NotificationPreferenceManagerInterface; $manager = app(NotificationPreferenceManagerInterface::class); // Clear all cached preferences for a user $manager->clearUserCache($userId); // Clear the memoized config cache (useful after runtime config changes) $manager->clearConfigCache();
Exception Handling
The package validates all inputs and throws specific exceptions with helpful messages:
use OffloadProject\NotificationPreferences\Exceptions\InvalidNotificationTypeException; use OffloadProject\NotificationPreferences\Exceptions\InvalidChannelException; use OffloadProject\NotificationPreferences\Exceptions\InvalidGroupException; try { $user->setNotificationPreference('UnregisteredNotification', 'mail', false); } catch (InvalidNotificationTypeException $e) { // "Notification type 'UnregisteredNotification' is not registered... // Add it to the 'notifications' array in 'config/notification-preferences.php'." } try { $user->setNotificationPreference(OrderShipped::class, 'sms', false); } catch (InvalidChannelException $e) { // "Channel 'sms' is not registered... Available channels: mail, database." } try { $user->setGroupPreferences('nonexistent', 'mail', false); } catch (InvalidGroupException $e) { // "Group 'nonexistent' is not registered... Available groups: system, marketing." }
Email Unsubscribe Links
The package can generate signed URLs that let users unsubscribe directly from emails — no login required. It also supports List-Unsubscribe headers for native unsubscribe buttons in Gmail, Apple Mail, and other clients (RFC 8058).
Adding Unsubscribe Links to Notifications
Use the HasUnsubscribeUrl trait on your notification class:
use OffloadProject\NotificationPreferences\Concerns\HasUnsubscribeUrl; class OrderShipped extends Notification { use HasUnsubscribeUrl; public function toMail($notifiable): MailMessage { return $this->withUnsubscribeHeaders( (new MailMessage) ->line('Your order has shipped!') ->action('Unsubscribe', $this->getUnsubscribeUrl($notifiable)), $notifiable ); } }
withUnsubscribeHeaders() adds List-Unsubscribe and List-Unsubscribe-Post headers so email clients can show native unsubscribe buttons. The getUnsubscribeUrl() method generates a signed URL you can place anywhere in the email body.
Generating URLs Directly
You can also generate URLs from the user model or facade:
// From the user model $url = $user->notificationUnsubscribeUrl(OrderShipped::class); $url = $user->notificationResubscribeUrl(OrderShipped::class); // From the facade use OffloadProject\NotificationPreferences\Facades\NotificationPreferences; $url = NotificationPreferences::unsubscribeUrl($user, OrderShipped::class); $url = NotificationPreferences::resubscribeUrl($user, OrderShipped::class); // For a specific channel (defaults to 'mail') $url = NotificationPreferences::unsubscribeUrl($user, OrderShipped::class, 'database');
How It Works
When a user clicks the unsubscribe link, the package:
- Validates the signed URL (tamper-proof, no auth required)
- Disables the notification type for that channel
- Returns a JSON response, or redirects to your configured URL
The POST method is also supported for RFC 8058 one-click unsubscribe from email clients.
Configuration
// config/notification-preferences.php 'unsubscribe' => [ // Whether to register unsubscribe routes 'enabled' => true, // Route prefix for unsubscribe/resubscribe endpoints 'route_prefix' => 'notification-preferences', // Middleware for the unsubscribe routes 'middleware' => ['web'], // Signed URL TTL in minutes (null for permanent/non-expiring) 'url_ttl' => null, // Redirect to this URL after unsubscribing (with status, notification_type, and channel query params) // Set to null to return a JSON response instead 'redirect_url' => null, // Enable resubscribe functionality 'resubscribe_enabled' => true, ],
Redirect Example
When redirect_url is set, users are redirected after unsubscribing:
'redirect_url' => '/notification-settings', // Redirects to: /notification-settings?status=unsubscribed¬ification_type=App\Notifications\OrderShipped&channel=mail
This lets you handle the confirmation page in your own frontend (Blade, Inertia, Livewire, etc.).
Uninstalling
php artisan notification-preferences:uninstall --force composer remove offload-project/laravel-notification-preferences rm config/notification-preferences.php
Configuration Reference
Global Options
| Option | Type | Description |
|---|---|---|
default_preference |
string | opt_in or opt_out for all notifications |
cache_ttl |
int | Cache duration in minutes (default: 1440 = 24h) |
table_name |
string | Database table name (default: notification_preferences) |
user_model |
string | User model class (default: App\Models\User) |
Channels
| Option | Type | Description |
|---|---|---|
label |
string | Display name for UI |
enabled |
bool | Whether channel is available (default: true) |
Groups
| Option | Type | Description |
|---|---|---|
label |
string | Display name for UI |
description |
string | Optional description for UI |
default_preference |
string | opt_in or opt_out (overrides global) |
order |
int | Sort order in UI |
Notifications
| Option | Type | Description |
|---|---|---|
group |
string | Group key this notification belongs to |
label |
string | Display name for UI |
description |
string | Optional description for UI |
default_preference |
string | opt_in or opt_out (overrides group) |
default_channels |
array | Specific channels enabled by default |
force_channels |
array | Channels that cannot be disabled |
order |
int | Sort order within group |
Unsubscribe
| Option | Type | Description |
|---|---|---|
enabled |
bool | Register unsubscribe routes (default: true) |
route_prefix |
string | URL prefix for routes (default: notification-preferences) |
middleware |
array | Middleware stack for routes (default: ['web']) |
url_ttl |
int|null | Signed URL expiration in minutes (null = permanent) |
redirect_url |
string|null | Redirect after action, or null for JSON response |
resubscribe_enabled |
bool | Register resubscribe route (default: true) |
Testing
./vendor/bin/pest
License
The MIT License (MIT). Please see License File for more information.