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

# Mercure 廣播驅動的內部結構

> 以原始碼為基礎，解說 Laravel framework 新增的 Mercure 廣播驅動的運作方式。內容涵蓋與 Reverb、Pusher 不同的 HTTP/SSE 架構、授權 Cookie，以及端對端加密頻道。

<Warning>
  本頁介紹的 `MercureBroadcaster` 是在 [laravel/framework PR #61474](https://github.com/laravel/framework/pull/61474)（於 2026 年 9 月 10 日合併）中新增的功能。撰寫本文時尚未包含於任何已標記的正式版本中，[官方文件](https://laravel.com/docs/broadcasting)也還沒有相關說明。這是根據原始碼與 docblock 所做的先行解說，若要實際使用，請務必確認 `laravel/framework` 的變更歷程。
</Warning>

## 概觀

長期以來，Laravel 的廣播功能提供 [Reverb](/zh-TW/broadcasting)、Pusher、Ably 等「WebSocket 原生」驅動，如今 `config/broadcasting.php` 的 `driver` 又新增了 `mercure` 這個選項。

[Mercure](https://mercure.rocks) 是以 Server-Sent Events (SSE) 為基礎的即時通訊協定。它不像 WebSocket 那樣採用雙向連線，而是接近 HTTP/1.1 或 HTTP/2 上的長輪詢，因此具有以下特點：

* 可以透明地穿越一般的 HTTP 基礎設施（反向代理、CDN、負載平衡器）
* 客戶端只需瀏覽器的 `EventSource` API 就能實作，不需要專屬客戶端函式庫
* [FrankenPHP](/zh-TW/blog/laravel-cloud) 內建 Mercure hub，不需要額外基礎設施即可運作

```mermaid theme={null}
flowchart LR
    A["伺服器<br>broadcast(event)"] --> B["MercureBroadcaster"]
    B --> C["Mercure hub<br>(Hub / FrankenPhpHub)"]
    C -->|"SSE (EventSource)"| D["瀏覽器<br>Laravel Echo"]
    D --> E["UI 的<br>即時更新"]
```

## 新增的 `driver` 值

`config/broadcasting.php` 開頭列出支援驅動的註解中，新增了 `mercure`：

```php theme={null}
// Supported: "reverb", "pusher", "ably", "mercure", "redis", "log", "null"
```

同時也提供了連線設定的範例：

```php theme={null}
'mercure' => [
    'driver' => 'mercure',
    'url' => env('MERCURE_URL'),
    'public_url' => env('MERCURE_PUBLIC_URL'),
    'secret' => env('MERCURE_JWT_SECRET'),
    'encryption_key' => env('MERCURE_ENCRYPTION_KEY'),
    'claims' => [
        'iss' => env('MERCURE_JWT_ISSUER'),
        'client_id' => env('APP_NAME'),
    ],
    'cookie_name' => env('MERCURE_COOKIE_NAME'),
    'subscribe_expiration' => (int) env('MERCURE_SUBSCRIBE_EXPIRATION', 5),
],
```

若省略 `url`，驅動會自動退回使用 FrankenPHP 內建的 Mercure hub（即 `mercure_publish()` 函式）。此判斷位於 `CreatesMercureDrivers::mercure()`：

```php theme={null}
public function mercure(array $config)
{
    if (empty($config['url'])) {
        return $this->frankenPhpMercure($config);
    }

    // ...
}
```

若使用外部 Mercure hub，`url` 應指向用於發布的管理 API，而 `public_url` 則是瀏覽器所看到的 URL。之所以分開這兩者，是為了支援「透過內部網路（例如 Docker Compose）發布訊息，而只把公開 URL 暴露給瀏覽器」這類部署方式。

## 分離 publish 與 subscribe 兩種權杖

Mercure 是以 JWT (JSON Web Token) 進行存取控制的協定。`CreatesMercureDrivers` trait 會為 publish（伺服器 → hub）與 subscribe（瀏覽器 → hub）分別建立**獨立的權杖工廠**：

* `secret`、`publish_secret`、`subscribe_secret` 可以個別設定，未設定時會退回使用 `secret`
* `algorithm`、`publish_algorithm`、`subscribe_algorithm` 同樣可個別設定（預設 `HS256`）
* HS256 要求 secret 至少 32 bytes、HS384 至少 48 bytes、HS512 至少 64 bytes，未達標會拋出 `InvalidArgumentException`

```php theme={null}
protected function mercureSecret(array $config, string $side)
{
    $secret = ($config[$side.'_secret'] ?? null) ?: ($config['secret'] ?? null);

    // ...

    $minimumLength = ['HS256' => 32, 'HS384' => 48, 'HS512' => 64][$algorithm] ?? 0;

    if (strlen($secret) < $minimumLength) {
        throw new InvalidArgumentException(/* ... */);
    }

    return $secret;
}
```

publish 權杖持有針對所有 topic（`*`）的 `Grant::ACTION_PUBLISH` 權限，並會由 `CachingTokenProvider` 記憶快取，避免每次廣播都要重新產生 JWT。

## 以單一 Cookie 進行頻道授權

Mercure 與 WebSocket 驅動最大的架構差異，在於訂閱端的授權是透過**單一 Cookie** 完成的。`MercureBroadcaster::auth()` 會接收 `channel_names` 陣列（每個請求上限 100 個），逐一評估頻道授權，然後一次發出一個涵蓋所有頻道的授權 Cookie：

```php theme={null}
public function auth($request)
{
    $channelNames = (array) $request->input('channel_names', []);

    if ($channelNames === [] ||
        count($channelNames) > 100 ||
        $channelNames !== array_filter($channelNames, 'is_string')) {
        throw new AccessDeniedHttpException;
    }

    // 逐一評估每個頻道的授權並累積 grant...

    return (new JsonResponse([/* ... */]))
        ->cookie($this->makeAuthorizationCookie($request, $grants, $user));
}
```

值得注意的重點是：**單一頻道授權失敗，並不會讓整個回應失敗**。被拒絕的頻道只會在回應中被標記為 `denied: true`，其他已授權的頻道仍可繼續訂閱。這樣的設計是因為 Mercure 在單一 EventSource 連線上多工承載多個 topic，所以要能讓頻道在連線中途新增或移除，同時不中斷既有的連線。

Presence 頻道會發出與一般 `private` / `private-encrypted` 頻道不同形狀的 `Grant`，其 `subscribe` 權限對應到 Mercure [Subscription API](https://mercure.rocks/docs/hub/concepts/active-subscriptions) 所使用的訂閱 URL 樣式。

## 端對端加密頻道

以 `private-encrypted-` 為前綴的頻道會被視為端對端加密（E2EE）頻道 —— 也就是說，連 Mercure hub 本身都看不到 payload。這是既有 Pusher 與 Reverb 驅動所沒有的能力。

只要將 `encryption_key` 設定為 base64 編碼的 32 bytes 金鑰，`ChannelEncrypter` 就會啟用，並在 `broadcast()` 送出訊息時，將事件名稱、payload 與 socket ID 一併封裝成 JWE（JSON Web Encryption）。hub 中繼的只會是加密後的位元組資料。

```php theme={null}
'encryption_key' => env('MERCURE_ENCRYPTION_KEY'),
```

```shell theme={null}
php -r "echo base64_encode(random_bytes(32));"
```

訂閱端則透過 `auth()` 回應中攜帶的 `jwk`（JSON Web Key）欄位，在瀏覽器內完成解密。這是 Mercure 規範建議的「將金鑰放在授權回應中以帶外方式傳遞」機制，hub 完全不會知道金鑰內容。

<Info>
  Presence 頻道無法加密，因為成員列表本來就需要透過 hub 的 Subscription API 流動，這在設計上就與 E2EE 不相容。
</Info>

## Whisper（客戶端之間的直接訊息）

當 `client_events` 啟用時（預設為 true），每個受保護的頻道都會被指派一個專屬的「whisper topic」，訂閱者僅能對該 topic 擁有 `publish` 權限：

```php theme={null}
if ($this->clientEvents && $whisperTopics !== []) {
    $grants[] = new Grant([Grant::ACTION_SUBSCRIBE, Grant::ACTION_PUBLISH], $whisperTopics);
}
```

頻道本身的 topic 仍為伺服器專用，因此客戶端不可能偽造成伺服器發出的事件。這樣的設計類似 Pusher 的「client events」功能，但透過分離 topic 讓權限劃分更為清楚。

## Topic 命名規則

由於 Mercure hub 常常被多個應用共用，因此 topic 會以 `topic_prefix` 加上命名空間以避免衝突。預設值如下：

```php theme={null}
protected string $topicPrefix = 'https://laravel.alt/echo/',
```

`.alt` 是 DNS 中保留、無法解析的網域字尾（[RFC 9476](https://www.rfc-editor.org/rfc/rfc9476.html)），因此絕不會與真實網域衝突。頻道名稱會依 [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html) 進行 URL 編碼，並收納在單一路徑區段中：

```php theme={null}
protected function channelTopic($channelName)
{
    return $this->topicPrefix.'channel/'.rawurlencode($channelName);
}
```

## Cookie 網域與錯誤訊息

Mercure hub 常常會與應用執行在不同的子網域上（例如 `mercure.example.com` 與 `app.example.com`），因此當找不到共用的 Cookie 網域時，會拋出方便定位問題的例外訊息：

```php theme={null}
throw new BroadcastException(sprintf(
    'Mercure error: %s. Adjust the Mercure "public_url" configuration value so the hub [%s] shares a registrable domain with the application host [%s].',
    rtrim($e->getMessage(), '.'), $this->hub->getPublicUrl(), $request->getHost()
), 0, $e);
```

若使用帶有 `__Secure-` 或 `__Host-` 前綴的 Cookie 名稱，驅動也會在啟動時檢查 `public_url` 是否為 HTTPS：

```php theme={null}
if (str_starts_with($hub->getCookieName(), '__') &&
    parse_url($hub->getPublicUrl(), PHP_URL_SCHEME) === 'http') {
    throw new InvalidArgumentException(/* ... */);
}
```

## 如何在 Reverb、Pusher/Ably 與 Mercure 之間取捨

| 驅動            | 傳輸方式       | 基礎設施                     | 端對端加密                   |
| ------------- | ---------- | ------------------------ | ----------------------- |
| Reverb        | WebSocket  | 需自行架設伺服器                 | 無                       |
| Pusher / Ably | WebSocket  | SaaS                     | 無                       |
| Mercure       | SSE (HTTP) | 自行架設 hub，或內建於 FrankenPHP | 有（`private-encrypted-`） |

Mercure 適合不需要持續雙向連線的情境 —— 例如通知、進度更新，或以伺服器對客戶端為主的類聊天應用。若你已經使用 FrankenPHP，更是完全不需要額外的基礎設施。

## 相關頁面

* [廣播基礎](/zh-TW/broadcasting)
* [Laravel Cloud](/zh-TW/blog/laravel-cloud)（以 FrankenPHP 為基礎的執行環境）


## Related topics

- [廣播](/zh-TW/broadcasting.md)
- [套件自動偵測的內部結構](/zh-TW/advanced/package-discovery.md)
- [自訂驗證 Guard 的實作](/zh-TW/advanced/custom-auth-guard.md)
- [PHP FFI](/zh-TW/advanced/ffi.md)
- [Laravel 套件開發](/zh-TW/advanced/package-development.md)
