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:
emaildatabasebroadcast
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 orderdrivers()sorts the returned summaries alphabetically by each resolvedChannelDriver::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:
emailrejects the log mailer, missing sender configuration, and missing test recipientsdatabaseverifies thenotificationstable exists and that a test notification can be persistedbroadcastrejects anullbroadcast 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:
mailfor drivers whosechannel()ismailnotificationsfor 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.