> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 包的自定义通知渠道与投递验证

> 讲解如何从包中提供 Laravel 13 的通知渠道，涵盖按类名解析、目标路由、队列处理以及实际投递的测试。

当你想从包中增加通知的投递方式时，需要把**渠道的选择**与**通知目标的确定**分开设计。本页以一个把运维事件记录到日志的小型渠道为例，说明包所提供类的解析路径，以及更新时需要进行的验证。即使替换为向外部服务发送，渠道与通知之间的职责划分也是一样的。

本页基于对 Laravel Framework `v13.38.0` 实现的确认。通知的基本操作请参阅[通知](/zh-CN/notifications.md)，包的测试环境请参阅[包的测试](/zh-CN/advanced/package-testing.md)。

## 类名决定投递方式，路由决定目标

`via()` 返回的渠道类名指定要调用哪个对象的 `send()`。而从 `routeNotificationFor()` 获取的值则是该渠道使用的目标。两者不必使用相同的名称。

| 指定位置 | 本页示例 | 作用 |
| - | - | - |
| 通知的 `via()` | `OperationsLogChannel::class` | 渠道的解析与投递 |
| 可通知对象的路由 | `operationsLog` → `operations` | 渠道内部使用的目标 |
| 通知的 `viaQueues()` | `OperationsLogChannel::class` → `operations-log` | 放入队列时任务的去向 |

`NotificationSender` 会把 `via()` 选出的渠道名传给 `ChannelManager::driver()`。如果 `ChannelManager` 在内置名称和已注册名称中都找不到，而该名称是一个实际存在的类，就会通过容器解析这个类。**如果只按类名使用，则不需要 `Notification::extend()` 或包的服务提供者**。你只需确保 Composer 的自动加载能找到该类，并且容器能够解析其构造函数的依赖。

```mermaid theme={null}
flowchart LR
    A["在 via() 中选择类名"] --> B["ChannelManager::driver()"]
    B --> C{"已解析？"}
    C -->|是| D["保留的渠道"]
    C -->|否| E["通过容器创建"]
    E --> D
    D --> F["在 send() 中获取目标并投递"]
```

## 在包中提供渠道本体

示例中的 `Acme\OperationsLog` 包会把不含敏感信息的事件名与通知目标运维团队名一起记录下来。在 Laravel 应用中，`Psr\Log\LoggerInterface` 被绑定到日志服务，因此可以通过构造函数接收。

```php theme={null}
<?php

namespace Acme\OperationsLog;

use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;

final class OperationsLogChannel
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function send(object $notifiable, Notification $notification): void
    {
        $destination = $notifiable->routeNotificationFor('operationsLog', $notification);

        if (! is_string($destination) || $destination === '') {
            throw new InvalidArgumentException('Operations log destination is required.');
        }

        $this->logger->info('Operations notification', [
            'destination' => $destination,
            'event' => $notification->toOperationsLog($notifiable),
        ]);
    }
}
```

接收的对象预期是使用 `Notifiable` trait 的模型，或由 `Notification::route()` 创建的按需通知目标。前者返回 `routeNotificationForOperationsLog()` 的值，后者返回通过 `route('operationsLog', ...)` 注册的值。为避免在没有目标的情况下被当作已投递处理，本示例会抛出异常。替换为实际的外部投递时，也应确认目标和发送结果，设计成不会把失败吞掉并当作成功。

> [!WARNING]
>
> 请不要在记录到日志的事件名或目标中包含访问令牌或个人信息。本示例是用于演示渠道解析与测试的日志发送，并不是向外部服务投递的实现。

## 在使用方定义通知与目标

应用中的通知返回渠道类，并且只定义传给渠道的消息。使用 `ShouldQueue` 和 `Queueable` 后，普通的 `notify()` 调用会经由队列处理。

```php theme={null}
<?php

namespace App\Notifications;

use Acme\OperationsLog\OperationsLogChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class HealthCheckPassed extends Notification implements ShouldQueue
{
    use Queueable;

    public function via(object $notifiable): array
    {
        return [OperationsLogChannel::class];
    }

    public function toOperationsLog(object $notifiable): string
    {
        return 'health-check-passed';
    }

    public function viaQueues(): array
    {
        return [OperationsLogChannel::class => 'operations-log'];
    }
}
```

当场指定目标时，`Notification::route()` 的键**不是渠道类名**，而是渠道内部调用的 `routeNotificationFor('operationsLog', ...)` 的键。

```php theme={null}
use App\Notifications\HealthCheckPassed;
use Illuminate\Support\Facades\Notification;

Notification::route('operationsLog', 'operations')
    ->notify(new HealthCheckPassed());
```

如果从应用的模型发送，请在使用 `Notifiable` 的模型中准备以下方法。

```php theme={null}
use Illuminate\Notifications\Notification;

public function routeNotificationForOperationsLog(Notification $notification): string
{
    return 'operations';
}
```

对该模型调用 `$user->notify(new HealthCheckPassed())` 也会选中同一个渠道。请配置队列连接，并启动处理 `operations-log` 队列的 worker。

```shell theme={null}
php artisan queue:work --queue=operations-log
```

`OperationsLogChannel` 本身会按渠道保留在 Manager 中，因此**不要把通知目标或通知内容保存在渠道的属性中**，而应每次从 `send()` 的参数中取出。这样即使是长时间运行的队列 worker，也不会把上一条通知的信息带入下一次处理。

## 需要简短别名时

只有当你希望使用者在 `via()` 中指定简短名称而不是类名时，才需要在包的普通服务提供者中使用 `Notification::extend()`。

```php theme={null}
<?php

namespace Acme\OperationsLog;

use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ServiceProvider;

class OperationsLogServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Notification::extend(
            'acme-operations-log',
            fn ($app) => $app->make(OperationsLogChannel::class),
        );
    }
}
```

请通过[包自动发现](/zh-CN/advanced/package-discovery.md)等方式，让服务提供者在投递处理开始前被加载。在选择别名的通知中，把 `via()` 改为 `['acme-operations-log']`，并把 `viaQueues()` 的键也改为同一名称。**用于获取目标的 `operationsLog` 是另一个键**，无需修改。请使用不会与内置渠道名冲突的名称。

Manager 会按名称保留已解析的渠道。即使在同一进程中重新注册别名的创建处理，已经创建的实例也不会被替换。注册方式的变更，也请在新启动的应用或 worker 中进行验证。

## 在 fake 之外测试投递处理

`Notification::fake()` 用于验证发送通知的**意图**。它无法确认渠道的 `send()` 或路由获取是否正常运作。在投递测试中，请使用真实的通知 Manager，并把 `LoggerInterface` 替换为测试用实现。

下面是从 [Testbench 的 `TestCase`](/zh-CN/advanced/package-testing) 运行的示例。使用 `notifyNow()` 后，即使是 `ShouldQueue` 通知，也可以不启动 worker 而直接执行渠道的处理。

```php theme={null}
<?php

namespace Acme\OperationsLog\Tests;

use Acme\OperationsLog\OperationsLogChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification as BaseNotification;
use Illuminate\Support\Facades\Notification;
use Orchestra\Testbench\TestCase;
use Psr\Log\LoggerInterface;

class OperationsLogChannelTest extends TestCase
{
    public function test_channel_writes_the_routed_event(): void
    {
        $notification = new class extends BaseNotification implements ShouldQueue {
            use Queueable;

            public function via(object $notifiable): array
            {
                return [OperationsLogChannel::class];
            }

            public function toOperationsLog(object $notifiable): string
            {
                return 'health-check-passed';
            }
        };

        $logger = $this->createMock(LoggerInterface::class);
        $logger->expects($this->once())->method('info')->with(
            'Operations notification',
            ['destination' => 'operations', 'event' => 'health-check-passed'],
        );

        $this->app->instance(LoggerInterface::class, $logger);

        Notification::route('operationsLog', 'operations')->notifyNow($notification);
    }
}
```

此外，还要确认没有目标时会抛出异常，以及提供别名时能够在全新启动后解析。如果经由队列使用，还应在另外运行队列 worker 的验证应用中，测试任务能够到达 `operations-log` 队列并被处理。

`NotificationSender` 会把 `ShouldQueue` 通知按渠道拆分为任务，并根据 `viaQueues()` 的键选择队列。如果渠道的 `send()` 抛出异常，会触发 `NotificationFailed` 并重新抛出异常；正常返回时则触发 `NotificationSent`。队列的重试设置遵循应用的配置。请注意，如果在渠道内部吞掉异常，即使投递失败也会被当作成功处理。

## 参考的一手资料

* [Laravel 13 官方文档：自定义通知渠道与队列](https://github.com/laravel/docs/blob/13.x/notifications.md#custom-channels)
* [Laravel 13 官方文档：包自动发现](https://github.com/laravel/docs/blob/13.x/packages.md#package-discovery)
* [ChannelManager：根据类名解析](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Notifications/ChannelManager.php)
* [NotificationSender：按渠道的投递、队列与事件](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Notifications/NotificationSender.php)
* [RoutesNotifications 与 AnonymousNotifiable：通知目标路由](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Notifications/RoutesNotifications.php)
* [Manager：驱动的保留与扩展](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Support/Manager.php)
* [Application：LoggerInterface 的绑定](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Foundation/Application.php)


## Related topics

- [通知（Notifications）](/zh-CN/notifications.md)
- [自定义验证规则](/zh-CN/advanced/custom-validation-rules.md)
- [创建 Boost 的自定义 Agent](/zh-CN/advanced/boost-custom-agent.md)
- [包的服务解析后钩子](/zh-CN/advanced/package-service-resolution.md)
- [包的 Storage 驱动注册与重新解析](/zh-CN/advanced/package-filesystem-drivers.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.