Skip to main content
When you want a package to add a new way to deliver notifications, design channel selection and destination resolution as separate concerns. This page uses a small channel that records operational events to the log as an example, and explains how classes shipped in a package are resolved and what you need to verify when you update them. If you replace the log with delivery to an external service, the division of responsibilities between the channel and the notification stays the same. This page is based on the Laravel Framework v13.38.0 implementation. For basic notification usage, see Notifications. For setting up a package test environment, see Package testing.

The class name is the delivery method, the route is the destination

The channel class name returned from via() determines which object’s send() method is called. The value retrieved through routeNotificationFor(), on the other hand, is the destination that the channel uses. The two don’t need to share the same name. NotificationSender passes the channel name chosen by via() to ChannelManager::driver(). If ChannelManager can’t find the name among built-in or registered names and the name refers to an existing class, it resolves that class from the container. If you only use the class name, you don’t need Notification::extend() or a package service provider. Just make sure Composer’s autoloader can find the class and the container can resolve its constructor dependencies.

Ship the channel itself in your package

The example Acme\OperationsLog package records event names that contain no sensitive information, together with the name of the operations team being notified. In a Laravel application, Psr\Log\LoggerInterface is bound to the logging service, so you can receive it through the constructor.
The channel expects to receive either a model that uses the Notifiable trait or an on-demand notifiable created by Notification::route(). For the former, the value comes from routeNotificationForOperationsLog(); for the latter, it’s the value registered with route('operationsLog', ...). This example throws an exception so that a notification without a destination is never treated as delivered. When you replace this with real external delivery, check the destination and the send result as well, and design the channel so that failures are never swallowed and reported as success.
Don’t include access tokens or personal information in the event names or destinations you write to the log. This example sends to the log only to demonstrate channel resolution and testing. It isn’t a delivery implementation for an external service.

Define the notification and destination in the consuming application

The application’s notification returns the channel class and defines only the message passed to the channel. When you use ShouldQueue and Queueable, a regular notify() call goes through the queue.
When you specify the destination on the fly, the key passed to Notification::route() is not the channel class name. It’s the key the channel uses when it calls routeNotificationFor('operationsLog', ...).
To send from an application model, add the following method to a model that uses Notifiable.
Calling $user->notify(new HealthCheckPassed()) on this model selects the same channel. Configure a queue connection and start a worker that processes the operations-log queue.
The Manager retains one OperationsLogChannel instance per channel, so don’t store the destination or notification content in channel properties. Read them from the arguments of each send() call instead. This keeps long-running queue workers from carrying information over from a previous notification.

When you need a short alias

Only if you want users to specify a short name in via() instead of the class name, use Notification::extend() in your package’s regular service provider.
Make sure the provider is loaded before any delivery starts, for example through package discovery. In notifications that use the alias, change via() to ['acme-operations-log'] and change the viaQueues() key to the same name. The operationsLog key used to retrieve the destination is a separate key, so you don’t need to change it. Choose a name that doesn’t collide with built-in channel names. The Manager retains resolved channels by name. Re-registering the alias’s creator within the same process doesn’t replace an instance that has already been created. When you change how the channel is registered, also verify the change in a freshly booted application and worker.

Test delivery separately from fake

Notification::fake() verifies the intent to send a notification. It can’t confirm that the channel’s send() runs or that the route is retrieved. To test delivery, use the real notification Manager and replace LoggerInterface with a test double. The following example runs from Testbench’s TestCase. With notifyNow(), the channel logic runs without starting a worker, even for a ShouldQueue notification.
Also verify that a missing destination throws an exception and, if you provide an alias, that it resolves from a fresh boot. If the channel is used through the queue, also test in a verification application that runs a separate queue worker that jobs arrive on the operations-log queue and are processed. NotificationSender splits a ShouldQueue notification into one job per channel and selects the queue using the viaQueues() key. If the channel’s send() throws an exception, it dispatches NotificationFailed and rethrows the exception; if send() returns normally, it dispatches NotificationSent. Queue retry behavior follows the application’s configuration. Note that if you swallow exceptions inside the channel, failed deliveries are treated as successful.

Primary sources

Last modified on October 22, 2026