offload-project/laravel-notification-preferences 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

offload-project/laravel-notification-preferences

Composer 安装命令:

composer require offload-project/laravel-notification-preferences

包简介

Manage and display user notification preferences in Laravel

README 文档

README

Latest Version on Packagist GitHub Tests Action Status Total Downloads

Laravel Notification Preferences

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.

Features

  • 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
  • Permission Gating — Hook into Laravel's Gate to hide and block notifications users aren't authorized to receive
  • 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
  • Email Unsubscribe Links — Signed URLs for one-click unsubscribe with List-Unsubscribe header support
  • Input Validation — Prevents setting preferences for unregistered notifications/channels

Table of Contents

Requirements

  • PHP 8.3+
  • Laravel 11/12/13

Installation

composer require offload-project/laravel-notification-preferences

Publish the config and migrations:

php artisan vendor:publish --tag=notification-preferences-config
php artisan vendor:publish --tag=notification-preferences-migrations
php artisan migrate

Quick Start

1. 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));

Managing Preferences

// 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();

Bulk Operations

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.

Explicit Control with Trait

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']);
    }
}

Forced Channels

Prevent users from disabling critical notifications:

'notifications' => [
    SecurityAlert::class => [
        'group' => 'security',
        'label' => 'Security Alerts',
        'force_channels' => ['mail', 'database'],
    ],
],

Permission Gating

Tie notifications to a Gate ability so users only see — and only receive — notifications they're authorized for. Works with native policies, Spatie Permission, Bouncer, or any package that registers with Laravel's Gate.

Option 1: Implement the interface (recommended for notifications you own):

use OffloadProject\NotificationPreferences\Contracts\AuthorizesNotification;

class OrderShipped extends Notification implements AuthorizesNotification
{
    public static function notificationAbility(): string
    {
        return 'view-orders';
    }
}

Option 2: Declare the ability in config (for notifications from other packages):

'notifications' => [
    \Vendor\Package\SomeNotification::class => [
        'group' => 'system',
        'label' => 'Vendor notification',
        'ability' => 'view-orders',
    ],
],

When a user fails the Gate check:

  • Dispatch is blocked across all channels — authorization overrides forced channels and self-filtering traits.
  • The notification is hidden from getPreferencesTable() so the UI doesn't expose toggles for things they can't access.
  • A NotificationAuthorizationDenied event is dispatched per blocked channel for observability.

The notification instance is passed to your Gate closure at dispatch time, so you can authorize against the payload:

Gate::define('view-orders', function (User $user, ?OrderShipped $notification = null) {
    // $notification is null when called from the preferences UI (no instance exists)
    if ($notification) {
        return $user->can('view', $notification->order);
    }
    return $user->can('view-orders');
});

Per-Channel Defaults

Set specific channels enabled by default:

'notifications' => [
    OrderShipped::class => [
        'group' => 'system',
        'label' => 'Order Shipped',
        'default_channels' => ['mail', 'database'], // Only these enabled by default
    ],
],

Events

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
});

NotificationAuthorizationDenied is dispatched whenever a notification is blocked by a failing Gate check (see Permission Gating):

use OffloadProject\NotificationPreferences\Events\NotificationAuthorizationDenied;

Event::listen(NotificationAuthorizationDenied::class, function ($event) {
    // $event->notifiable   - The user the notification was being sent to
    // $event->notification - The notification instance
    // $event->channel      - The channel that was blocked
    // $event->ability      - The Gate ability that failed
});

Using the Facade

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, ...]

Using the Interface

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
        );
    }
}

Table Structure Output

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

Inertia.js Integration

Share preferences via middleware:

// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'notificationPreferences' => fn () => $request->user()?->getNotificationPreferencesTable(),
    ];
}

Cache Management

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();

Exception Handling

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."
}

Email Unsubscribe Links

The package can generate signed URLs that let users unsubscribe directly from emails — no login required. It also supports List-Unsubscribe headers for native unsubscribe buttons in Gmail, Apple Mail, and other clients (RFC 8058).

Adding Unsubscribe Links to Notifications

Use the HasUnsubscribeUrl trait on your notification class:

use OffloadProject\NotificationPreferences\Concerns\HasUnsubscribeUrl;

class OrderShipped extends Notification
{
    use HasUnsubscribeUrl;

    public function toMail($notifiable): MailMessage
    {
        return $this->withUnsubscribeHeaders(
            (new MailMessage)
                ->line('Your order has shipped!')
                ->action('Unsubscribe', $this->getUnsubscribeUrl($notifiable)),
            $notifiable
        );
    }
}

withUnsubscribeHeaders() adds List-Unsubscribe and List-Unsubscribe-Post headers so email clients can show native unsubscribe buttons. The getUnsubscribeUrl() method generates a signed URL you can place anywhere in the email body.

Generating URLs Directly

You can also generate URLs from the user model or facade:

// From the user model
$url = $user->notificationUnsubscribeUrl(OrderShipped::class);
$url = $user->notificationResubscribeUrl(OrderShipped::class);

// From the facade
use OffloadProject\NotificationPreferences\Facades\NotificationPreferences;

$url = NotificationPreferences::unsubscribeUrl($user, OrderShipped::class);
$url = NotificationPreferences::resubscribeUrl($user, OrderShipped::class);

// For a specific channel (defaults to 'mail')
$url = NotificationPreferences::unsubscribeUrl($user, OrderShipped::class, 'database');

How It Works

When a user clicks the unsubscribe link, the package:

  1. Validates the signed URL (tamper-proof, no auth required)
  2. Disables the notification type for that channel
  3. Returns a JSON response, or redirects to your configured URL

The POST method is also supported for RFC 8058 one-click unsubscribe from email clients.

Configuration

// config/notification-preferences.php
'unsubscribe' => [
    // Whether to register unsubscribe routes
    'enabled' => true,

    // Route prefix for unsubscribe/resubscribe endpoints
    'route_prefix' => 'notification-preferences',

    // Middleware for the unsubscribe routes
    'middleware' => ['web'],

    // Signed URL TTL in minutes (null for permanent/non-expiring)
    'url_ttl' => null,

    // Redirect to this URL after unsubscribing (with status, notification_type, and channel query params)
    // Set to null to return a JSON response instead
    'redirect_url' => null,

    // Enable resubscribe functionality
    'resubscribe_enabled' => true,
],

Redirect Example

When redirect_url is set, users are redirected after unsubscribing:

'redirect_url' => '/notification-settings',
// Redirects to: /notification-settings?status=unsubscribed&notification_type=App\Notifications\OrderShipped&channel=mail

This lets you handle the confirmation page in your own frontend (Blade, Inertia, Livewire, etc.).

Uninstalling

php artisan notification-preferences:uninstall --force
composer remove offload-project/laravel-notification-preferences
rm config/notification-preferences.php

Configuration Reference

Global Options

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)

Channels

Option Type Description
label string Display name for UI
enabled bool Whether channel is available (default: true)

Groups

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

Notifications

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
ability string Gate ability the user must pass to receive (also hides from preferences UI). Prefer the AuthorizesNotification interface for notifications you own.
order int Sort order within group

Unsubscribe

Option Type Description
enabled bool Register unsubscribe routes (default: true)
route_prefix string URL prefix for routes (default: notification-preferences)
middleware array Middleware stack for routes (default: ['web'])
url_ttl int|null Signed URL expiration in minutes (null = permanent)
redirect_url string|null Redirect after action, or null for JSON response
resubscribe_enabled bool Register resubscribe route (default: true)

AI Coding Assistant Skill

This package ships a Laravel Boost skill so coding assistants (Claude Code, Cursor, etc.) follow the package's conventions when generating code. Install it in your app with:

php artisan boost:add-skill offload-project/laravel-notification-preferences

The skill source lives at skills/SKILL.md.

Testing

composer test

Contributing

Contributions are welcome! Please see the documents below before getting started.

Security

License

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

offload-project/laravel-notification-preferences 适用场景与选型建议

offload-project/laravel-notification-preferences 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1.4k 次下载、GitHub Stars 达 7, 最近一次更新时间为 2025 年 12 月 16 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「notification」 「laravel」 「preferences」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 offload-project/laravel-notification-preferences 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 offload-project/laravel-notification-preferences 我们能提供哪些服务?
定制开发 / 二次开发

基于 offload-project/laravel-notification-preferences 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 1.4k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 7
  • 点击次数: 20
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 7
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-12-16