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

# 套件的廣播頻道授權與覆寫

> 說明套件註冊私有頻道時授權的評估順序、使用端應用程式覆寫它的機制，以及如何用 HTTP 測試確認。

套件在推送即時事件時，要把**頻道名稱的定義**與**放行使用者的判定**分開設計。套件在 `boot()` 中註冊的頻道，若與使用端應用程式 `routes/channels.php` 中的名稱相同，可能會被覆寫。理解這種註冊順序與評估順序，可以減少維護時的錯誤。

本頁參考了 Laravel 官方文件的[廣播](https://kawax.biz/zh-TW/broadcasting.md)，以及用於確認的最新版本 `v13.42.0` 的實作。事件的傳送方式請參閱[廣播](https://kawax.biz/zh-TW/broadcasting.md)，提供者的載入請參閱[套件自動探索](https://kawax.biz/zh-TW/advanced/package-discovery.md)。

## 頻道授權的機制

訂閱私有頻道時，用戶端會向 `/broadcasting/auth` 傳送請求。`BroadcastController::authenticate()` 將處理交給 `Broadcast::auth()`，由 `Broadcaster` 依序評估已註冊的回呼。

評估規則如下。

* 回呼回傳 `false` 時，會立即拋出 `AccessDeniedHttpException`。
* 回傳 `true` 等真值時，授權在此時成立。
* 回傳 `null` 等假值時，會繼續評估下一個註冊的回呼。若沒有任何回呼放行，最後會拋出 `AccessDeniedHttpException`。

> [!NOTE]
>
> `Pusher` 與 `Reverb` 驅動程式會在授權前移除 `private-` 與 `presence-` 前綴。因此，註冊的頻道名稱中不要包含前綴。

## 在套件中註冊頻道

以一個虛構的 `Acme\Orders` 套件推送訂單狀態變化為例。頻道名稱為 `acme.orders.{order}`，只允許訂單的擁有者訂閱。

在套件的 `ServiceProvider` 中，於 `boot()` 內呼叫 `Broadcast::channel()`。判定委派給套件內的類別。

```php theme={null}
use Acme\Orders\Broadcasting\OrderChannel;
use Illuminate\Support\Facades\Broadcast;

public function boot(): void
{
    Broadcast::channel('acme.orders.{order}', OrderChannel::class);
}
```

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

namespace Acme\Orders\Broadcasting;

use Acme\Orders\Models\Order;
use Illuminate\Contracts\Auth\Authenticatable;

class OrderChannel
{
    public function join(Authenticatable $user, Order $order): bool
    {
        return $user->getAuthIdentifier() === $order->owner_id;
    }
}
```

`join()` 的第 1 個參數會傳入授權時的使用者。之後的參數是頻道名稱中 `{...}` 的值。當 `{order}` 的參數名稱與 `Order $order` 一致，且型別是 `UrlRoutable` 的子類別時，路由模型繫結就會生效。

> [!NOTE]
>
> `Broadcast::channel()` 的 `$callback` 可以指定 `callable`，或帶有 `join()` 方法的類別名稱。指定類別名稱時，`Broadcaster::normalizeChannelHandlerToCallable()` 會透過容器建立實例，然後呼叫 `join()`。

## 使用端應用程式的覆寫

使用端應用程式傳給 `withBroadcasting()` 的檔案（通常是 `routes/channels.php`）會在 `withBroadcasting()` 註冊的 `booted` 回呼中載入。`booted` 回呼在所有服務提供者的 `boot()` 結束後執行，因此套件 `boot()` 中的註冊會先進行。

| 順序 | 處理 | 註冊的內容 |
| - | - | - |
| 1 | 套件的 `boot()` | `acme.orders.{order}` 的回呼 |
| 2 | 在應用程式的 `booted` 中載入路由 | `routes/channels.php` 的回呼 |

`Broadcaster::channel()` 以頻道名稱為鍵，儲存到內部陣列中。因此，若使用端應用程式註冊了相同的名稱，第 2 次註冊會取代套件的註冊。Laravel 不會把同名註冊視為重複錯誤。

要防止覆寫，請讓頻道名稱唯一。例如像 `acme.orders.{order}` 這樣加上套件專屬的前綴，使用端應用程式註冊相同名稱的可能性就會降低。
頻道名稱是與用戶端之間公開的約定。一旦決定，請不要變更。變更後，舊的用戶端將無法訂閱。

> [!WARNING]
>
> 對於同名頻道，後註冊的會在沒有任何提示的情況下優先。`php artisan channel:list` 會顯示頻道名稱與解析目標（類別名稱或 `Closure`），可以確認留下的是哪一個註冊。

## 頻道選項與守衛

`Broadcast::channel()` 的第 3 個參數 `$options` 會作為每個頻道的設定儲存。指定 `guards` 後，會依序嘗試用於認證的守衛。

```php theme={null}
Broadcast::channel('acme.orders.{order}', OrderChannel::class, [
    'guards' => ['web', 'api'],
]);
```

省略 `guards` 時，使用請求的預設使用者（`$request->user()`）進行授權。指定 `guards` 後，會從頭開始依序呼叫 `$request->user($guard)`，並使用最先取得的使用者。若套件不寫死守衛名稱，而是作為設定值公開，使用端應用程式就可以自行選擇。

## 在測試中確認頻道註冊

已註冊的頻道可以透過驅動程式的 `getChannels()` 以 `Collection` 形式取得。鍵就是頻道名稱本身。

```php theme={null}
use Illuminate\Support\Facades\Broadcast;

public function test_package_registers_order_channel(): void
{
    $channels = Broadcast::driver()->getChannels();

    $this->assertTrue($channels->has('acme.orders.{order}'));
}
```

授權結果透過對 `/broadcasting/auth` 的 HTTP 測試來確認。`null` 與 `log` 驅動程式的 `auth()` 不回傳任何內容，因此不能用於此測試。頻道會在應用程式啟動時註冊到預設驅動程式，因此請先在測試用的 `phpunit.xml` 等處把 `BROADCAST_CONNECTION` 設為 `reverb` 或 `pusher`，再傳送帶認證的請求。

```php theme={null}
use Acme\Orders\Models\Order;
use App\Models\User;

public function test_owner_can_join_order_channel(): void
{
    $user = User::factory()->create();
    $order = Order::factory()->for($user, 'owner')->create();

    $this->actingAs($user)
        ->post('/broadcasting/auth', [
            'channel_name' => 'private-acme.orders.'.$order->id,
            'socket_id' => '1234.5678',
        ])
        ->assertOk();
}
```

> [!TIP]
>
> 測試應用程式中需要註冊 `/broadcasting/auth`。這個路由由 `Broadcast::routes()` 註冊，`BroadcastManager::channelRoutes()` 只是把相同的處理交給 `routes()`。若路由已被快取，`routes()` 不會執行任何操作。

## 維護時的注意事項

* 頻道名稱是公開的約定。變更名稱後，現有用戶端將無法訂閱。
* 以類別為基礎的頻道透過 `Container` 解析。建構子的相依請使用容器能夠解析的型別。

## 總結

套件的頻道授權只要在 `boot()` 中註冊 `Broadcast::channel()` 即可運作。若使用端應用程式的 `routes/channels.php` 之後註冊了相同的名稱，套件的授權就會被取代。為頻道名稱加上前綴、把判定拆分到類別中，並用 HTTP 測試確認 `/broadcasting/auth` 的結果，可以讓套件更易於維護。

## 參考的一手資料

* [Laravel 官方文件：廣播](https://github.com/laravel/docs/blob/13.x/broadcasting.md)
* [Framework v13.42.0：Broadcaster](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php)
* [Framework v13.42.0：UsePusherChannelConventions](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Broadcasting/Broadcasters/UsePusherChannelConventions.php)
* [Framework v13.42.0：BroadcastManager](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Broadcasting/BroadcastManager.php)
* [Framework v13.42.0：ApplicationBuilder](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php)
* [Framework v13.42.0：BroadcastController](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Broadcasting/BroadcastController.php)
* [Framework v13.42.0：channel:list 指令](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Console/ChannelListCommand.php)


## Related topics

- [套件的授權 Policy 註冊與覆寫](https://kawax.biz/zh-TW/advanced/package-authorization.md)
- [廣播](https://kawax.biz/zh-TW/broadcasting.md)
- [授權（Gate 與 Policy）](https://kawax.biz/zh-TW/authorization.md)
- [Mercure 廣播驅動的內部結構](https://kawax.biz/zh-TW/advanced/mercure-broadcasting.md)
- [套件翻譯的覆寫與更新](https://kawax.biz/zh-TW/advanced/package-translations.md)


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