sameoldnick / laravel-notification-preferences
A Laravel package for managing user notification preferences by notification type and delivery channel.
Package info
github.com/SameOldNick/laravel-notification-preferences
pkg:composer/sameoldnick/laravel-notification-preferences
Requires
- php: ^8.4
- illuminate/contracts: ^11.0||^12.0||^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/fortify: ^1.37
- laravel/pint: ^1.30
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^11.0.0||^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Let users choose which channels each type of notification is delivered through, while guaranteeing that important notifications always reach them.
Requirements
- PHP 8.4 or higher
- Laravel 11, 12, or 13
- Composer
Installation
Install the package with Composer:
composer require sameoldnick/laravel-notification-preferences
Publish the configuration file and the migration:
php artisan vendor:publish --tag=notification-preferences-config php artisan vendor:publish --tag=notification-preferences-migrations
Run the migration:
php artisan migrate
Add the HasNotificationPreferences trait to the notifiable model, usually your User model:
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use SameOldNick\NotificationPreferences\Concerns\HasNotificationPreferences; class User extends Authenticatable { use HasNotificationPreferences; // ... }
Configuration
The published configuration file is config/notification-preferences.php.
Channels
Three top-level keys configure channels:
channels— the channels that can be offered, each with a label.available_channels— the channels offered to users by default. Users can enable or disable each one.required_channels— the channels that are always sent by default, regardless of user preferences. Users cannot disable these.
<?php // config/notification-preferences.php return [ 'channels' => [ 'mail' => [ 'label' => 'Email', ], 'database' => [ 'label' => 'In-app', ], 'ntfy' => [ 'label' => 'Ntfy', ], ], 'available_channels' => [ 'mail', 'database', 'ntfy', ], 'required_channels' => [ 'database', ], ];
- Channel keys must match the channel names your notifications return from
via(), such asmail,database,broadcast, or a channel class name. - Labels may be plain strings or translation keys.
- A required channel does not have to appear in
available_channels; it is appended to the resolved channels either way. - The published file contains example values. Replace them with the channels your application actually sends through, and only list required channels your application can deliver.
Note:
channelsprovides display metadata (used byNotificationCatalog::notificationChannels()).available_channelsandrequired_channelscontrol delivery behavior.
Notification types
Each notification type is keyed by the value returned from its notificationType() method and defines a label and a category. Both may be translation keys.
'types' => [ 'my-notification' => [ 'label' => 'My Notification', 'category' => 'General', ], ],
Per-type channel overrides
A notification type may override the global channel lists:
'types' => [ 'security-alert' => [ 'label' => 'Security Alert', 'category' => 'Security', 'available_channels' => ['mail', 'database'], 'required_channels' => ['mail'], ], ],
- Omitting a key or setting it to
nullfalls back to the matching global list. - An empty array opts out of the global list. For example,
'required_channels' => []means no channel is forced for that type. - Overrides only apply to the type that declares them.
- A type with no available channels (global or overridden) produces no preference entries.
How the channel list is resolved
When a notification that uses RoutesViaNotifiablePreferences is sent, its channels are resolved in this order:
- Start with the available channels for the notification type.
- Remove channels the notifiable has explicitly disabled.
- Append the required channels for the notification type.
- Remove duplicates.
Channels with no stored preference are treated as enabled, so newly added channels still reach existing users.
Usage
Add the RoutesViaNotifiablePreferences trait to every notification that should respect preferences:
<?php namespace App\Notifications; use Illuminate\Notifications\Notification; use SameOldNick\NotificationPreferences\Concerns\RoutesViaNotifiablePreferences; class MyNotification extends Notification { use RoutesViaNotifiablePreferences; // ... }
The trait provides via(). Do not also define via() on the notification, or it will take precedence and bypass preference routing.
Mapping a notification to a type
By default, notificationType() returns the notification's fully-qualified class name, which must exist as a key in notification-preferences.types. To use a shorter key instead, override the method:
public function notificationType(): string { return 'my-notification'; }
The returned value must match a key in notification-preferences.types.
Reading and updating preferences
The package does not ship routes or controllers. Resolve NotificationPreferencesService from the container and expose it through your own application:
use SameOldNick\NotificationPreferences\Services\NotificationPreferencesService; $service = app(NotificationPreferencesService::class); // Keyed by "{channel}:{type}", for example "mail:my-notification". $preferences = $service->getPreferencesForNotifiable($user); $service->updatePreferencesForNotifiable($user, [ 'my-notification' => [ 'channels' => [ 'mail' => false, 'database' => true, ], ], ]);
getPreferencesForNotifiable() returns type labels and categories, but only channel keys. To display channel names (for example, as column headings), resolve the catalog and call notificationChannels():
use SameOldNick\NotificationPreferences\Contracts\NotificationCatalog; $channels = app(NotificationCatalog::class)->notificationChannels(); // [ // 'mail' => ['label' => 'Email'], // 'database' => ['label' => 'In-app'], // ]
Channel labels may be translation keys, so pass them through __() before displaying them.
Validate the incoming types and channels against SameOldNick\NotificationPreferences\Contracts\NotificationCatalog before calling updatePreferencesForNotifiable(), since the service stores whatever it is given.
Custom notification catalogs
NotificationCatalog is bound to the config-backed NotificationCatalogConfigProvider. To source types or channels from somewhere else — a database, a tenant, or a user's plan — bind your own implementation in a service provider:
<?php namespace App\Providers; use App\Support\DatabaseNotificationCatalog; use Illuminate\Support\ServiceProvider; use SameOldNick\NotificationPreferences\Contracts\NotificationCatalog; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(NotificationCatalog::class, DatabaseNotificationCatalog::class); } }
Both RoutesViaNotifiablePreferences and NotificationPreferencesService resolve the interface from the container, so replacing the binding changes routing and preferences together. Implement all six interface methods, return list<string> channel keys, and keep the singleton free of per-notifiable state — availableNotificationChannelsFor() and requiredNotificationChannelsFor() receive the notifiable for that reason.
Laravel Boost
This package ships AI guidelines and an on-demand agent skill for Laravel Boost:
- Guidelines:
resources/boost/guidelines/core.blade.php - Skill:
resources/boost/skills/notification-preferences-development/SKILL.md
Both are installed automatically when an application runs php artisan boost:install (skills depend on the user selecting the skills feature).
Development
composer install
composer test
composer lint
composer format
Security
Please review SECURITY.md for the vulnerability reporting policy.
License
This package is open-sourced software licensed under the MIT license.