PHP code example of fomvasss / laravel-notify-templates

1. Go to this page and download the library: Download fomvasss/laravel-notify-templates library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

fomvasss / laravel-notify-templates example snippets


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.
    // Used automatically by NotifyTemplatesManager (resolveTemplate/resolveChannels/resolveDelay,
    // and therefore BaseNotify) whenever no explicit $tenantId is passed — set $this->tenantId in
    // a concrete Notify class to override it per-instance.
    '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,
    ],
];

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

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

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

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

public static function typeDefinition(): array
{
    return [
        'key' => 'UserOtp',
        // ...
        'user_configurable' => false,
        // Only these roles can have a subscription/template for this type. An OTP code always goes to the
        // user who is logging in, so a template for any other role would never be used
        'allowed_roles' => ['client'],
        // Sent directly ($user->notify()) in response to the user's own action, not through a role resolver.
        // Combined with user_configurable = false, a disabled subscription falls back to default_channels
        // and the mail goes out anyway, so the admin UI shows the "active" toggle locked instead
        'always_sent' => true,
    ];
}

// Admin matrix: hide cells for roles the type doesn't allow
$allowed = NotifyTemplates::getType($key)['allowed_roles'] ?? null;
$roleAllowed = $allowed === null || in_array($role, $allowed, true);

// Subscription toggle: refuse to disable what can't be disabled
abort_if(NotifyTemplates::getType($key)['always_sent'] ?? false, 422, 'This type is always sent');

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

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

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

use Fomvasss\NotifyTemplates\Traits\HasNotifySettings;

class User extends Authenticatable
{
    use HasNotifySettings;

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

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

  if (!$this->manager()->isNotifyEnabled($this->getNotifyKey(), $notifiable)) {
      return [];
  }
  

  $override = $this->manager()->resolveNotifyUserChannels($this->getNotifyKey(), $notifiable); // null = no override
  

NotifyUserSetting::updateOrCreate(
    ['notifiable_type' => $user->getMorphClass(), 'notifiable_id' => $user->getKey(), 'notify_key' => 'OrderOrdered'],
    ['is_enabled' => true, 'channels' => ['telegram']],
);

public static function typeDefinition(): array
{
    return [
        'key' => 'UserOtp',
        'name' => 'Login code',
        'group' => 'user',
        'user_configurable' => false, // default true
    ];
}

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

class AppNotifyRoleResolver implements NotifyRoleResolverInterface
{
    // Roles you actually trust to receive a broadcast — typically your internal staff role(s).
    // Whitelist, not blacklist: any role not listed here (your "customer"/"tenant" role, and any
    // future role you add and forget to review) defaults to personal-only delivery. Without this,
    // a subscription row created with its default personal_only=false — e.g. the first time
    // someone opens the admin UI for a brand-new notify type, via a lazy firstOrNew() — silently
    // broadcasts to *every* holder of that role the first time the event fires. For a role with
    // many unrelated accounts (customers, tenants) that means leaking one person's event (an
    // invite link, a generated password, ...) to everyone else on the platform.
    private const BROADCAST_SAFE_ROLES = ['admin'];

    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) {
            $forcePersonal = !in_array($sub->role_key, self::BROADCAST_SAFE_ROLES, true);

            if (($sub->personal_only || $forcePersonal) && $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,
);

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; // override per-instance; otherwise falls back to config('notify-templates.tenant_id')
    }

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

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

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

namespace App\Notifications;

use Fomvasss\NotifyTemplates\Notifications\BaseNotify;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Str;
use NotificationChannels\Telegram\TelegramMessage;

abstract class BaseNotification extends BaseNotify
{
    // 1. Custom channels: map a subscription channel slug to a channel name / class-string,
    //    or null to skip (no route). This is the ONLY method to touch — the opt-out gate,
    //    user channel preferences, subscription resolution, the user_configurable fallback
    //    and only()/except() all keep applying to these channels automatically.
    protected function mapChannel(string $channel, mixed $notifiable): ?string
    {
        return match ($channel) {
            'telegram' => $notifiable->routeNotificationForTelegram() ? 'telegram' : null,
            'sms' => $notifiable->phone ? TurboSmsChannel::class : null,
            default => parent::mapChannel($channel, $notifiable),
        };
    }

    // 2. A to{Channel}() per added channel. getMessengerBody() resolves the 'messenger'
    //    template slot (falling back to 'mail') and runs prepareText() on it.
    //    Messengers have hard message limits — trim and strip HTML per channel.
    public function toTelegram(mixed $notifiable): TelegramMessage
    {
        return TelegramMessage::create()
            ->options(['parse_mode' => 'HTML'])
            ->line($this->getMessengerBody($notifiable));
    }

    public function toTurboSms(mixed $notifiable): string
    {
        return Str::limit(strip_tags($this->getMessengerBody($notifiable)), 660);
    }

    // 3. Token replacement — applied to subject/body of every channel
    //    (example uses fomvasss/laravel-str-tokens; any templating works)
    protected function prepareText(string $text, mixed $notifiable): string
    {
        return \StrToken::setEntity($notifiable)->setText($text)->replace();
    }

    // 4. Optional: custom mail view instead of the default ->line()
    public function toMail(mixed $notifiable): MailMessage
    {
        $template = $this->resolveTemplate('mail');

        return (new MailMessage())
            ->subject($this->prepareText($template?->subject ?: $this->getSubjectDefault(), $notifiable))
            ->view('mails.plain', ['body' => $this->prepareText($template?->body ?: $this->getBodyDefault(), $notifiable)]);
    }
}

'buttons' => [
    ['text' => 'Pay', 'url' => '[order:payUrl]'],
],
'buttons_by_role' => [
    'admin' => [['text' => 'Order in admin', 'url' => '[order:adminUrl]']],
],
'buttons_columns' => 1,

public function toTelegram(mixed $notifiable): TelegramMessage
{
    $message = TelegramMessage::create()
        ->options(['parse_mode' => 'HTML'])
        ->line($this->getMessengerBody($notifiable));

    $columns = $this->getMessengerButtonsColumns();

    foreach ($this->getMessengerButtons($notifiable) as $button) {
        $message->button($button['text'], $button['url'], $columns);
    }

    return $message;
}

'buttons' => [
    ['text' => ['uk' => 'Оплатити', 'en' => 'Pay'], 'url' => '[order:payUrl]'],
],

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

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

// Messenger buttons, raw: template options.buttons → buttons_by_role[role] → buttons
NotifyTemplates::resolveButtons(string $notifyKey, ?string $roleKey, ?string $tenantId, string $channel = 'messenger', ?array $type = null): array
NotifyTemplates::resolveButtonsColumns(string $notifyKey, ?string $roleKey, ?string $tenantId, string $channel = 'messenger', ?array $type = null): int
// Button text for a locale: plain string as is, locale map → locale → fallback_locale → first filled
NotifyTemplates::localizeButtonText(string|array $text, ?string $locale = null): string

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

// Delivery report from a provider; status only moves forward
NotifyTemplates::updateDelivery(string $channel, string $externalId, string $status, array $payload = []): bool

// config/notify-templates.php
'log' => [
    'enabled' => true,
    'retention_days' => 90,
    'external_id_resolvers' => [
        'mail' => \Fomvasss\NotifyTemplates\Resolvers\MailMessageIdResolver::class,
        'telegram' => \Fomvasss\NotifyTemplates\Resolvers\TelegramMessageIdResolver::class,
    ],
    'content_resolvers' => [
        'mail' => \Fomvasss\NotifyTemplates\Resolvers\MailContentResolver::class,
        'telegram' => \Fomvasss\NotifyTemplates\Resolvers\TelegramContentResolver::class,
    ],
    'store_body' => true,
],

NotifyTemplates::updateDelivery($channel, $externalId, 'delivered', $rawPayload);

use Fomvasss\NotifyTemplates\Contracts\ExternalIdResolverInterface;

class TurboSmsIdResolver implements ExternalIdResolverInterface
{
    public function resolve(mixed $response): ?string
    {
        return $response['response_result'][0]['message_id'] ?? null;
    }
}

Schedule::command('model:prune', ['--model' => [\Fomvasss\NotifyTemplates\Models\NotifyLog::class]])->daily();

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

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

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

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

use Illuminate\Contracts\Translation\HasLocalePreference;

class User extends Authenticatable implements HasLocalePreference
{
    public function preferredLocale(): string
    {
        return $this->locale ?? config('app.locale');
    }
}
bash
php artisan vendor:publish --tag=notify-templates-migrations
php artisan migrate
bash
php artisan vendor:publish --tag=notify-templates-config
bash
php artisan notify:make OrderOrdered
# → app/Notifications/OrderOrderedNotify.php
bash
php artisan notify:make Shop/OrderOrdered
# → app/Notifications/Shop/OrderOrderedNotify.php
bash
php artisan vendor:publish --tag=notify-templates-lang