Plugin channel providers
Build a Slack/Teams-style notification channel plugin — ChannelDriver contract, channel_drivers manifest section, config schema, testing, and lifecycle expectations.
Overview
Plugins can supply notification channel providers — delivery drivers such as Slack incoming webhooks or Microsoft Teams connectors. Core already ships email, database, and broadcast. A channel-provider plugin declares drivers in signals.yaml, registers them through PluginRegistrar::channelDriver(), and stores tenant credentials on channel_providers rows (encrypted config) managed by the admin Notifications settings UI and the Channel Providers API.
Reference implementation: signals/slack-webhook under plugins/signals/slack-webhook/.
Related contracts:
Contract walkthrough
Implement App\Contracts\Notifications\ChannelDriver:
| Method | Role |
|---|---|
key() |
Stable driver key — must match the vendor-prefixed manifest channel_drivers[].key (e.g. signals.slack_webhook) |
channel() |
Logical delivery channel (e.g. slack) — may appear in notifications[].channels |
label() |
Human label for the Channel Providers panel |
configSchema() |
list<ChannelConfigFieldData> driving the admin form + secret masking |
resolveConfig() |
Effective config at send/test time (usually the active ChannelProvider row) |
send(ChannelSendData) |
Deliver one payload (called sync by SendChannelNotification / registry-backed Laravel channel) |
test(TestChannelProviderData) |
Connectivity self-test for the admin Send test button and API |
Register in PluginBase::register():
$registrar->channelDriver('signals.slack_webhook', SlackWebhookChannelDriver::class);
Wiring ⊆ declaration: when the registrar is scoped via forPlugin(), the key must appear in channel_drivers[] or registration throws PluginRegistrarException.
Manifest channel_drivers
channel_drivers:
- key: signals.slack_webhook
channel: slack
label: Slack Incoming Webhook
config_fields:
- webhook_url
- default_channel
notifications:
- key: signals.slack_webhook.test_ping
label: Slack Test Ping
channels: [slack]
Rules (see Manifest reference):
- Keys are vendor-prefixed like permissions (
{vendor}.…). channelis a lowercase slug and must not collide with coredatabase/mail/broadcast.notifications[].channelsmay list core channels or logical channels declared in the same plugin'schannel_drivers[].config_fieldsis optional documentation for review; the runtime form comes fromconfigSchema().
Declare outbound hosts under network when the driver calls external HTTP (e.g. hooks.slack.com).
Config schema & admin UI
Tenant configuration lives on ChannelProvider rows (driver, encrypted config, is_active, last test status) — not plugin settings — so operators manage Slack/Teams credentials next to Email/Database/Broadcast under Admin → Settings → Notifications → Channel Providers.
- Secret fields (
secret: true) are masked on read and preserved on leave-blank save. - Plugin-supplied drivers show a Plugin badge.
- When the plugin is disabled or uninstalled, existing rows become inert and display Provider unavailable (configure/test are blocked with a toast).
Permissions (no extra plugin permission required for core provider CRUD):
| Surface | Gate |
|---|---|
| Admin Notifications UI | notifications.manage |
| API read | channel-providers:read (+ notifications.manage) |
| API write / test | channel-providers:write (+ notifications.manage) |
Add a plugin-declared permission only if you expose a custom page or slot that needs its own grant.
Delivery & preferences
- Driver registration happens at plugin boot (
PluginServiceProviderloads enabled packages only). - Non-core logical channels are bridged into Laravel's notification channel manager so
Signals::notify()/SendNotificationcan deliver them. NotificationChannelResolversilently drops channels whose drivers are not registered (intersect-only model).- Account preference catalogues pick up plugin channels dynamically via the registry label map.
- Queued mid-flight rule: if a channel becomes unavailable after the job was queued, skip that channel with a warning log and still deliver remaining channels — never fail the whole job.
Lifecycle expectations
| Event | Behaviour |
|---|---|
| Enable | Next boot registers the driver; providers become configurable/testable again |
| Disable | Driver is not registered; ChannelProvider rows stay; UI flags Provider unavailable; delivery skips with a warning |
| Uninstall | Keep rows inert (same as disable). Prompted cleanup of orphaned provider rows is a future admin UX pass — do not delete tenant config automatically |
Testing
Unit/feature pattern for a channel plugin:
- Parse/validate the package
signals.yaml(includingchannel_drivers+ lifted notification channels). - Register via
PluginRegistrar::forPlugin($manifest, $package)->channelDriver(…). Http::fake()the webhook host; callChannelDriver::send()/ChannelProviderRegistry::test().- Assert resolver inclusion when registered and silent drop when not.
- Assert admin panel shows the driver (plugin badge) and inert rows after the driver disappears.
See tests/Feature/Plugins/SlackWebhookPluginTest.php and tests/Feature/Services/Notifications/PluginChannelLifecycleTest.php.
Minimal checklist
- Add
channel_drivers[](+networkhosts) tosignals.yaml. - Implement
ChannelDriverwith vendor-prefixedkey()matching the manifest. - Call
$registrar->channelDriver($key, $class)inregister(). - Optionally declare a sample
notifications[]entry that lists your logical channel. - Cover send/test with mocked HTTP; verify disable → no crash on send.