PHPackages                             codinglabsau/laravel-notification-subscriptions - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. codinglabsau/laravel-notification-subscriptions

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

codinglabsau/laravel-notification-subscriptions
===============================================

Manage user notification subscriptions across multiple notification types and channels

v1.0.0(3mo ago)016.1k↓40.7%MITPHPPHP ^8.3CI passing

Since Jan 29Pushed 3mo agoCompare

[ Source](https://github.com/codinglabsau/laravel-notification-subscriptions)[ Packagist](https://packagist.org/packages/codinglabsau/laravel-notification-subscriptions)[ Docs](https://github.com/codinglabsau/laravel-notification-subscriptions)[ RSS](/packages/codinglabsau-laravel-notification-subscriptions/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (6)Versions (3)Used By (0)

Laravel Notification Subscriptions
==================================

[](#laravel-notification-subscriptions)

[![Latest Version on Packagist](https://camo.githubusercontent.com/b6e95d44f558f049b85e9dfb9d8947417cf1db64cbafb170e23dc23de97d74d3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f636f64696e676c61627361752f6c61726176656c2d6e6f74696669636174696f6e2d737562736372697074696f6e732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/codinglabsau/laravel-notification-subscriptions)[![Test](https://github.com/codinglabsau/laravel-notification-subscriptions/actions/workflows/run-tests.yml/badge.svg)](https://github.com/codinglabsau/laravel-notification-subscriptions/actions/workflows/run-tests.yml)[![Total Downloads](https://camo.githubusercontent.com/284f9a68fcaa74bbd62d7c03eb79e0e2d9ddecb7eeb47a9563dd9efe79e04107/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f636f64696e676c61627361752f6c61726176656c2d6e6f74696669636174696f6e2d737562736372697074696f6e732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/codinglabsau/laravel-notification-subscriptions)

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
--------

[](#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
- **Mandatory channels** - Per-notification channels that users can't opt out of
- **Simple API** - `$user->getNotificationPreferences()` and `$user->updateNotificationPreferences()` for settings UIs

Installation
------------

[](#installation)

### 1. Install via Composer

[](#1-install-via-composer)

```
composer require codinglabsau/laravel-notification-subscriptions
```

### 2. Publish and run migrations

[](#2-publish-and-run-migrations)

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

### 3. Publish configuration (optional)

[](#3-publish-configuration-optional)

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

### 4. Create your channel enum

[](#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,
        };
    }
}
```

### 5. Add trait to your User model

[](#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

[](#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
-----------

[](#basic-usage)

### Creating a Subscribable Notification

[](#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

[](#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

[](#transactional-vs-subscribable-notifications)

Use CaseMethodBehavior**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

[](#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
----------------------

[](#channel-enum-reference)

Your channel enum must implement `SubscribableChannel` with these methods:

MethodReturn TypeDescription`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### Example: Advanced Channel Configuration

[](#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),
        };
    }
}
```

Rate Limiting
-------------

[](#rate-limiting)

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

### How Rate Limiting Works

[](#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

[](#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.

Mandatory Channels
------------------

[](#mandatory-channels)

Some notifications must always be sent via certain channels regardless of user preferences. For example, a support ticket reply might always need an email notification, even if the user has opted out of email for other notifications.

### Defining Mandatory Channels

[](#defining-mandatory-channels)

Override `mandatoryChannels()` in your notification class to specify channels that cannot be unsubscribed from:

```
class TicketReplyNotification extends Notification implements SubscribableNotification
{
    use DispatchesNotifications;

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

    public static function channels(): array
    {
        return [NotificationChannel::DATABASE, NotificationChannel::MAIL, NotificationChannel::PUSH];
    }

    // Mail is mandatory — users cannot unsubscribe from it
    public static function mandatoryChannels(): array
    {
        return [NotificationChannel::MAIL];
    }

    // ...
}
```

By default, `mandatoryChannels()` returns an empty array, meaning all channels are optional.

### How Mandatory Channels Work

[](#how-mandatory-channels-work)

Mandatory channels are enforced at three levels:

1. **`shouldSend()` defense-in-depth** — When dispatching a notification, mandatory channels always return `true` in `shouldSend()`, even if the user's subscription record excludes them. This ensures delivery even with stale subscription data.
2. **Validation re-injection** — The `ValidatesNotificationPreferences` trait automatically re-injects mandatory channels into form requests during `prepareForValidation()`, so they can never be removed by user input.
3. **`NotificationPreferences` DTO** — The `getNotificationPreferences()` method populates a `mandatory` property on the DTO, mapping each notification type to its mandatory channel values. This allows your UI to render mandatory channels as disabled/locked checkboxes.

### Using Mandatory Data in the UI

[](#using-mandatory-data-in-the-ui)

The `NotificationPreferences` DTO includes a `mandatory` property:

```
NotificationPreferences {
    types: [...],
    values: [...],
    mandatory: [
        'ticket_reply' => ['mail'],
    ],
}
```

Use this in your frontend to disable checkboxes for mandatory channels:

```

```

Building a Settings UI
----------------------

[](#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

[](#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 mandatory channels (users can't opt out of them)

### The NotificationPreferences DTO

[](#the-notificationpreferences-dto)

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

```
NotificationPreferences {
    // Channel options for the UI (enabled channels only)
    types: [
        'order_shipped' => ['database' => 'In-App', 'mail' => 'Email', 'slack' => 'Slack'],
        'new_message' => ['mail' => 'Email'],
    ],
    // User's current selections (or defaults)
    values: [
        'order_shipped' => ['database', 'mail'],
        'new_message' => ['mail', 'slack'],
    ],
    // Channels that cannot be unsubscribed from
    mandatory: [
        'order_shipped' => ['mail'],
    ],
}
```

### Example Blade Template

[](#example-blade-template)

```

    @csrf
    @method('PUT')

    @foreach($preferences->types as $notificationType => $channels)

            {{ Str::title(str_replace('_', ' ', $notificationType)) }}

            @foreach($channels as $channel => $label)

                    values[$notificationType] ?? []))
                    />
                    {{ $label }}

            @endforeach

    @endforeach

    Save Preferences

```

### Inertia/Vue Example

[](#inertiavue-example)

```

import { useForm } from '@inertiajs/vue3';

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

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

            {{ type }}

                {{ label }}

        Save

```

Advanced Usage
--------------

[](#advanced-usage)

### Custom Notification Labels

[](#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

[](#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

[](#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
---------------

[](#database-schema)

The package creates a `notification_subscriptions` table:

ColumnTypeDescriptionidbigintPrimary keyuser\_idbigintForeign key to users tabletypestringNotification type identifierchannelsjsonArray of enabled channel namescreated\_attimestampCreation timestampupdated\_attimestampLast update timestampA unique constraint ensures one subscription record per user/type combination.

Testing
-------

[](#testing)

```
composer test
```

Credits
-------

[](#credits)

- [Coding Labs](https://github.com/codinglabsau)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

44

—

FairBetter than 92% of packages

Maintenance79

Regular maintenance activity

Popularity28

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 70% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

110d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/6324d6d1a5b089cfa3d7519b590f0762281ff3bad210c6139209a1c6394d6f5c?d=identicon)[stevethomas](/maintainers/stevethomas)

---

Top Contributors

[![stevethomas](https://avatars.githubusercontent.com/u/1127412?v=4)](https://github.com/stevethomas "stevethomas (14 commits)")[![JonathanLouw](https://avatars.githubusercontent.com/u/14814040?v=4)](https://github.com/JonathanLouw "JonathanLouw (6 commits)")

---

Tags

laravelnotificationsCodinglabsNotification Subscriptionslaravel-notification-subscriptions

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/codinglabsau-laravel-notification-subscriptions/health.svg)

```
[![Health](https://phpackages.com/badges/codinglabsau-laravel-notification-subscriptions/health.svg)](https://phpackages.com/packages/codinglabsau-laravel-notification-subscriptions)
```

###  Alternatives

[vormkracht10/laravel-mails

Laravel Mails can collect everything you might want to track about the mails that has been sent by your Laravel app.

24149.7k](/packages/vormkracht10-laravel-mails)[xammie/mailbook

Laravel Mail Explorer

482458.3k1](/packages/xammie-mailbook)[spatie/laravel-notification-log

Log notifications sent by your Laravel app

207902.8k](/packages/spatie-laravel-notification-log)[wnx/laravel-sends

Keep track of outgoing emails in your Laravel application.

200427.3k](/packages/wnx-laravel-sends)[spatie/laravel-discord-alerts

Send a message to Discord

151408.0k](/packages/spatie-laravel-discord-alerts)[backstage/laravel-mails

Laravel Mails can collect everything you might want to track about the mails that has been sent by your Laravel app.

24157.5k5](/packages/backstage-laravel-mails)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
