Skip to content

Notifications

Precious Lyson edited this page Nov 13, 2026 · 2 revisions

The Notification component is a channel-based notification system that allows you to send notifications to your users through multiple channels (email, SMS, database, webhooks, or custom channels) using a unified, expressive API.

It follows the same philosophy as Laravel's notification system but is built specifically for the Machinjiri Core framework with a focus on:

  • Extensibility — Add custom channels in minutes
  • Flexibility — Send via one or many channels in a single call
  • Testability — Clean separation of concerns for easy mocking
  • Async support — Queue notifications for background delivery
  • Observability — Built-in logging and event hooks

Features

Feature Description
Multi-channel delivery Send via mail, SMS, database, webhook, or custom channels in one call
Fluent API Chainable methods for queue names and delays
Custom channels Register your own channel with a single call
Delayed delivery Schedule notifications to be sent later
Queued delivery Dispatch to a job queue for background processing
Conditional sending Per-recipient, per-channel opt-out logic
Result objects Structured responses with success/failure tracking
Logging & events Automatic logging and event hooks (notification.sent, notification.failed, etc.)
Lazy resolution Channel instances are resolved on demand

Installation

The Notification component is part of the Machinjiri Core framework. If you're using the framework, it should already be available.

Then ensure the NotificationManager is bootstrapped in your application's service provider or bootstrap file:

use Mlangeni\Machinjiri\Core\Components\Notification\NotificationManager;

$manager = new NotificationManager($container);

The NotificationManager automatically registers itself as a singleton, so the Notifiable trait can access it via NotificationManager::instance().


Quick Start

Here's a minimal example to get you up and running in under 2 minutes:

1. Create a Notification

namespace App\Notifications;

use Mlangeni\Machinjiri\Core\Components\Notification\Notification;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;

class WelcomeNotification extends Notification
{
    public function via(NotifiableInterface $notifiable): array
    {
        return ['mail'];
    }

    public function toMail(NotifiableInterface $notifiable): array
    {
        return [
            'subject' => 'Welcome aboard!',
            'html'    => '<h1>Welcome!</h1><p>We are glad to have you.</p>',
        ];
    }
}

2. Make Your Model Notifiable

use Mlangeni\Machinjiri\Core\Components\Notification\Notifiable;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;

class User implements NotifiableInterface
{
    use Notifiable;

    public function __construct(
        public int $id,
        public string $email,
    ) {}
}

3. Send It

$user = new User(1, 'jane@example.com');
$user->notify(new WelcomeNotification());

That's it! 🎉


Core Concepts

Notifications

A Notification is a class extending Notification. It describes:

  1. Which channels to use (via())
  2. What payload to send per channel (toMail(), toSms(), etc.)
  3. Optional behavior like queue name, delay, and conditional sending
abstract class Notification
{
    abstract public function via(NotifiableInterface $notifiable): array;
    public function toMail(NotifiableInterface $notifiable): mixed { return null; }
    public function toSms(NotifiableInterface $notifiable): mixed { return null; }
    public function toDatabase(NotifiableInterface $notifiable): ?array { return null; }
    public function toWebhook(NotifiableInterface $notifiable): ?array { return null; }
    public function shouldSend(NotifiableInterface $notifiable, string $channel): bool { return true; }
}

Notifiables

A Notifiable is any entity that can receive notifications (typically a User or similar model). It implements NotifiableInterface and typically uses the Notifiable trait, which provides:

  • notify(), notifyNow(), notifyAsync() convenience methods
  • routeNotificationFor() to resolve channel-specific addresses
  • getNotifiableId() for tracking

Channels

A Channel implements ChannelInterface and is responsible for delivering a notification via one transport:

interface ChannelInterface
{
    public function name(): string;
    public function send(Notification $notification, NotifiableInterface $notifiable): NotificationResponse;
}

Channels are registered with the ChannelManager under a name that matches the key returned from via().

Notification Manager

The NotificationManager is the orchestrator. It:

  • Resolves channel instances lazily
  • Iterates over channels returned by via()
  • Applies shouldSend() filtering
  • Dispatches to the queue if needed
  • Aggregates responses into a NotificationResult
  • Logs and emits events

Beginner Guide

Creating Your First Notification

Notifications are simple classes. The only required method is via(), which returns an array of channel names:

namespace App\Notifications;

use Mlangeni\Machinjiri\Core\Components\Notification\Notification;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;

class InvoicePaidNotification extends Notification
{
    public function __construct(
        private readonly int $invoiceId,
        private readonly float $amount,
    ) {}

    public function via(NotifiableInterface $notifiable): array
    {
        return ['mail', 'sms'];
    }

    public function toMail(NotifiableInterface $notifiable): array
    {
        return [
            'subject' => "Invoice #{$this->invoiceId} paid",
            'html'    => "<p>Your invoice of \${$this->amount} was paid.</p>",
        ];
    }

    public function toSms(NotifiableInterface $notifiable): array
    {
        return [
            'to'   => $notifiable->routeNotificationFor('sms'),
            'body' => "Invoice #{$this->invoiceId} paid: \${$this->amount}",
        ];
    }
}

Making a Model Notifiable

Use the Notifiable trait on your model and implement the two required methods:

use Mlangeni\Machinjiri\Core\Components\Notification\Notifiable;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;

class User implements NotifiableInterface
{
    use Notifiable;

    public function __construct(
        public int $id,
        public string $email,
        public ?string $phone = null,
    ) {}
}

The Notifiable trait automatically resolves:

Channel Resolved property
mail $email / $email_address / $emailAddress
sms $phone / $phone_number / $mobile
webhook $webhook_url

Override routeNotificationFor() if your model uses different property names:

public function routeNotificationFor(string $channel): mixed
{
    return match ($channel) {
        'mail' => $this->contact_email,
        'sms'  => $this->cell,
        default => null,
    };
}

Sending Notifications

Three ways to send:

// 1. Convenience (uses the trait)
$user->notify(new InvoicePaidNotification(123, 99.50));

// 2. Explicit (uses the manager)
$manager = app(NotificationManager::class);
$manager->send($user, new InvoicePaidNotification(123, 99.50));

// 3. Fan-out to multiple recipients
$manager->send(
    [$user1, $user2, $user3],
    new SystemMaintenanceNotification()
);

Every call returns a NotificationResult:

$result = $user->notify(new InvoicePaidNotification(123, 99.50));

if ($result->isSuccessful()) {
    echo "All channels delivered!";
} else {
    foreach ($result->failures() as $failure) {
        echo "{$failure->channel} failed: {$failure->error}\n";
    }
}

Intermediate Guide

Conditional Sending

Use shouldSend() to opt-out of delivery for specific recipients or channels. This is useful for things like "do not disturb" preferences, quotas, or unsupported channels:

public function shouldSend(NotifiableInterface $notifiable, string $channel): bool
{
    // Only email users who have opted in
    if ($channel === 'mail' && ! $notifiable->email_opt_in) {
        return false;
    }

    // Only SMS on weekdays
    if ($channel === 'sms' && in_array(date('N'), [6, 7])) {
        return false;
    }

    return true;
}

You can also conditionally return channels in via():

public function via(NotifiableInterface $notifiable): array
{
    $channels = ['mail'];

    if ($notifiable->phone) {
        $channels[] = 'sms';
    }

    if ($notifiable->wants_digest) {
        $channels[] = 'database';
    }

    return $channels;
}

Queueing Notifications

For slow channels (email, SMS, webhooks), prefer async delivery:

// Queue with default queue name
$user->notifyAsync(new InvoicePaidNotification(123, 99.50));

// Queue with a specific queue
$user->notifyAsync(new InvoicePaidNotification(123, 99.50), 'high-priority');

// Or use the manager
$manager->queue($user, $notification, 'notifications');

The queue API returns an array of job IDs (one per notifiable):

$jobIds = $manager->queue([$user1, $user2], $notification);
// => ['job_abc', 'job_def']

Delayed delivery — queue the notification for later:

$notification = (new InvoicePaidNotification(123, 99.50))
    ->delay(3600); // Send in 1 hour

$user->notifyAsync($notification);

Note: The queue requires a JobDispatcherInterface bound in your container as 'queue.dispatcher'. If no dispatcher is bound, calling queue() will throw a NotificationException.

Custom Channels

Register a custom channel in the manager:

use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\ChannelInterface;
use Mlangeni\Machinjiri\Core\Components\Notification\Notification;
use Mlangeni\Machinjiri\Core\Components\Notification\NotificationResponse;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;

class SlackChannel implements ChannelInterface
{
    public function __construct(private SlackClient $slack) {}

    public function name(): string
    {
        return 'slack';
    }

    public function send(Notification $notification, NotifiableInterface $notifiable): NotificationResponse
    {
        $payload = $notification->toSlack($notifiable);

        if ($payload === null) {
            return NotificationResponse::failure('slack', 'No Slack payload provided.');
        }

        try {
            $response = $this->slack->post($notifiable->routeNotificationFor('slack'), $payload);
            return NotificationResponse::success('slack', $response);
        } catch (\Throwable $e) {
            return NotificationResponse::failure('slack', $e->getMessage());
        }
    }
}

Register it:

$manager->extend('slack', new SlackChannel($slackClient));

// Or with a lazy factory
$manager->extend('slack', fn($app) => new SlackChannel($app->make(SlackClient::class)));

Then in your notification:

public function via(NotifiableInterface $notifiable): array
{
    return ['mail', 'slack'];
}

public function toSlack(NotifiableInterface $notifiable): ?array
{
    return [
        'text' => 'Deployment succeeded!',
        'channel' => '#ops',
    ];
}

Tip: Methods like toSlack() are not part of the base Notification class. PHP's dynamic dispatch means you can define arbitrary to{Channel}() methods — just make sure your custom channel calls them.

Channel Payload Builders

Each channel expects a specific payload shape. Returning null signals "this channel is unsupported for this notification":

toMail()

Accepts either a string (treated as HTML body) or an array:

public function toMail(NotifiableInterface $notifiable): array
{
    return [
        'to'          => 'override@example.com', // optional, overrides route
        'cc'          => ['cc@example.com'],
        'bcc'         => ['bcc@example.com'],
        'subject'     => 'Your invoice',
        'html'        => '<p>Hello!</p>',
        'text'        => 'Hello!',
        'attachments' => ['/path/to/file.pdf'],
    ];
}

You can also return a MailMessage instance directly for full control.

toSms()

Accepts either a Message object or an array (via Message::fromArray()):

public function toSms(NotifiableInterface $notifiable): array
{
    return [
        'to'   => $notifiable->routeNotificationFor('sms'),
        'body' => 'Your verification code is 123456.',
    ];
}

toDatabase()

Returns a payload array. The database channel will inject notification_id, notification (class name), and created_at automatically:

public function toDatabase(NotifiableInterface $notifiable): ?array
{
    return [
        'type'    => 'invoice_paid',
        'message' => "Invoice #{$this->invoiceId} was paid.",
        'amount'  => $this->amount,
        'meta'    => ['invoice_id' => $this->invoiceId],
    ];
}

toWebhook()

Returns a payload with url (or relies on routeNotificationFor('webhook')), event, data, and optional headers/secret:

public function toWebhook(NotifiableInterface $notifiable): ?array
{
    return [
        'url'    => 'https://example.com/hooks/payments',
        'event'  => 'invoice.paid',
        'data'   => ['invoice_id' => $this->invoiceId, 'amount' => $this->amount],
        'secret' => 'shared-secret', // optional HMAC signature
    ];
}

Working with Results

NotificationResult gives you structured access to what happened:

$result = $user->notify($notification);

$result->isSuccessful();   // true if no failures
$result->isEmpty();        // true if no channels were attempted
$result->count();          // number of responses
$result->all();            // NotificationResponse[]
$result->successes();      // successful responses
$result->failures();       // failed responses

foreach ($result->successes() as $response) {
    echo "✓ {$response->channel}: " . json_encode($response->result);
}

foreach ($result->failures() as $response) {
    echo "✗ {$response->channel}: {$response->error}";
}

NotificationResult implements JsonSerializable, so it can be returned directly from a controller:

return response()->json($result);
// {
//   "successful": true,
//   "responses": [
//     { "channel": "mail", "success": true, "result": {...}, "error": null, "meta": {} }
//   ]
// }

Advanced Guide

Extending the Channel Manager

The ChannelManager is accessible via $manager->channels(). You can interact with it directly:

$channels = $manager->channels();

$channels->has('slack');          // bool
$channels->registered();          // string[] — all registered channel names
$channels->channel('mail');       // ChannelInterface
$channels->register('sms', $instance);  // re-register/override

Registering Channels via Config

The ChannelManager::registerChannels() method reads from notification.channels in your configuration. This is the recommended way to register application-level channels:

// config/notification.php
return [
    'channels' => [
        'slack'    => \App\Notifications\Channels\SlackChannel::class,
        'telegram' => fn($app) => new TelegramChannel($app->make(TelegramClient::class)),
    ],
    'notification_table' => 'notifications',
];

Then call $manager->channels()->registerChannels() at bootstrap.

Resolution order for a channel registered as a class string:

  1. If the class is callable and a ChannelInterface — use directly
  2. If the class is callable only — invoke as factory with $app
  3. Otherwise — resolve via $app->resolve($class) and verify ChannelInterface

Overriding Built-in Channels

Simply re-register with the same name:

$manager->extend('mail', fn($app) => new MyCustomMailChannel($app->make(MailManager::class)));

Custom Notification Stores

The DatabaseChannel writes through any NotificationStoreInterface. The default is QueryBuilder, but you can swap it:

use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotificationStoreInterface;

class RedisNotificationStore implements NotificationStoreInterface
{
    public function __construct(private Redis $redis) {}

    public function store(
        string $notifiableId,
        string $channel,
        array $payload,
        ?string $notificationId = null
    ): mixed {
        $key = "notifications:{$notifiableId}";
        $id  = $notificationId ?? bin2hex(random_bytes(12));

        $this->redis->lpush($key, json_encode([
            'id'    => $id,
            'channel' => $channel,
            'payload' => $payload,
            'created_at' => date(DATE_ATOM),
        ]));

        return $id;
    }
}

Then bind it in the container or construct a DatabaseChannel with it:

$manager->extend('database', fn($app) => new DatabaseChannel($app->make(RedisNotificationStore::class)));

The bundled LoggerNotificationStore is a no-op store that logs instead of persisting — useful in development or when you don't want a DB dependency.

Event Hooks

The manager triggers the following events on the EventListener:

Event Payload
notification.sending notification, notifiable, channel
notification.sent notification, notifiable, channel, response
notification.failed notification, notifiable, channel, response or exception
notification.queued notification, notifiable, queue

Attach listeners to react to these events (e.g., increment metrics, alert on failures):

$events->listen('notification.failed', function (array $payload) use ($alerting) {
    $alerting->warning('Notification failed', [
        'channel' => $payload['channel'],
        'class'   => get_class($payload['notification']),
    ]);
});

Error Handling & Exceptions

All notification-specific errors are thrown as NotificationException with a context array and a code:

Code Meaning
notification_channel Channel not registered, factory returned wrong type
notification_config Manager not bootstrapped, invalid via() return, invalid notifiable, missing dispatcher

Per-channel failures are not thrown. They're captured in the NotificationResult as NotificationResponse::failure(). This means a failing SMS channel never prevents the mail channel from being attempted.

To check if any failure occurred:

if (! $result->isSuccessful()) {
    // At least one channel failed
}

To bubble exceptions (e.g., in tests), use failures() and re-throw.

Testing Notifications

Using a Fake Channel

Register a spy channel to assert deliveries:

use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\ChannelInterface;
use Mlangeni\Machinjiri\Core\Components\Notification\Notification;
use Mlangeni\Machinjiri\Core\Components\Notification\NotificationResponse;
use Mlangeni\Machinjiri\Core\Components\Notification\Contracts\NotifiableInterface;

class FakeChannel implements ChannelInterface
{
    public array $sent = [];

    public function name(): string { return 'fake'; }

    public function send(Notification $notification, NotifiableInterface $notifiable): NotificationResponse
    {
        $this->sent[] = [$notification, $notifiable];
        return NotificationResponse::success('fake', ['ok' => true]);
    }
}
$fake = new FakeChannel();
$manager->extend('fake', $fake);

$user->notify(new NotificationUsingFakeChannel());

$this->assertCount(1, $fake->sent);

Testing shouldSend()

public function test_it_skips_sms_on_weekends()
{
    $notification = new ReminderNotification();
    $user = new User(1, 'a@b.c', phone: '123');

    // Assume today is a Saturday in the test environment
    $result = $user->notify($notification);

    $this->assertEmpty(
        array_filter($result->all(), fn($r) => $r->channel === 'sms')
    );
}

Testing Queue Dispatch

Mock the dispatcher and assert the job was dispatched:

$dispatcher = $this->createMock(JobDispatcherInterface::class);
$dispatcher->expects($this->once())
    ->method('dispatchToQueue')
    ->willReturn('job-123');

$manager = new NotificationManager($app, dispatcher: $dispatcher);
NotificationManager::setInstance($manager);

$user->notifyAsync(new ReminderNotification(), 'reminders');

Performance Considerations

  • Lazy resolution — Channels aren't instantiated until first use; the first send in a process pays the construction cost.
  • Per-notifiable channel re-resolution — via() and shouldSend() are called per notifiable. Keep them cheap.
  • Avoid expensive work in via() — Prefer to do heavier checks inside shouldSend(), which can short-circuit.
  • Queue slow channels — Mail, SMS, and webhooks are I/O bound. Use notifyAsync() to avoid blocking request lifecycles.
  • Result aggregation — Fan-out to N recipients × M channels creates N×M responses in memory. For very large batches, chunk your recipients.
  • Webhook DNS lookups — The webhook channel re-resolves HttpRequest per send when not bound to the container; bind HttpRequest::class in your container to reuse a single client.

Built-in Channels Reference

Mail Channel

Sends through MailManager. Accepts string, array, or MailMessage payload from toMail().

Payload key Description
to, cc, bcc Recipients (defaults to routeNotificationFor('mail'))
subject Email subject
html, text Body (both, or one)
attachments Array of file paths

SMS Channel

Sends through SMSManager. Accepts a Message or an array compatible with Message::fromArray(). If the notification has a queue name set, the SMS manager is told to send asynchronously.

Database Channel

Persists notifications to a store (default: QueryBuilder → database_channel_notifications table). Requires toDatabase() to return an array. Automatically injects notification_id, notification (class name), and created_at.

Bug note: In DatabaseChannel::withFallback(), the default QueryBuilder is bypassed in favor of a LoggerNotificationStore. If you rely on withFallback() for DB persistence, override the channel explicitly instead.

Webhook Channel

POSTs JSON to a URL. Requires toWebhook() to return an array with a url (or rely on routeNotificationFor('webhook')).

Payload key Description
url Target URL (validated)
event Event name (defaults to notification class)
data Arbitrary data payload
headers Extra request headers
secret Adds X-Notification-Signature (HMAC-SHA256)

API Reference

Notification (abstract)

Method Description
via(NotifiableInterface $n): array Required — returns channel names
toMail(), toSms(), toDatabase(), toWebhook() Payload builders (return null to skip)
shouldSend(NotifiableInterface $n, string $ch): bool Per-recipient/channel gate
onQueue(?string $queue): static Set queue name
queueName(): ?string Get queue name
delay(?int $seconds): static Set delay
delaySeconds(): ?int Get delay
id(): ?string Unique notification ID (UUID v1)

Notifiable (trait)

Method Description
notify(Notification): NotificationResult Send synchronously
notifyNow(Notification): NotificationResult Alias of notify()
notifyAsync(Notification, ?string $queue): array Queue for async delivery
routeNotificationFor(string $channel): mixed Resolve recipient address
getNotifiableId(): string Stable ID (uuid → id → spl_object_hash)

NotificationManager

Method Description
static instance(): self Get singleton
static setInstance(?self): void Set singleton (for tests)
channels(): ChannelManager Access channel manager
extend(string, callable|ChannelInterface): self Register a channel
send($notifiables, Notification): NotificationResult Sync send
sendNow($notifiables, Notification): NotificationResult Alias of send()
queue($notifiables, Notification, ?string $queue): string[] Async send; returns job IDs

ChannelManager

Method Description
register(string $name, mixed $channel): self Register/override a channel
has(string $name): bool Check registration
channel(string $name): ?ChannelInterface Resolve a channel
registered(): string[] List registered names
registerChannels(): void Load channels from config

NotificationResponse

Property/Method Description
$channel Channel name
$success Whether it succeeded
$result Channel-specific result value
$error Error message (on failure)
$meta Extra metadata
static success(string, mixed, array) Construct a success
static failure(string, string, array) Construct a failure
toArray(): array Array representation

NotificationResult

Method Description
add(NotificationResponse): self Add a response
merge(self): self Merge another result
all(): NotificationResponse[] All responses
successes(): NotificationResponse[] Successful responses
failures(): NotificationResponse[] Failed responses
isSuccessful(): bool No failures
isEmpty(): bool No responses
count(): int Response count
jsonSerialize(): array JSON representation

Troubleshooting

Symptom Cause Fix
NotificationManager has not been instantiated yet The manager was never constructed in the bootstrap Construct a NotificationManager (or make it via the container) before sending
Notification channel 'x' is not registered Channel name in via() has no registration Register it via $manager->extend() or config
Factory for notification channel 'x' did not return a ChannelInterface A class-string channel resolved to a non-channel Ensure the class implements ChannelInterface
Notification X::via() must return an array via() returned null/string Return [], ['mail'], etc.
Job dispatcher not bound in Container notifyAsync() called without a dispatcher Bind JobDispatcherInterface as 'queue.dispatcher' in the container
Notifications silently skipped Channel failed has() check or shouldSend() returned false Check the logger output; add notification.sending listener to debug
MailChannel falls through with empty message toMail() returned null Return a payload or remove 'mail' from via()
Webhook failed with 4xx/5xx Target server rejected the request Inspect response->meta['body'] in the failure
DatabaseChannel writes to log instead of DB Used withFallback() or notification_table missing Construct DatabaseChannel directly with a QueryBuilder

Warning on ChannelManager::channel(): The method is documented as returning ?ChannelInterface, but the branch that resolves a registered ChannelInterface instance directly (when it's not a callable and not already resolved) falls off the end without a return. This is a latent bug — if you hit null when you expected a channel, this is likely why. Fix by adding return $this->resolved[$name] = $channel; at the end of channel(), or ensure your channels are registered as factories.


Best Practices

  1. Keep notifications focused — One notification = one logical event. Don't mix "invoice paid" and "welcome aboard" in one class.
  2. Prefer notifyAsync() for I/O channels — mail, SMS, and webhooks should always be queued in production.
  3. Use shouldSend() over via() for dynamic filtering — via() should describe what could be sent; shouldSend() should decide what should be sent.
  4. Return null to skip a channel — If toMail() returns null, the mail channel records a failure. If you want to skip silently, remove the channel from via() or gate it in shouldSend().
  5. Namespace your custom channels — e.g., App\Notifications\Channels\SlackChannel.
  6. Version your payloads — For webhooks, include a version field in the payload so consumers can evolve safely.
  7. Sign webhooks — Always use the secret option in production webhooks to allow HMAC verification on the receiving end.
  8. Log correlation IDs — Include a notification_id or a business-level correlation ID in all payloads. The base class provides id() for you.
  9. Handle fan-out memory — For very large recipient lists, chunk sends into batches of ~100.
  10. Test the whole pipeline — Assert both that channels received the correct payloads and that shouldSend() filtering behaves as expected.

FAQ

Q: Can I send the same notification to multiple channels per recipient? Yes — return all channel names from via(). Each channel's send() is called independently.

Q: What happens if one channel fails — do the others still run? Yes. Exceptions inside a channel's send() are caught by the manager and converted to a NotificationResponse::failure(). All channels are attempted.

Q: Can I change the queue used by a specific notification? Yes — (new MyNotification())->onQueue('high-priority'), or pass the queue as the third argument to notifyAsync() / queue().

Q: How do I send a notification to many users efficiently? Use queue() with an array of notifiables, or use notifyAsync() per user. Both dispatch one job per notifiable.

Q: Where are queued notifications processed? Wherever your JobDispatcherInterface implementation processes jobs. The dispatch target is SendNotificationJob, which calls sendNow() on the recipient + notification.

Q: Can I serialize a Notification for the queue? Yes — Notification implements __serialize()/__unserialize() and persists its id, queueName, and delay. If you add state to your subclass, override these methods to include it.

Q: Can I use a class-string as a channel registration? Yes — $manager->extend('foo', MyChannel::class). The manager will resolve it from the container.

Q: How do I make a channel async-aware? Channels don't need to know about queues — the manager handles async orchestration. Only channels that internally queue (like the current SMS channel, which checks queueName()) need extra care.

Q: Can I send a notification without a Notifiable model (e.g., a raw email address)? Create an ad-hoc NotifiableInterface implementation (an anonymous class works) that returns the address from routeNotificationFor().

Clone this wiki locally