sysmatter / laravel-notification-preferences
Laravel package for managing user notification preferences across channels.
Requires
- php: ^8.2|^8.3|^8.4
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- larastan/larastan: ^3.7
- laravel/pint: ^1.25
- orchestra/testbench: ^10.6
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.2
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11
- roave/security-advisories: dev-latest
This package is auto-updated.
Last update: 2025-10-01 00:29:07 UTC
README
A Laravel package that allows users to manage their notification preferences across different channels. Users can enable or disable specific notifications for email, SMS, push notifications, and more.
Features
- ๐ง Seamless Integration: Works with Laravel's built-in notification system
- ๐ Table-Ready Output: Perfect for building settings forms with notification/channel grids
- ๐ Automatic Filtering: Notifications are automatically filtered based on user preferences
- ๐พ Caching Support: Built-in caching for performance optimization
- ๐ฏ Channel Flexibility: Support for any notification channel (mail, SMS, push, database, etc.)
- ๐งช Fully Tested: Comprehensive test suite with Pest
Requirements
- PHP 8.2, 8.3, 8.4
- Laravel 11, 12
Installation
Install the package via Composer:
composer require sysmatter/laravel-notification-preferences
Publish and run the migrations:
php artisan vendor:publish --tag="notification-preferences-migrations"
php artisan migrate
Optionally, publish the config file:
php artisan vendor:publish --tag="notification-preferences-config"
This is the contents of the published config file:
return [ /* |-------------------------------------------------------------------------- | User Model |-------------------------------------------------------------------------- | | The model that represents users in your application | */ 'user_model' => env('NOTIFICATION_PREFERENCES_USER_MODEL', 'App\Models\User'), /* |-------------------------------------------------------------------------- | Default Channels |-------------------------------------------------------------------------- | | Default notification channels available in your application | */ 'default_channels' => [ 'mail' => 'Email', 'database' => 'In-App', 'sms' => 'SMS', 'push' => 'Push Notifications', ], /* |-------------------------------------------------------------------------- | Default Preferences |-------------------------------------------------------------------------- | | Default preference state for new notifications/users | */ 'default_enabled' => true, /* |-------------------------------------------------------------------------- | Cache Settings |-------------------------------------------------------------------------- | | Cache preferences for performance | */ 'cache' => [ 'enabled' => true, 'ttl' => 3600, // 1 hour 'prefix' => 'notification_preferences', ], ];
Usage
1. Add the Trait to Your User Model
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use SysMatter\NotificationPreferences\Traits\HasNotificationPreferences; class User extends Authenticatable { use HasNotificationPreferences; // ... rest of your user model }
2. Register Your Notifications
In your AppServiceProvider
:
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use SysMatter\NotificationPreferences\NotificationRegistry; use App\Notifications\OrderShipped; use App\Notifications\PaymentReceived; class AppServiceProvider extends ServiceProvider { public function boot(): void { $registry = app(NotificationRegistry::class); $registry->register( OrderShipped::class, 'Order Updates', ['mail', 'database', 'sms'] ); $registry->register( PaymentReceived::class, 'Payment Notifications', ['mail', 'database'] ); } }
3. Update Your Notification Classes
You have two options for making notifications preference-aware:
Option A: Extend the Base Class (Recommended for simplicity)
<?php namespace App\Notifications; use SysMatter\NotificationPreferences\PreferenceAwareNotification; class OrderShipped extends PreferenceAwareNotification { // That's it! By default, uses all channels from config // Optional: Override to customize channels for this notification protected function getOriginalChannels($notifiable): array { return ['mail', 'database', 'sms']; } // ... rest of your notification implementation }
Option B: Use the Trait Directly
<?php namespace App\Notifications; use Illuminate\Notifications\Notification; use SysMatter\NotificationPreferences\Traits\HasPreferenceAwareNotifications; class OrderShipped extends Notification { use HasPreferenceAwareNotifications; protected function getOriginalChannels($notifiable): array { return ['mail', 'database', 'sms']; } // ... rest of your notification implementation }
4. Create a Settings Form
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class NotificationPreferencesController extends Controller { public function show(Request $request) { $request->user()->getNotificationPreferencesTable(); return view('notification-preferences', compact('preferencesTable')); } public function update(Request $request) { $request->validate([ 'preferences' => 'required|array', 'preferences.*' => 'array', 'preferences.*.*' => 'boolean', ]); $request->user()->updateNotificationPreferences($request->input('preferences')); return back()->with('success', 'Preferences updated successfully!'); } }
5. Build the Settings UI
The package provides table-ready data structure:
@foreach($preferencesTable as $notification) <tr> <td>{{ $notification['notification_name'] }}</td> @foreach($notification['channels'] as $channel => $channelData) <td> <input type="checkbox" name="preferences[{{ $notification['notification_type'] }}][{{ $channel }}]" value="1" {{ $channelData['enabled'] ? 'checked' : '' }} > {{ $channelData['name'] }} </td> @endforeach </tr> @endforeach
Usage with Inertia.js + React
Controller Setup
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; class NotificationPreferencesController extends Controller { public function show(Request $request): Response { return Inertia::render('account/notification-preferences', [ 'preferencesTable' => $request->user()->getNotificationPreferencesTable(), ]); } public function update(Request $request): RedirectResponse { $request->validate([ 'preferences' => 'required|array', 'preferences.*' => 'array', 'preferences.*.*' => 'boolean', ]); $request->user()->updateNotificationPreferences($request->input('preferences')); return back()->with('success', 'Notification preferences updated successfully!'); } }
Form-Based React Component
// resources/js/Pages/NotificationPreferences.tsx import React from 'react'; import {Head, useForm} from '@inertiajs/react'; interface Channel { name: string; enabled: boolean; } interface NotificationRow { notification_type: string; notification_name: string; channels: Record<string, Channel>; } interface Props { preferencesTable: NotificationRow[]; flash?: { success?: string; }; } export default function NotificationPreferences({preferencesTable, flash}: Props) { // Use Inertia's useForm with the preferences data structure const {data, setData, post, processing} = useForm({ preferences: Object.fromEntries( preferencesTable.map(notification => [ notification.notification_type, Object.fromEntries( Object.entries(notification.channels).map(([channel, channelData]) => [ channel, channelData.enabled ]) ) ]) ) }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); post(route('notification-preferences.update')); }; const updatePreference = (notificationType: string, channel: string, enabled: boolean) => { setData('preferences', { ...data.preferences, [notificationType]: { ...data.preferences[notificationType], [channel]: enabled } }); }; // Get all unique channels for headers const allChannels = Array.from(new Set( preferencesTable.flatMap(n => Object.keys(n.channels)) )); const getChannelName = (channel: string): string => { const firstNotificationWithChannel = preferencesTable.find(n => n.channels[channel]); return firstNotificationWithChannel?.channels[channel]?.name || channel; }; return ( <> <Head title="Notification Preferences"/> <div className="max-w-6xl mx-auto py-8 px-4"> {flash?.success && ( <div className="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded"> {flash.success} </div> )} <h1 className="text-2xl font-bold mb-6">Notification Preferences</h1> <div className="bg-white shadow rounded-lg overflow-hidden"> <form onSubmit={handleSubmit}> <div className="overflow-x-auto"> <table className="w-full"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Notification Type </th> {allChannels.map(channel => ( <th key={channel} className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> {getChannelName(channel)} </th> ))} </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {preferencesTable.map(notification => ( <tr key={notification.notification_type} className="hover:bg-gray-50"> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"> {notification.notification_name} </td> {allChannels.map(channel => { const channelData = notification.channels[channel]; return ( <td key={channel} className="px-6 py-4 whitespace-nowrap text-center"> {channelData ? ( <input type="checkbox" checked={data.preferences[notification.notification_type]?.[channel] || false} onChange={(e) => updatePreference( notification.notification_type, channel, e.target.checked )} className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" /> ) : ( <span className="text-gray-400">โ</span> )} </td> ); })} </tr> ))} </tbody> </table> </div> <div className="px-6 py-4 bg-gray-50 text-right"> <button type="submit" disabled={processing} className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50" > {processing ? 'Saving...' : 'Save Preferences'} </button> </div> </form> </div> </div> </> ); }
Routes
// routes/web.php Route::middleware(['auth'])->group(function () { Route::get('/notification-preferences', [NotificationPreferencesController::class, 'show']) ->name('notification-preferences.show'); Route::post('/notification-preferences', [NotificationPreferencesController::class, 'update']) ->name('notification-preferences.update'); });
Key Advantages of the Inertia Form-Based Approach
- Simple State Management: Uses Inertia's
useForm
- no complex React state - Type Safety: Full TypeScript support with proper interfaces
- Automatic Form Handling: Inertia manages serialization and validation
- Loading States: Built-in
processing
state for UI feedback - Flash Messages: Automatic success/error message handling
- Progressive Enhancement: Works even if JavaScript fails
Usage
Setting Individual Preferences
// Disable email notifications for order updates $user->setNotificationPreference(OrderShipped::class, 'mail', false); // Enable SMS notifications for payments $user->setNotificationPreference(PaymentReceived::class, 'sms', true);
Getting Preferences
// Check if user wants email notifications for orders $wantsEmail = $user->getNotificationPreference(OrderShipped::class, 'mail'); // Get the full table structure for building forms $preferencesTable = $user->getNotificationPreferencesTable();
Bulk Updates
$preferences = [ OrderShipped::class => [ 'mail' => true, 'sms' => false, 'database' => true, ], PaymentReceived::class => [ 'mail' => false, 'database' => true, ], ]; $user->updateNotificationPreferences($preferences);
Sending Notifications (Automatic Filtering)
Just send notifications normally - the package automatically respects user preferences:
// This will only send via channels the user has enabled $user->notify(new OrderShipped($order));
Configuration
The config file allows you to customize:
return [ // User model to use 'user_model' => env('NOTIFICATION_PREFERENCES_USER_MODEL', 'App\Models\User'), // Available notification channels 'default_channels' => [ 'mail' => 'Email', 'database' => 'In-App', 'sms' => 'SMS', 'push' => 'Push Notifications', ], // Default state for new notifications 'default_enabled' => true, // Caching settings 'cache' => [ 'enabled' => true, 'ttl' => 3600, // 1 hour 'prefix' => 'notification_preferences', ], ];
API Reference
User Methods (via HasNotificationPreferences
trait)
// Get a specific preference $user->getNotificationPreference(string $notificationType, string $channel): bool // Set a specific preference $user->setNotificationPreference(string $notificationType, string $channel, bool $enabled): void // Get table structure for forms $user->getNotificationPreferencesTable(): array // Bulk update preferences $user->updateNotificationPreferences(array $preferences): void // Get the relationship $user->notificationPreferences(): HasMany
Registry Methods
$registry = app(\SysMatter\NotificationPreferences\NotificationRegistry::class); // Register a notification type $registry->register(string $notificationClass, string $name, array $channels): void // Check if notification is registered $registry->isRegistered(string $notificationClass): bool // Get available channels for a notification $registry->getChannelsForNotification(string $notificationClass): array // Get all registered notifications $registry->getRegisteredNotifications(): array
Table Structure
The getNotificationPreferencesTable()
method returns data structured like this:
[ [ 'notification_type' => 'App\Notifications\OrderShipped', 'notification_name' => 'Order Updates', 'channels' => [ 'mail' => [ 'name' => 'Email', 'enabled' => true ], 'sms' => [ 'name' => 'SMS', 'enabled' => false ] ] ], // ... more notifications ]
This structure makes it easy to build tables where:
- Each row is a notification type
- Each column is a channel
- Each cell shows if that notification/channel combination is enabled
Testing
Run tests with:
composer test
Run tests with coverage:
composer test-coverage
Run static analysis:
composer analyse
Format code:
composer format
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.