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.
- 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
- Input Validation — Prevents setting preferences for unregistered notifications/channels
- PHP 8.3+
- Laravel 11+
composer require offload-project/laravel-notification-preferencesPublish the config and migrations:
php artisan vendor:publish --tag=notification-preferences-config
php artisan vendor:publish --tag=notification-preferences-migrations
php artisan migrate1. 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));// 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();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.
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']);
}
}Prevent users from disabling critical notifications:
'notifications' => [
SecurityAlert::class => [
'group' => 'security',
'label' => 'Security Alerts',
'force_channels' => ['mail', 'database'],
],
],Set specific channels enabled by default:
'notifications' => [
OrderShipped::class => [
'group' => 'system',
'label' => 'Order Shipped',
'default_channels' => ['mail', 'database'], // Only these enabled by default
],
],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
});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, ...]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
);
}
}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],
],
],
],
],
]Share preferences via middleware:
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'notificationPreferences' => fn () => $request->user()?->getNotificationPreferencesTable(),
];
}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();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."
}php artisan notification-preferences:uninstall --force
composer remove offload-project/laravel-notification-preferences
rm config/notification-preferences.php| 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) |
| Option | Type | Description |
|---|---|---|
label |
string | Display name for UI |
enabled |
bool | Whether channel is available (default: true) |
| 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 |
| 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 |
./vendor/bin/pestThe MIT License (MIT). Please see License File for more information.