定制 fomvasss/laravel-notify-templates 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

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

fomvasss/laravel-notify-templates

Composer 安装命令:

composer require fomvasss/laravel-notify-templates

包简介

Notification templates management for Laravel: DB-based templates, role/user subscriptions, channel resolution.

README 文档

README

DB-based notification templates for Laravel. Manages notification templates, role/user subscriptions, channel resolution, and delay — without coupling to any specific role package.

Українська: README.uk.md

Concepts

  • notify_templates — stores subject/body per notify type + channel slot + role + tenant, with fallback chain
  • notify_role_subscriptions — which notify types are active for which role (channels, delay, personal_only)
  • BaseNotify — abstract base that resolves templates and channels; concrete classes live in the app
  • NotifyTemplatesManager — type registry + resolve methods, available via NotifyTemplates facade

Installation

composer require fomvasss/laravel-notify-templates

Publish and run migrations:

php artisan vendor:publish --tag=notify-templates-migrations
php artisan migrate

Publish config (optional):

php artisan vendor:publish --tag=notify-templates-config

Configuration

config/notify-templates.php:

return [
    'tables' => [
        'notify_templates'          => 'notify_templates',
        'notify_role_subscriptions' => 'notify_role_subscriptions',
    ],

    // All delivery channels available in the project.
    // Used as UI listing and as fallback when typeDefinition()['channels'] is empty.
    // Include only channels actually wired up in the app.
    'channels' => ['mail', 'telegram', 'sms', 'database', 'broadcast'],

    // Fallback channels when subscription has no channels configured,
    // or when via() resolves to nothing entirely
    'default_channels' => ['mail'],

    // Tenant ID: null (single-tenant), or a callable that returns the tenant ID string
    'tenant_id' => null,
    // 'tenant_id' => fn() => app('domain')->getId(),

    // Directories to scan for BaseNotify subclasses on boot (auto-discovery)
    'discover' => [
        app_path('Notifications'),
    ],

    // Optional: pre-register notify types via config
    'types' => [],

    // Override models in the project (e.g. to add translatable support)
    'models' => [
        'notify_template' => \Fomvasss\NotifyTemplates\Models\NotifyTemplate::class,
    ],
];

Type Registry

Auto-discovery (recommended)

By default the package scans app/Notifications on every boot — no code needed in the app. Configured via discover in the config:

'discover' => [
    app_path('Notifications'),
],

Scanning is recursive — subdirectories like Notifications/Order/, Notifications/User/ are included automatically. Set to [] to disable.

The package discovers all classes that extend BaseNotify and return a non-empty typeDefinition(). Safe in production — types are registered once on boot, the singleton is read-only during requests.

Manual call is also available:

NotifyTemplates::discoverIn(app_path('Notifications'));

Manual registration

For dynamic types (e.g. generated from DB records), register in AppServiceProvider::boot():

NotifyTemplates::registerTypes([
    [
        'key'      => 'UserCreated',
        'name'     => 'Користувач створено',
        'group'    => 'user',
        'weight'   => 10,
        'settings' => ['delay'],
        'tokens'   => [
            ['key' => '[user:name]',  'name' => 'Ім\'я користувача'],
            ['key' => '[user:email]', 'name' => 'Email'],
        ],
        'defaults' => [
            'mail'      => ['subject' => 'Новий користувач', 'body' => 'Користувача [user:name] створено.'],
            'messenger' => ['body' => 'Новий користувач: [user:name]'],
        ],
    ],
]);

// or from DB
foreach (Order::statusesList() as $status) {
    NotifyTemplates::registerType([
        'key'   => 'OrderStatus' . ucfirst($status['key']),
        'name'  => 'Статус: ' . $status['name'],
        'group' => 'order',
    ]);
}

Or statically via config:

'types' => [
    ['key' => 'UserCreated', 'name' => 'Користувач створено', 'group' => 'user'],
],

typeDefinition() fields

Field Type Description
key string Unique identifier, e.g. 'OrderOrdered'
name string Human-readable label for UI
group string Grouping key for UI tables, e.g. 'order'
weight int Sort weight within the group; lower values appear first
desc string Optional description shown as tooltip in the UI table
settings array Option keys editable in the admin UI, stored in notify_role_subscriptions.options
tokens array Token hints for the template editor: [['key' => '[order:number]', 'name' => 'Номер']]
channels array Channels this notify type supports. Empty (default) — falls back to config('notify-templates.channels')
defaults array Default subject/body per channel slot, used as placeholder in the editor when no DB template exists

tokens and defaults are UI metadata — the package does not use them for sending. getBodyDefault() / getSubjectDefault() on BaseNotify read from defaults.mail automatically. Keep them in sync.

settings field

settings declares which option keys are shown in the admin UI. The only key the package reads natively is delay:

'settings' => ['delay']
// notify_role_subscriptions.options = {"delay": 5}
// NotifyTemplates::resolveDelay() returns 5 * 60 = 300 seconds

Any other keys are project-defined — read them via $subscription->getOption('key').

Recommended controller pattern — save all settings keys generically so adding a new option requires no controller changes:

$settings = NotifyTemplates::getType($notifyKey)['settings'] ?? [];
if ($settings) {
    $sub = NotifyRoleSubscription::firstOrNew(
        ['notify_key' => $notifyKey, 'role_key' => $roleKey, 'tenant_id' => null],
        ['is_active' => false, 'personal_only' => false, 'channels' => []],
    );
    $incoming = collect($settings)
        ->mapWithKeys(fn($key) => [$key => $request->input($key)])
        ->toArray();
    $sub->options = array_merge($sub->options ?? [], $incoming);
    $sub->save();
}

Retrieve registered types:

NotifyTemplates::getTypes();            // all types
NotifyTemplates::getTypes('order');     // filtered by group
NotifyTemplates::getType('OrderOrdered');

User model — HasNotifySettings

Add the trait to your User model with a notify_channels column (cast to array):

use Fomvasss\NotifyTemplates\Traits\HasNotifySettings;

class User extends Authenticatable
{
    use HasNotifySettings;

    protected $casts = [
        'notify_channels' => 'array',
    ];
}

Override getNotifyChannels() if your column has a different name:

public function getNotifyChannels(): array
{
    return $this->channels ?? [];
}

getNotifyChannels() defines the user's preferred channels. The result is intersected with the channels configured in notify_role_subscriptions — the user can opt out of channels but cannot add new ones beyond what the role allows. If the user returns [] or the method is absent, all subscription channels are used.

NotifyRoleResolverInterface

Implement to resolve which users receive a given notify type. Bind in your ServiceProvider:

use Fomvasss\NotifyTemplates\Contracts\NotifyRoleResolverInterface;
use Fomvasss\NotifyTemplates\Models\NotifyRoleSubscription;

class AppNotifyRoleResolver implements NotifyRoleResolverInterface
{
    public function resolveUsersForNotify(string $notifyKey, mixed $context = null): array
    {
        $tenantId = config('notify-templates.tenant_id');
        $tenantId = is_callable($tenantId) ? $tenantId() : $tenantId;

        $subscriptions = NotifyRoleSubscription::query()
            ->active()
            ->forNotify($notifyKey)
            ->forTenant($tenantId)
            ->get();

        $result = [];

        foreach ($subscriptions as $sub) {
            if ($sub->personal_only && $context?->user) {
                $result[$sub->role_key] = collect([$context->user]);
            } else {
                $result[$sub->role_key] = User::role($sub->role_key)
                    ->where('status', User::STATUS_ACTIVE)
                    ->get();
            }
        }

        return $result;
    }
}
// AppServiceProvider::register()
$this->app->bind(
    \Fomvasss\NotifyTemplates\Contracts\NotifyRoleResolverInterface::class,
    \App\Services\AppNotifyRoleResolver::class,
);

Artisan commands

php artisan notify:make OrderOrdered
# → app/Notifications/OrderOrderedNotify.php

The Notify suffix is added automatically. Nested namespaces are supported:

php artisan notify:make Shop/OrderOrdered
# → app/Notifications/Shop/OrderOrderedNotify.php

The generated stub includes typeDefinition() with all fields pre-filled and a prepareText() hook ready to override. To customise the stub — copy it to stubs/notify.stub in your project root:

cp vendor/fomvasss/laravel-notify-templates/src/Console/stubs/notify.stub stubs/notify.stub

Concrete Notify classes

Extend BaseNotify. Generate with php artisan notify:make, fill typeDefinition(), and add constructor arguments for the models you need.

getBodyDefault() and getSubjectDefault() are derived automatically from typeDefinition()['defaults']['mail'] — no need to define them.

Hooks available for the host app to override:

  • prepareText(string $text, mixed $notifiable): string — token replacement; returns $text as-is by default
  • via(mixed $notifiable): arrayparent::via() handles mail/database/broadcast; extend to add telegram/sms/etc.

manager() and resolveTemplate() are protected — accessible from a trait mixed into concrete classes.

use Fomvasss\NotifyTemplates\Notifications\BaseNotify;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;

final class OrderOrderedNotify extends BaseNotify implements ShouldQueue
{
    use Queueable;

    public function __construct(protected Order $order, protected string $roleKey)
    {
        // $this->tenantId = $order->domain_id; // set if multi-tenant
    }

    public static function typeDefinition(): array
    {
        return [
            'key'      => 'OrderOrdered',
            'name'     => 'Замовлення оформлено',
            'group'    => 'order',
            'weight'   => 20,
            'desc'     => 'Відправляється в момент оформлення замовлення',
            'settings' => ['delay'],
            'tokens'   => [
                ['key' => '[order:number]', 'name' => 'Номер замовлення'],
                ['key' => '[user:name]',    'name' => 'Ім\'я клієнта'],
            ],
            'defaults' => [
                'mail'      => ['subject' => 'Замовлення оформлено', 'body' => 'Ваше замовлення [order:number] прийнято.'],
                'messenger' => ['body' => 'Нове замовлення [order:number]'],
            ],
        ];
    }
}

For types sent directly without an event/listener (e.g. OTP):

$user->notify(new UserOtpNotify(roleKey: 'client', code: $code));

Use only() or except() to override channels at call site:

// send only via mail, regardless of subscription settings
$user->notify((new UserOtpNotify(roleKey: 'client', code: $code))->only(['mail']));

// send via all resolved channels except sms
$user->notify((new OrderOrderedNotify(roleKey: 'client'))->except(['sms']));

Listeners

use Fomvasss\NotifyTemplates\Contracts\NotifyRoleResolverInterface;
use Fomvasss\NotifyTemplates\Facades\NotifyTemplates;
use Illuminate\Support\Facades\Notification;

class OrderOrderedListener
{
    public function __construct(protected NotifyRoleResolverInterface $resolver) {}

    public function handle(OrderOrdered $event): void
    {
        $order = $event->order->fresh();

        foreach ($this->resolver->resolveUsersForNotify('OrderOrdered', $order) as $roleKey => $users) {
            $delay = NotifyTemplates::resolveDelay('OrderOrdered', $roleKey, $order->domain_id);

            Notification::send(
                $users,
                (new OrderOrderedNotify($order, $roleKey))->delay($delay),
            );
        }
    }
}

DB data examples

notify_role_subscriptions — which roles receive which notify types:

role_key notify_key tenant_id is_active personal_only channels options
client OrderOrdered null 1 1 ["mail","sms"] {"delay": 0}
manager OrderOrdered null 1 0 ["mail","telegram"] {"delay": 0}
client OrderOrdered shop-ua 1 1 ["mail","telegram"] {"delay": 2}

personal_only=true — send only to the user from the event context (e.g. the client who placed the order).

notify_templates — subject/body per notify type, channel slot, role, tenant:

notify_key channel role_key tenant_id subject body
OrderOrdered mail null null Замовлення оформлено Ваше замовлення [order:number] прийнято...
OrderOrdered mail client shop-ua Дякуємо за замовлення Привіт, [user:name]! Замовлення [order:number]…
OrderOrdered messenger null null null Замовлення [order:number] оформлено

Channel slots in notify_templates:

  • mail — used by toMail() (subject + body)
  • messenger — generic fallback for non-mail channels; used by getMessengerBody()
  • sms — optional SMS-specific slot; toTurboSms() tries this first, falls back to messenger
  • any other slot name is resolved via resolveTemplate('slot') in the host app

Facade reference

// Type registry
NotifyTemplates::discoverIn(string $path): void
NotifyTemplates::registerType(array $type): void
NotifyTemplates::registerTypes(array $types): void
NotifyTemplates::getTypes(?string $group = null): array
NotifyTemplates::getType(string $key): ?array

// Channels supported by a notify type (from typeDefinition or config fallback)
NotifyTemplates::getTypeChannels(string $notifyKey): array

// Template resolution (8-level fallback chain)
NotifyTemplates::resolveTemplate(string $notifyKey, string $channel, ?string $roleKey, ?string $tenantId): ?NotifyTemplate

// Delivery channels: subscription channels intersected with user preferences (user can opt out, not add)
NotifyTemplates::resolveChannels(string $notifyKey, string $roleKey, ?string $tenantId, array $userChannels = []): array

// Delay in seconds (options.delay in DB is stored in minutes)
NotifyTemplates::resolveDelay(string $notifyKey, string $roleKey, ?string $tenantId): int

Template fallback chain

resolveTemplate('OrderOrdered', 'mail', 'client', 'shop-ua') tries in order:

  1. notify_key=OrderOrdered, channel=mail, role=client, tenant=shop-ua ← most specific
  2. notify_key=OrderOrdered, channel=mail, role=client, tenant=null
  3. notify_key=OrderOrdered, channel=mail, role=null, tenant=shop-ua
  4. notify_key=OrderOrdered, channel=mail, role=null, tenant=null
  5. notify_key=OrderOrdered, channel=null, role=client, tenant=shop-ua
  6. notify_key=OrderOrdered, channel=null, role=client, tenant=null
  7. notify_key=OrderOrdered, channel=null, role=null, tenant=shop-ua
  8. notify_key=OrderOrdered, channel=null, role=null, tenant=null ← global fallback

Returns the first match, or nullBaseNotify then falls back to getBodyDefault() / getSubjectDefault().

Queues & Octane

Queues — fully safe. Types are registered once in boot(), DB queries in resolveChannels / resolveDelay / resolveTemplate are fresh per call.

Octane — safe. The NotifyTemplatesManager singleton is intentionally long-lived: $types is populated once on boot and only read during requests — no request-scoped state is stored.

Caveat: call registerType() / registerTypes() / discoverIn() only in ServiceProvider::boot(), never during request handling — a mutation would persist across all Octane requests.

Optionally pre-resolve the singleton:

// config/octane.php
'warm' => [
    \Fomvasss\NotifyTemplates\NotifyTemplatesManager::class,
],

Multilingual templates (astrotomic/laravel-translatable)

Override the NotifyTemplate model via config to add translation support without touching the package.

1. Migration in your project:

Schema::create('notify_template_translations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('notify_template_id')->constrained('notify_templates')->cascadeOnDelete();
    $table->string('locale', 10);
    $table->text('subject')->nullable();
    $table->longText('body')->nullable();
    $table->unique(['notify_template_id', 'locale']);
});

2. Extend the model:

// app/Models/NotifyTemplate.php

namespace App\Models;

use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract;
use Astrotomic\Translatable\Translatable;
use Fomvasss\NotifyTemplates\Models\NotifyTemplate as BaseNotifyTemplate;

class NotifyTemplate extends BaseNotifyTemplate implements TranslatableContract
{
    use Translatable;

    public array $translatable = ['subject', 'body'];
}

3. Point config to your model:

'models' => [
    'notify_template' => \App\Models\NotifyTemplate::class,
],

$template->subject now returns the current locale's translation — BaseNotify::toMail() and getMessengerBody() require no changes.

Locale in queues — implement HasLocalePreference on User:

use Illuminate\Contracts\Translation\HasLocalePreference;

class User extends Authenticatable implements HasLocalePreference
{
    public function preferredLocale(): string
    {
        return $this->locale ?? config('app.locale');
    }
}

Laravel reads this automatically and sets the locale before toMail() / toTelegram() — even in queued jobs.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

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

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-28

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固