> ## 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-CN/broadcasting.md)，以及用于确认的最新版本 `v13.42.0` 的实现。事件的发送方法请参阅[广播](https://kawax.biz/zh-CN/broadcasting.md)，提供者的加载请参阅[包自动发现](https://kawax.biz/zh-CN/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

- [广播](https://kawax.biz/zh-CN/broadcasting.md)
- [包的授权 Policy 注册与覆盖](https://kawax.biz/zh-CN/advanced/package-authorization.md)
- [Mercure 广播驱动内部机制](https://kawax.biz/zh-CN/advanced/mercure-broadcasting.md)
- [授权（Gate 与 Policy）](https://kawax.biz/zh-CN/authorization.md)
- [Laravel Reverb](https://kawax.biz/zh-CN/reverb.md)


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