codinglabsau/laravel-notification-subscriptions

Manage user notification subscriptions across multiple notification types and channels

Installs: 56

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/codinglabsau/laravel-notification-subscriptions

v1.0.0 2026-01-16 02:01 UTC

This package is auto-updated.

Last update: 2026-01-29 02:19:41 UTC


README

Latest Version on Packagist Test Total Downloads

A Laravel package for managing user notification preferences across multiple channels. Let your users control how they receive notifications (email, in-app, push, Slack) while you maintain sensible defaults and rate limiting.

Features

  • Channel-based subscriptions - Users can enable/disable notifications per channel
  • Custom channels - Define your own channel enum with any channels you need (mail, database, Slack, Pusher, OneSignal, etc.)
  • Per-notification control - Configure which channels each notification type supports
  • Smart defaults - New users get sensible defaults; preferences are only stored when changed
  • Rate limiting - Prevent notification spam with configurable per-channel rate limits
  • System channels - Mark channels that users can't opt out of (like in-app notifications)
  • Simple API - $user->getNotificationPreferences() and $user->updateNotificationPreferences() for settings UIs

Installation

1. Install via Composer

composer require codinglabsau/laravel-notification-subscriptions

2. Publish and run migrations

php artisan vendor:publish --tag="laravel-notification-subscriptions-migrations"
php artisan migrate

3. Publish configuration (optional)

php artisan vendor:publish --tag="laravel-notification-subscriptions-config"

4. Create your channel enum

Create an enum that implements SubscribableChannel to define your notification channels:

// app/Enums/NotificationChannel.php
namespace App\Enums;

use Codinglabs\NotificationSubscriptions\Contracts\SubscribableChannel;

enum NotificationChannel: string implements SubscribableChannel
{
    case DATABASE = 'database';
    case MAIL = 'mail';
    case SLACK = 'slack';

    public function driver(): string
    {
        return $this->value;
    }

    public function label(): string
    {
        return match ($this) {
            self::DATABASE => 'In-App',
            self::MAIL => 'Email',
            self::SLACK => 'Slack',
        };
    }

    public function isEnabled(): bool
    {
        return true;
    }

    public function defaultOn(): bool
    {
        return match ($this) {
            self::SLACK => false,
            default => true,
        };
    }

    public function hasRateLimiting(): bool
    {
        return match ($this) {
            self::DATABASE => false,
            default => true,
        };
    }

    public function rateLimitDuration(): int
    {
        return match ($this) {
            self::MAIL => 300, // 5 minutes
            default => 60,
        };
    }

    public function isSystemChannel(): bool
    {
        return $this === self::DATABASE;
    }
}

5. Add trait to your User model

use Codinglabs\NotificationSubscriptions\Concerns\HasNotificationSubscriptions;

class User extends Authenticatable
{
    use HasNotificationSubscriptions;

    // ...
}

6. Publish and configure the service provider

php artisan vendor:publish --tag="laravel-notification-subscriptions-provider"

Then register your subscribable notifications in app/Providers/NotificationSubscriptionsServiceProvider.php:

use Codinglabs\NotificationSubscriptions\Facades\NotificationSubscriptions;

public function boot(): void
{
    NotificationSubscriptions::register([
        \App\Notifications\OrderShippedNotification::class,
        \App\Notifications\NewMessageNotification::class,
    ]);

    // Conditional registration
    if (config('features.slack_enabled')) {
        NotificationSubscriptions::register([
            \App\Notifications\SlackAlertNotification::class,
        ]);
    }
}

Don't forget to add this service provider to your bootstrap/providers.php:

return [
    // ...
    App\Providers\NotificationSubscriptionsServiceProvider::class,
];

Basic Usage

Creating a Subscribable Notification

Transform any Laravel notification into a subscribable notification by implementing the SubscribableNotification interface and using the DispatchesNotifications trait:

use App\Enums\NotificationChannel;
use Illuminate\Notifications\Notification;
use Codinglabs\NotificationSubscriptions\Concerns\DispatchesNotifications;
use Codinglabs\NotificationSubscriptions\Contracts\SubscribableNotification;

class OrderShippedNotification extends Notification implements SubscribableNotification
{
    use DispatchesNotifications;

    public function __construct(
        public Order $order
    ) {}

    // Unique identifier for this notification type
    public static function type(): string
    {
        return 'order_shipped';
    }

    // Which channels this notification supports
    public static function channels(): array
    {
        return [NotificationChannel::DATABASE, NotificationChannel::MAIL];
    }

    // Who should receive this notification
    public function subscribers()
    {
        return collect([$this->order->user]);
    }

    // Standard Laravel notification methods
    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('Your order has shipped!')
            ->line("Order #{$this->order->id} is on its way.");
    }

    public function toArray($notifiable)
    {
        return [
            'title' => 'Order Shipped',
            'message' => "Order #{$this->order->id} has shipped.",
            'order_id' => $this->order->id,
        ];
    }
}

Dispatching Notifications

Use the static sendToSubscribers() method instead of Laravel's standard notification sending:

// This automatically:
// 1. Finds all subscribers
// 2. Checks each user's channel preferences
// 3. Applies rate limiting
// 4. Sends to appropriate channels only

OrderShippedNotification::sendToSubscribers($order);

Transactional vs Subscribable Notifications

Use Case Method Behavior
Transactional (password reset, order confirmation) Standard Laravel $user->notify() Always sends, no filtering
Subscribable (messages, updates, marketing) Notification::sendToSubscribers() Respects user preferences
// Subscribable - respects user preferences
OrderShippedNotification::sendToSubscribers($order);

// Transactional - always sends (standard Laravel)
$user->notify(new PasswordResetNotification());

How It Works

When a notification is dispatched:

  1. Subscriber lookup - The subscribers() method determines who should receive the notification
  2. Channel filtering - For each subscriber, the package checks their preferences:
    • If they have a stored preference for this notification type, only enabled channels are used
    • If no preference exists, channels with defaultOn() === true are used
  3. Rate limiting - If a channel has rate limiting enabled and the notification has a subject, duplicate notifications are throttled
  4. Delivery - The notification is sent only to the appropriate channels

Channel Enum Reference

Your channel enum must implement SubscribableChannel with these methods:

Method Return Type Description
driver() string Laravel notification channel driver (e.g., 'database', 'mail', OneSignalChannel::class)
label() string Human-readable label for UI (e.g., 'Email', 'Push Notifications')
isEnabled() bool Whether this channel is currently available
defaultOn() bool Whether new users have this channel enabled by default
hasRateLimiting() bool Whether rate limiting applies to this channel
rateLimitDuration() int Rate limit duration in seconds
isSystemChannel() bool Whether users can opt out of this channel

Example: Advanced Channel Configuration

use NotificationChannels\OneSignal\OneSignalChannel;

enum NotificationChannel: string implements SubscribableChannel
{
    case DATABASE = 'database';
    case MAIL = 'mail';
    case PUSH = OneSignalChannel::class;
    case SLACK = 'slack';

    public function driver(): string
    {
        return $this->value;
    }

    public function label(): string
    {
        return match ($this) {
            self::DATABASE => 'In-App',
            self::MAIL => 'Email',
            self::PUSH => 'Push Notifications',
            self::SLACK => 'Slack',
        };
    }

    public function isEnabled(): bool
    {
        return match ($this) {
            self::PUSH => config('services.onesignal.app_id') !== null,
            self::SLACK => config('services.slack.webhook_url') !== null,
            default => true,
        };
    }

    public function defaultOn(): bool
    {
        return match ($this) {
            self::PUSH, self::SLACK => false,
            default => true,
        };
    }

    public function hasRateLimiting(): bool
    {
        return match ($this) {
            self::DATABASE => false, // In-app doesn't need rate limiting
            default => true,
        };
    }

    public function rateLimitDuration(): int
    {
        return match ($this) {
            self::MAIL => 300,  // 5 minutes for emails
            self::PUSH => 60,   // 1 minute for push
            self::SLACK => 60,  // 1 minute for Slack
            default => config('notification-subscriptions.default_rate_limit_duration', 60),
        };
    }

    public function isSystemChannel(): bool
    {
        return $this === self::DATABASE; // Users can't opt out of in-app
    }
}

Rate Limiting

Rate limiting prevents notification spam when the same notification could be triggered multiple times in quick succession.

How Rate Limiting Works

Rate limits are applied per combination of:

  • Notification type (e.g., order_shipped)
  • Channel (e.g., mail)
  • Subject (the model that triggered the notification)
  • Recipient (the user receiving the notification)

Subject Method

For rate limiting to work, your notification must return a subject:

public function subject(): ?Model
{
    return $this->order;  // The model triggering the notification
}

If subject() returns null, rate limiting is skipped for that notification.

Building a Settings UI

The package provides a simple API for building notification preference UIs. The HasNotificationSubscriptions trait adds two methods to your User model:

  • getNotificationPreferences() - Returns a DTO with types and values for the UI
  • updateNotificationPreferences(array $preferences) - Updates preferences in the database

Controller Setup

use Codinglabs\NotificationSubscriptions\Concerns\ValidatesNotificationPreferences;

class NotificationSettingsController extends Controller
{
    public function edit()
    {
        return view('settings.notifications', [
            'preferences' => auth()->user()->getNotificationPreferences(),
        ]);
    }

    public function update(UpdateNotificationSettingsRequest $request)
    {
        auth()->user()->updateNotificationPreferences($request->validated());

        return redirect()->back()->with('success', 'Preferences saved.');
    }
}

// Form request - just add the trait, no configuration needed
class UpdateNotificationSettingsRequest extends FormRequest
{
    use ValidatesNotificationPreferences;
}

The ValidatesNotificationPreferences trait:

  • Generates validation rules for each registered notification type
  • Automatically re-injects system channels (users can't opt out of them)

The NotificationPreferences DTO

The getNotificationPreferences() method returns a NotificationPreferences object with two properties:

NotificationPreferences {
    // Channel options for the UI (excludes system channels)
    types: [
        'order_shipped' => ['mail' => 'Email', 'slack' => 'Slack'],
        'new_message' => ['mail' => 'Email'],
    ],
    // User's current selections (or defaults)
    values: [
        'order_shipped' => ['mail'],
        'new_message' => ['mail', 'slack'],
    ],
}

Example Blade Template

<form method="POST" action="{{ route('settings.notifications.update') }}">
    @csrf
    @method('PUT')

    @foreach($preferences->types as $notificationType => $channels)
        <div class="notification-group">
            <h3>{{ Str::title(str_replace('_', ' ', $notificationType)) }}</h3>

            @foreach($channels as $channel => $label)
                <label>
                    <input
                        type="checkbox"
                        name="{{ $notificationType }}[]"
                        value="{{ $channel }}"
                        @checked(in_array($channel, $preferences->values[$notificationType] ?? []))
                    />
                    {{ $label }}
                </label>
            @endforeach
        </div>
    @endforeach

    <button type="submit">Save Preferences</button>
</form>

Inertia/Vue Example

<script setup>
import { useForm } from '@inertiajs/vue3';

const props = defineProps({ preferences: Object });

// Initialize form directly from values
const form = useForm({ ...props.preferences.values });
</script>

<template>
    <form @submit.prevent="form.put('/settings/notifications')">
        <div v-for="(channels, type) in preferences.types" :key="type">
            <h3>{{ type }}</h3>

            <label v-for="(label, value) in channels" :key="value">
                <input
                    type="checkbox"
                    :value="value"
                    v-model="form[type]"
                />
                {{ label }}
            </label>
        </div>

        <button type="submit">Save</button>
    </form>
</template>

Advanced Usage

Custom Notification Labels

Add metadata methods to your notifications for richer UIs:

class OrderShippedNotification extends Notification implements SubscribableNotification
{
    use DispatchesNotifications;

    public static function type(): string
    {
        return 'order_shipped';
    }

    // Custom methods for UI (not part of interface)
    public static function label(): string
    {
        return 'Order Shipping Updates';
    }

    public static function description(): string
    {
        return 'Get notified when your orders ship and are delivered.';
    }

    // ...
}

Before Send Hook

Execute code before any notification is sent:

class OrderShippedNotification extends Notification implements SubscribableNotification
{
    use DispatchesNotifications;

    public static function beforeSend($notification): void
    {
        // Log, track analytics, modify notification, etc.
        Log::info('Sending order shipped notification', [
            'order_id' => $notification->order->id,
        ]);
    }

    // ...
}

Custom Subscription Model

Extend the base model if you need additional functionality:

// app/Models/NotificationSubscription.php
use Codinglabs\NotificationSubscriptions\Models\NotificationSubscription as BaseModel;

class NotificationSubscription extends BaseModel
{
    // Add custom methods, scopes, etc.
}

// config/notification-subscriptions.php
'subscription_model' => App\Models\NotificationSubscription::class,

Database Schema

The package creates a notification_subscriptions table:

Column Type Description
id bigint Primary key
user_id bigint Foreign key to users table
type string Notification type identifier
channels json Array of enabled channel names
created_at timestamp Creation timestamp
updated_at timestamp Last update timestamp

A unique constraint ensures one subscription record per user/type combination.

Testing

composer test

Credits

License

The MIT License (MIT). Please see License File for more information.