> ## 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-TW/notifications.md)，套件的測試環境請參閱[套件測試](/zh-TW/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-TW/advanced/package-discovery.md)等方式，讓提供者在傳送處理開始前載入。在選用別名的通知中，將 `via()` 改為 `['acme-operations-log']`，`viaQueues()` 的鍵也改為相同名稱。**用於取得收件目標的 `operationsLog` 是另一個鍵**，因此不需要變更。請使用不會與內建頻道名稱衝突的名稱。

Manager 會依名稱保留已解析的頻道。即使在同一個行程中重新註冊別名的建立處理，已建立的實例也不會被取代。變更註冊方式時，也請在新啟動的應用程式或 worker 中進行驗證。

## 在 fake 之外另行測試傳送處理

`Notification::fake()` 是用來驗證傳送通知的**意圖**。它無法確認頻道的 `send()` 或路由的取得是否正常運作。測試傳送時，請使用真正的通知 Manager，並將 `LoggerInterface` 替換為測試用的實作。

以下是從 [Testbench 的 `TestCase`](/zh-TW/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

- [通知頻道 - Laravel Bluesky](/zh-TW/packages/laravel-bluesky/notification.md)
- [Laravel Bluesky](/zh-TW/packages/laravel-bluesky/index.md)
- [Laravel 啟動套件的建立方式](/zh-TW/advanced/starter-kit-creation.md)
- [套件的服務解析後掛鉤](/zh-TW/advanced/package-service-resolution.md)
- [教學 - Laravel Console Starter](/zh-TW/packages/laravel-console-starter/tutorial.md)


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