☰
Symfony Notifier Mercure 桥接组件实战:DSN 配置、ChatMessage 选项与底层发布原理
2026/10/4 13:38:00 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】symfony

The Symfony PHP framework

项目地址:https://gitcode.com/GitHub_Trending/sy/symfony
点击查看免费下载

Mercure 是一个基于 Server-Sent Events (SSE) 的实时通信协议,Symfony 通过symfony/mercure-notifier桥接组件将 Mercure Hub 无缝接入 Notifier 体系,使开发者可以像发送短信、邮件那样,用统一的ChatMessage把实时通知推送给浏览器端订阅者。本文以 Notifier Mercure Bridge README 为主线,结合仓库内 MercureTransport.php、MercureOptions.php 与 MercureTransportFactory.php 等源码实现,完整讲解 DSN 配置、消息选项、发送链路与异常处理,读完即可在生产项目中落地 Mercure 实时通知。

一、桥接组件概览:把 Mercure Hub 变成 Notifier 的 Transport

该桥接位于src/Symfony/Component/Notifier/Bridge/Mercure/目录,包名为symfony/mercure-notifier(见 composer.json)。它实现了 Notifier 的 Transport 抽象,使 ChatMessage 能通过任意 Mercure Hub 发布实时事件:

  • MercureTransportFactory.php:解析mercure://开头的 DSN,从配置的 Hub 注册表中取出对应 Hub;
  • MercureTransport.php:真正的发送逻辑,把 ChatMessage 转换为 MercureUpdate并调用HubInterface::publish()发布;
  • MercureOptions.php:承载每条消息的发布选项(topic、private、id、retry、content 等)。

根据 CHANGELOG.md,该桥接自 5.3 版本引入,并在 7.3 版本新增了content选项(用于携带 Web Notification 标准的通知内容)。当前 composer.json 声明依赖 PHP>=8.4.1、symfony/notifier^7.4|^8.0以及symfony/mercure^0.5.2|^0.6|^0.7|^0.8。

二、DSN 配置与工厂解析原理

2.1 DSN 语法

README 给出的 DSN 示例为:

MERCURE_DSN=mercure://HUB_ID?topic=TOPIC

字段含义:

参数说明
HUB_IDMercure Hub 的标识符(id),对应 Symfony 配置中注册的 Hub 名称
TOPIC要发布到的 topic IRI,可选。默认值为https://symfony.com/notifier。支持单个 topic(topic=https://foo),也支持多个 topic(topic[]=/foo/1&topic[]=https://bar)

在 Symfony 应用中,DSN 通常写入.env或.env.local:

MERCURE_DSN=mercure://default?topic=/notifications

2.2 工厂如何解析 DSN

MercureTransportFactory.php 的create()方法展示了完整的解析逻辑:

public function create(Dsn $dsn): MercureTransport { if ('mercure' !== $dsn->getScheme()) { throw new UnsupportedSchemeException($dsn, 'mercure', $this->getSupportedSchemes()); } $hubId = $dsn->getHost(); $topic = $dsn->getOption('topic'); try { $hub = $this->registry->getHub($hubId); } catch (InvalidArgumentException) { throw new IncompleteDsnException(\sprintf('Hub "%s" not found. Did you mean one of: "%s"?', $hubId, implode('", "', array_keys($this->registry->all())))); } return new MercureTransport($hub, $hubId, $topic, $this->client, $this->dispatcher); }

这里有几个关键点:

  1. HUB_ID取自 DSN 的 host 部分,而不是当作 URL 主机来连接网络——这就是为什么 MercureTransportTest.php 中testCanSetCustomPort、testCanSetCustomHost等常规 HTTP DSN 测试全部被标记跳过("Mercure transport doesn't use a regular HTTP Dsn")。
  2. Hub 实例来自HubRegistry:工厂构造时注入HubRegistry,getHub($hubId)按名称取出配置好的 Hub(Hub 的实际发布 URL、JWT 令牌等由 Symfony Mercure 组件配置)。
  3. Hub 不存在时抛出IncompleteDsnException,并附带提示当前可用的 Hub 名称列表,便于排查拼写错误。该行为在 MercureTransportFactoryTest.php 中有对应测试。
  4. topic选项可直接从 DSN 读取,会原样传给 Transport 作为默认 topic。

2.3 DSN 的字符串化与编码规则

MercureTransport::__toString()(MercureTransport.php)会把 Transport 还原为 DSN 字符串,多 topic 通过http_build_query编码:

return \sprintf('mercure://%s%s', $this->hubId, '?'.http_build_query(['topic' => $this->topics], '', '&'));

测试 MercureTransportTest.php 验证了三种形态:

构造参数序列化结果
不传 topicsmercure://hubId?topic=https%3A%2F%2Fsymfony.com%2Fnotifier
/topicmercure://customHubId?topic=%2Ftopic
['/topic/1', ['/topic/2']]mercure://customHubId?topic%5B0%5D=%2Ftopic%2F1&topic%5B1%5D%5B0%5D=%2Ftopic%2F2

可见 topic 值会被 URL 编码(如/编码为%2F),多 topic 以数组下标形式出现。从源码看,topics既支持字符串也支持字符串数组(甚至嵌套数组),构造时统一通过(array)强转。

三、MercureOptions:为 ChatMessage 添加发布选项

README 的核心实操部分是"Adding Options to a Chat Message"。MercureOptions实现了 Notifier 的MessageOptionsInterface,通过$chatMessage->options($options)挂载到消息上。

3.1 构造函数与全部参数

MercureOptions.php 的构造签名如下:

public function __construct( string|array|null $topics = null, private bool $private = false, private ?string $id = null, private ?string $type = null, private ?int $retry = null, private ?array $content = null, )
参数类型默认值说明
$topicsstring\|array\|nullnull发布目标 topic。为null时回退到 Transport 的默认 topic(即 DSN 中配置的 topic,若 DSN 也未配置则为https://symfony.com/notifier)
$privateboolfalse是否为私有更新。true时 Mercure Hub 仅向通过授权 cookie 验证的订阅者推送
$id?stringnull更新的 ID,用于去重与幂等
$type?stringnull更新的类型,可配合id在订阅端做去重
$retry?intnull订阅端连接断开后的重连间隔(秒)
$content?arraynullWeb Notification 标准的通知内容(7.3 新增),详见下文 3.2

注意:DSN 中配置的 topic 只是 Transport 级别的默认值;每条消息可以通过MercureOptions的$topics覆盖它。doSend()中采用$options->getTopics() ?? $this->topics的优先级(MercureTransport.php),测试testSendWithMercureOptionsButWithoutOptionTopic也验证了 options 未指定 topic 时回退到默认https://symfony.com/notifier(MercureTransportTest.php)。

3.2 content 数组支持的通知字段

$content参数是一个关联数组,注释中列出的合法键(MercureOptions.php)如下:

键类型说明
badgestring通知图标徽章 URL
bodystring通知正文
datamixed与通知关联的任意数据
dir'auto'\|'ltr'\|'rtl'文本方向
iconstring通知图标 URL
imagestring通知展示大图 URL
langstring通知语言标签
renotifybool新通知到达时是否重复提示
requireInteractionbool通知是否需要用户交互才消失
silentbool是否静默(不发出声音/振动)
tagstring通知分组标签
timestampint通知创建时间戳
vibrateint\|list<int>振动模式(单个数值或振动时长序列)

这些字段遵循 Web Notifications API 规范,订阅端浏览器可根据它们渲染原生通知。

3.3 完整发送示例(继承 README 原文)

use Symfony\Component\Notifier\Message\ChatMessage; use Symfony\Component\Notifier\Bridge\Mercure\MercureOptions; $chatMessage = new ChatMessage('Contribute To Symfony'); $options = new MercureOptions( ['/topic/1', '/topic/2'], true, 'id', 'type', 1, ['tag' => '1234', 'body' => 'TEST'] ); // Add the custom options to the chat message and send the message $chatMessage->options($options); $chatter->send($chatMessage);

对照 MercureOptionsTest.php,该示例实际生成的选项数组为:

[ 'topics' => ['/topic/1', '/topic/2'], 'private' => true, 'id' => 'id', 'type' => 'type', 'retry' => 1, 'content' => ['tag' => '1234', 'body' => 'TEST'], ]

其中$chatMessage->getSubject()(即'Contribute To Symfony')将作为事件的摘要(summary)发送;$chatter即 Notifier 的 Chatter 服务。

四、发送链路与底层实现:ChatMessage 如何变成 Mercure Update

4.1 支持的消息类型

MercureTransport.php 的supports()明确了适用范围:

public function supports(MessageInterface $message): bool { return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof MercureOptions); }

即:该 Transport 只处理ChatMessage。测试 MercureTransportTest.php 中,SmsMessage与DummyMessage均被列为不支持的负载;同时若消息携带了非MercureOptions的选项对象,doSend()会抛出UnsupportedOptionsException(对应测试testSendWithNonMercureOptionsThrows)。

4.2 doSend() 的完整转换逻辑

核心发送逻辑在 MercureTransport.php:

protected function doSend(MessageInterface $message): SentMessage { if (!$message instanceof ChatMessage) { throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message); } if (($options = $message->getOptions()) && !$options instanceof MercureOptions) { throw new UnsupportedOptionsException(__CLASS__, MercureOptions::class, $options); } $options ??= new MercureOptions($this->topics); // @see https://www.w3.org/TR/activitystreams-core/#jsonld $update = new Update($options->getTopics() ?? $this->topics, json_encode([ '@context' => 'https://www.w3.org/ns/activitystreams', 'type' => 'Announce', 'summary' => $message->getSubject(), 'mediaType' => 'application/json', 'content' => $options->getContent(), ]), $options->isPrivate(), $options->getId(), $options->getType(), $options->getRetry()); try { $messageId = $this->hub->publish($update); $sentMessage = new SentMessage($message, (string) $this); $sentMessage->setMessageId($messageId); return $sentMessage; } catch (MercureRuntimeException|InvalidArgumentException $e) { throw new RuntimeException('Unable to post the Mercure message: '.$e->getMessage(), $e->getCode(), $e); } }

关键细节:

  1. 无选项时的兜底:$options ??= new MercureOptions($this->topics),即未设置选项时,以 Transport 默认 topic 构造一个空选项对象。
  2. ActivityStreams JSON-LD 封装:消息体被编码为 JSON-LD 格式的Announce活动,包含@context、type、summary(消息主题)、mediaType(application/json)与content(选项中的通知内容)。测试 testSendWithMercureOptions 精确断言了生成的 JSON 串:
{"@context":"https:\/\/www.w3.org\/ns\/activitystreams","type":"Announce","summary":"subject","mediaType":"application\/json","content":{"tag":"1234","body":"TEST"}}
  1. MercureUpdate构造顺序:new Update(topics, data, private, id, type, retry)与MercureOptions的 6 个构造参数一一对应。
  2. 发布结果:HubInterface::publish()返回的消息 ID 被写入SentMessage::setMessageId(),调用方可据此跟踪消息(测试testSendSuccessfully验证了urn:uuid:...形式的 ID 被正确回传)。
  3. 异常包装:Mercure 层的RuntimeException与InvalidArgumentException(如 JWT 无效)会被统一包装为 Notifier 的RuntimeException,错误信息前缀固定为Unable to post the Mercure message:。测试testSendWithTransportFailureThrows与testSendWithWrongTokenThrows分别覆盖了连接失败与令牌非法两种场景。

五、异常处理与测试保障

该桥接的测试集中在 Tests 目录,可直接用于理解各环节的行为边界:

  • MercureTransportFactoryTest.php:覆盖 DSN scheme 校验(非mercure://抛UnsupportedSchemeException)、单/多 topic 解析、默认 topic 兜底,以及 Hub 不存在时的IncompleteDsnException提示语。
  • MercureTransportTest.php:覆盖消息类型支持、选项类型校验、JSON-LD 数据格式、发布失败与 JWT 非法异常包装、成功发布后的消息 ID 回传。
  • MercureOptionsTest.php:验证选项默认值、参数映射与错误 topic 类型(传入stdClass会抛TypeError)。
  • Fixtures/DummyHub.php:提供HubInterface的最小实现,供测试替换真实 Hub。

六、安装与接入建议

在项目中使用该桥接,标准方式是:

composer require symfony/mercure-notifier

随后配置.env中的MERCURE_DSN(如mercure://default?topic=/notifications),并确保 Symfony 的 Mercure 组件中已注册对应的 Hub(default)。需要注意的兼容性前提(以 composer.json 为准):

  • PHP>= 8.4.1;
  • symfony/notifier^7.4|^8.0;
  • symfony/mercure^0.5.2|^0.6|^0.7|^0.8。

实战建议:

  1. 按业务划分 Hub 与 topic:把不同业务线的实时事件拆到不同 topic(如/orders、/chat/room-1),订阅端只监听自己关心的频道;需要全量广播时可使用topic[]多值形式。
  2. 区分默认 topic 与消息级 topic:DSN 中的topic是兜底默认值;单条消息需要发往其他 topic 时,用MercureOptions覆盖,避免为每个 topic 配置独立 DSN。
  3. 敏感数据开启private:$private = true时只有通过授权验证的订阅者能收到更新,适合推送个人化、私有化通知。
  4. 利用id+type去重:为重要事件指定稳定的id与type,可帮助订阅端幂等处理重复投递。
  5. 善用content渲染原生通知:在订阅端结合 Web Notifications API,将body、tag、icon、silent等字段映射为浏览器通知,形成完整的实时提醒体验。

总结

Mercure Notifier 桥接把"连接 Mercure Hub、构建 ActivityStreams 事件、发布更新"这一整套流程封装进 Notifier 的 Transport 抽象中:开发者只需配置一行MERCURE_DSN,再以ChatMessage+MercureOptions组织消息,即可向一个或多个 topic 发布带 Web 通知语义的实时事件,同时获得统一的错误包装与消息 ID 回传能力。其 DSN 解析(HUB_ID取自 host、topic作为默认值)、选项优先级(消息级 topic 覆盖 Transport 默认 topic)以及 JSON-LD 数据格式,均由 MercureTransportFactory.php、MercureTransport.php 与配套测试逐一印证,可作为实现与排错的可靠参考。

  • 后端
  • Web框架

【免费下载链接】symfony

The Symfony PHP framework

项目地址:https://gitcode.com/GitHub_Trending/sy/symfony
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询