PHP项目Symfony mercure与实时更新

wen PHP项目 3

本文目录导读:

PHP项目Symfony mercure与实时更新

  1. 核心概念
  2. 安装与配置
  3. 后端发布更新(Publisher)
  4. 前端订阅(Subscriber - JavaScript)
  5. 私有通道(Private Topics)
  6. 生产环境注意事项
  7. 常见问题排查

这是一个关于在 Symfony 项目中使用 Mercure 实现实时更新的专业问题,Mercure 是 Symfony 官方推荐的实时通信协议,它允许服务器向客户端推送事件(Server-Sent Events, SSE),非常适合实现如通知、聊天、实时数据仪表盘等功能。

下面我将从核心概念、安装配置、后端发布、前端订阅到高级用法(如私有通道)为你做一个完整的梳理。


核心概念

  1. Hub(中心):Mercure 的服务器核心组件,它接收来自后端(Publisher)的更新,并将其转发给订阅的客户端(Subscriber),Hub 可以是一个独立的服务(如 mercure.rocks 提供的 Caddy 版)或者 Symfony 应用内的一个路由。
  2. Publisher(发布者):通常是你的 Symfony 后端,它通过 HTTP POST 请求向 Hub 发送更新数据。
  3. Subscriber(订阅者):浏览器端 JavaScript 或移动端,它通过 SSE 或 WebTransport 连接到 Hub,订阅特定主题(Topic)。
  4. Topic(主题):一个 URL 或 URI,用于标识资源的更新。https://your-app.com/books/123 或内置主题 /users/456
  5. JWT(JSON Web Token):安全关键,Hub 使用 JWT 来验证发布者和订阅者的身份以及权限。

安装与配置

安装 Symfony Mercure Bundle

composer require symfony/mercure-bundle

.env 中配置 Hub

你需要一个正在运行的 Mercure Hub,最简单的方式是使用安装在本地或 Docker 中的 Hub。

# 本地开发(用于测试)
MERCURE_URL=https://localhost:3000/.well-known/mercure
MERCURE_PUBLIC_URL=https://localhost:3000/.well-known/mercure
MERCURE_JWT_SECRET=!ChangeThisMercureHubJWTSecretKey!

生产环境注意MERCURE_JWT_SECRET 必须非常复杂且保密。

配置 mercure.yaml

# config/packages/mercure.yaml
mercure:
    hubs:
        default:
            url: '%env(MERCURE_URL)%'
            public_url: '%env(MERCURE_PUBLIC_URL)%'
            jwt:
                secret: '%env(MERCURE_JWT_SECRET)%'
                publish: ['*']  # 允许发布所有主题(生产环境应限制)
                subscribe: ['*'] # 允许订阅所有主题(生产环境应限制)

后端发布更新(Publisher)

在你的 Symfony 控制器或服务中注入 MercurePublisher 或使用 UpdateFactory

案例:发布一个“新订单通知”

// src/Controller/OrderController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Mercure\PublisherInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class OrderController extends AbstractController
{
    #[Route('/order/new', name: 'order_new')]
    public function newOrder(PublisherInterface $publisher): Response
    {
        $orderId = rand(1000, 9999);
        $data = [
            'type' => 'new_order',
            'id' => $orderId,
            'message' => 'A new order has been placed!'
        ];
        // 创建 Update
        $update = new Update(
            topics: ['https://your-app.com/orders', '/admin/notifications'], // 可以发布到多个主题
            data: json_encode($data),
            private: false, // 是否私有(需要订阅者的 JWT 有对应主题权限)
            id: null, // 可选,用于去重
            type: 'NewOrder' // 可选,event 类型
        );
        // 发布
        $publisher($update);
        return new Response('Order ' . $orderId . ' created and published.');
    }
}

关键点

  • topics 是一个数组,可以指定一个或多个主题,如上例,前端可以订阅 /orders/notifications 来接收此消息。
  • data 是 JSON 字符串。
  • private 决定该 Update 是否经过 JWT 权限检查。

前端订阅(Subscriber - JavaScript)

在浏览器中监听 Mercure Hub 的事件。

<script>
const eventSource = new EventSource(
    'https://localhost:3000/.well-known/mercure?topic=https://your-app.com/orders',
    {
        // 如果需要认证,可以在这里添加 JWT
        // headers: { 'Authorization': 'Bearer <your_subscriber_jwt>' }
        // 注意:EventSource 本身不支持自定义请求头。
        // 对于生产,请使用 Authorization 查询参数(需要 Hub 支持)或使用 fetch + ReadableStream。
    }
);
eventSource.onmessage = (event) => {
    console.log('Received update:', event.data);
    const data = JSON.parse(event.data);
    displayNotification(data);
};
eventSource.addEventListener('NewOrder', (event) => { // 匹配 type
    const data = JSON.parse(event.data);
    console.log('New Order:', data);
});
eventSource.onerror = (error) => {
    console.error('Mercure connection error', error);
};
</script>

重要:EventSource 无法自定义 Authorization 请求头,在生产中,有两种常用方法解决认证问题:

  1. 查询参数https://localhost:3000/.well-known/mercure?topic=...&authorization=Bearer <token> (需要 Hub 配置支持)。
  2. Symfony 代理:在 Symfony 应用内创建一个反向代理端点,将 /mercure/* 转发给 Hub,并在转发时添加 JWT。

私有通道(Private Topics)

这是 Mercure 的核心优势,用于实现用户专属通知。

后端生成 Subscriber JWT

use Symfony\Component\Mercure\Jwt\TokenFactoryInterface;
class NotificationService
{
    public function __construct(
        private TokenFactoryInterface $tokenFactory
    ) {}
    public function generateUserTopicJwt(int $userId): string
    {
        $token = $this->tokenFactory->create([
            'mercure' => [
                'subscribe' => ["/users/{$userId}"],
                'publish' => ["/users/{$userId}"] // 如果你想允许用户自行发布
            ]
        ]);
        return $token;
    }
}

发布私有更新

$update = new Update(
    topics: ["/users/{$targetUserId}"],
    data: json_encode(['message' => 'Private notification']),
    private: true // 标记为私有
);
$publisher($update);

前端订阅私有主题

前端需要带着拥有 /users/{$targetUserId} 订阅权限的 JWT 连接 Hub。

// 从你的 REST API 获取 token
const token = await fetch('/api/mercure-token').then(res => res.json());
const eventSource = new EventSource(
    `https://localhost:3000/.well-known/mercure?topic=/users/${userId}&authorization=Bearer ${token}`
);

生产环境注意事项

  1. Hub 部署:最推荐使用官方推荐的 Caddy Mercure 镜像,它性能好且易于配置,也可以使用 Symfony 的 PHP Hub(适合小规模)。
  2. JWT 安全:生成 JWT_SECRET 使用 openssl rand -hex 64,发布者和订阅者的 JWT 权限要严格分离,对于订阅者,仅授予其必须的主题权限(如 /users/{userId})。
  3. 性能与扩展
    • 如果用户量很大,不要将 Hub 与 Symfony 应用放在同一进程。
    • 使用 Redis 作为 Hub 的后端(Caddy Mercure 支持),以实现水平扩展。
    • 考虑使用负载均衡器,确保 WebSocket/SSE 连接保持到同一实例(Sticky Session)或使用外部广播机制。
  4. 错误处理与重连:SSE 会自动重连,在 onerror 中不要自动关闭连接,可以显示状态提示,浏览器有并发连接限制(通常每个域名 6 个 HTTP/1.1 连接),如果订阅多个主题,考虑使用单个连接订阅多个 topic(用 topic 查询参数传递多个值 ?topic=a&topic=b)。
  5. 请求合并:不要每个组件都创建一个 EventSource,创建全局的 Mercure 服务(如 React Context 或 Vue Pinia store),管理一个连接,并根据主题分发事件。

常见问题排查

  • Chrome 控制台报 404:Hub 地址错误或未运行。
  • 连接建立后又断开:可能是 JWT 过期或无效,检查你的 JWT mercure 权限是否包含所需主题。
  • 收到事件但前端无反应:检查 event.type 是否匹配(如果使用了自定义 type),或 data 格式是否正确。
  • 私有主题报 403:JWT 中缺少对该特定主题的 subscribe 权限。

使用 Symfony Mercure 实现实时更新的流程非常清晰:

  1. 配置 Hub 与 JWT
  2. 后端发布 Update 到主题。
  3. 前端订阅 对应主题的 SSE 流。

对于复杂场景,私有通道 配合 JWT 权限管理是保障实时通信安全的关键,Mercure 的设计将实时推送与常规 HTTP API 解耦,是 Symfony 生态中处理实时更新最优雅的方案之一。

抱歉,评论功能暂时关闭!