SIGNALS Documentation
API Reference

Channel Provider Registry

Contract for registering notification delivery drivers, resolving them by key or logical channel, and self-testing outbound delivery.

Overview

App\Services\Notifications\ChannelProviderRegistry is the notification delivery registry. It maps a driver key such as email or broadcast to a concrete App\Contracts\Notifications\ChannelDriver, then exposes the current driver catalogue to the settings UI, self-test flows, and queued delivery pipeline.

Core binds the registry as a singleton in AppServiceProvider and currently registers three built-in drivers there:

  • email
  • database
  • broadcast

Plugins currently register additional drivers through PluginRegistrar::channelDriver().

Public surface

Method Purpose
register() Store one driver class under a driver key
resolve() Return a driver by explicit key or by logical Laravel channel name
test() Self-test a driver using TestChannelProviderData
send() Queue SendChannelNotification for a resolved driver
drivers() Return ChannelDriverData summaries, optionally filtered by channel
keys() Return the registered driver keys
channels() Return the distinct logical channel names
has() Check whether a driver key is registered

Accepted contract and identity

Every registered driver class must implement App\Contracts\Notifications\ChannelDriver:

public function key(): string;
public function channel(): string;
public function label(): string;
public function configSchema(): array;
public function resolveConfig(): array;
public function send(ChannelSendData $payload): void;
public function test(TestChannelProviderData $data): ChannelTestResultData;

The registry key passed to register() is the primary identity for lookup, persistence, and queued send payloads. ChannelDriver::key() is a separate identifier supplied by the driver and used in catalogue summaries and test results. The registry does not require the registration key to equal ChannelDriver::key(), and it does not cross-validate them during registration or resolution.

channel() is a third identifier: the logical Laravel notification channel name such as mail, database, or broadcast. Channel lookup compares this value, not either key.

The registry stores driver class names rather than driver objects. Every successful key lookup, channel scan, catalogue build, or channel-list build resolves the stored class through the Laravel container. The registry therefore asks the flightcase on every lookup and does not cache driver instances. Normal container binding scope still applies: a driver class independently bound as a singleton may be returned as the same object.

The class-string<ChannelDriver> requirement is a PHPDoc and static-analysis contract. register() stores the class string without instantiating or runtime-checking it. Flightcase construction failures or a class that does not satisfy the ChannelDriver return type surface only when a lookup or catalogue operation resolves that registration.

Current core registrations

AppServiceProvider currently boots the registry like this:

$registry = new ChannelProviderRegistry($app);

$registry->register('email', EmailDriver::class);
$registry->register('database', DatabaseDriver::class);
$registry->register('broadcast', BroadcastDriver::class);

Those three drivers are the current built-in catalogue:

Key Channel Label Notes
email mail Email (Settings) Reads tenant email settings, masks secrets in summaries, and queues on mail
database database In-App (Database) Wraps Laravel database notifications and queues on notifications
broadcast broadcast Real-Time (Broadcast) Wraps Laravel broadcast notifications and queues on notifications

drivers() returns ChannelDriverData summaries with each driver's config schema plus masked resolved config. Each summary identifies and sorts the driver using ChannelDriver::key(), not the registration-map key; the registration key is not included separately. Secret fields are masked through ChannelConfigMasker before the data is exposed to the API or admin settings pages.

Configuration schemas

configSchema() returns a list of ChannelConfigFieldData values. Each field has key, label, type, required, and secret properties; required and secret both default to false. These are declarative form and masking metadata. The registry does not itself validate configuration against the schema.

Email declares its settings-backed fields as follows:

Field Type Required Secret
mailer select yes no
smtp_host string no no
smtp_port integer no no
smtp_username string no no
smtp_password password no yes
smtp_encryption select no no
ses_key string no yes
ses_secret password no yes
ses_region string no no
mailgun_domain string no no
mailgun_secret password no yes
postmark_token password no yes
from_address email yes no
from_name string no no
reply_to_address email no no

Email resolves its effective configuration from the email settings group. The broadcast driver declares only its required connection field, with type string, and resolves that value from broadcasting.default. The database driver declares an empty schema and resolves an empty configuration array.

Resolution, defaults, and ordering

Driver-key resolution

resolve(driverKey: ...) is the most precise lookup. It returns the driver registered for that key or throws InvalidArgumentException with Unknown channel driver [{$driverKey}]. when the key is missing.

Channel resolution

resolve(channel: ...) walks the registered drivers in registry insertion order and returns the first driver whose channel() matches the requested logical channel. If none match, it throws InvalidArgumentException with No channel driver registered for channel [{$channel}]..

Default driver selection for self-tests

test() accepts TestChannelProviderData, which may include either:

  • an explicit driverKey, or
  • only a logical channel

When driverKey is present, the explicit driverKey wins: the registry resolves that key without checking whether TestChannelProviderData::$channel matches the driver's channel(). It does not cross-validate the supplied channel against the resolved driver.

When driverKey is omitted, the registry chooses the first registered driver whose channel() matches the requested channel. That same channel-first scan is the current defaulting rule used by defaultDriverKeyForChannel().

Ordering rules

  • registry storage keeps insertion order for channel-based resolution and default-driver selection
  • re-registering an existing key replaces the stored class without adding a second entry
  • keys() returns registration-map keys in insertion order
  • drivers() sorts the returned summaries alphabetically by each resolved ChannelDriver::key()
  • channels() deduplicates channel names, then sorts them alphabetically

Testing and sending

Self-tests

test() resolves the target driver, then delegates to the driver's own test() implementation. Current shipped behavior includes:

  • email rejects the log mailer, missing sender configuration, and missing test recipients
  • database verifies the notifications table exists and that a test notification can be persisted
  • broadcast rejects a null broadcast connection and reports the configured connection on success

Plugins can register additional self-test logic by implementing ChannelDriver::test().

Queued sending

send(ChannelSendData $payload) resolves the driver by driverKey, then dispatches SendChannelNotification onto:

  • mail for drivers whose channel() is mail
  • notifications for every other channel

The queued job later calls the concrete driver's send() method synchronously. The registry itself does not deliver the notification inline.

Invariants and failure modes

Duplicate keys

Driver keys are stored in an associative array. Registering the same key twice replaces the earlier driver class. The registry does not reject duplicates.

Required lookup input

resolve() requires either driverKey or channel. Calling it without both throws InvalidArgumentException with Either driverKey or channel must be provided.

Channel and default misses

Both resolve(channel: ...) and the implicit default selection used by test() fail with No channel driver registered for channel [...] when no registered driver advertises that channel.

Driver-level failures

The registry does not normalise driver-specific send or test failures. Exceptions or failed ChannelTestResultData responses from the concrete driver surface as the current contract.

Worked example

This is the current consumption pattern for the built-in email driver:

$registry = app(ChannelProviderRegistry::class);
$driver = $registry->resolve(driverKey: 'email');

$result = $registry->test(new TestChannelProviderData(
    channel: $driver->channel(),
    driverKey: 'email',
    recipient: 'operator@example.test',
));

$registry->send(new ChannelSendData(
    driverKey: 'email',
    notifiableId: $account->id,
    notifiableType: Account::class,
    title: 'Delivery update',
    body: 'Your delivery is ready.',
    config: [],
));

The test path resolves the explicit core driver and delegates to its self-test. The send path queues SendChannelNotification, which later resolves the same driver key and calls the driver's send() method on the mail queue.