Symfony Pushy Notifier Bridge 完整指南:DSN 配置、ssl 选项与推送消息定制
Symfony Pushy Notifier Bridge 完整指南:DSN 配置、ssl 选项与推送消息定制
导读
本文基于 Pushy Bridge 的 CHANGELOG 展开,系统讲解 Symfony Notifier 组件中 Pushy 推送服务的集成方式。你将掌握 pushy:// DSN 的完整配置语法、8.2 新增的 ssl DSN 选项(用于强制走普通 HTTP 而非 HTTPS)、PushyOptions 中从收件人到 iOS 中断级别的全部消息定制能力,以及 PushyTransport 的底层发送流程与测试验证方法。本文所有结论均有本仓库源码与测试用例作为依据。
版本演进:Bridge 与 ssl 选项的由来
CHANGELOG 记录了 Pushy 桥接组件的两次关键变更:
- 7.1:新增 Pushy Notifier Bridge(
Add the bridge),即symfony/pushy-notifier包首次并入 Symfony Notifier 生态; - 8.2:新增
sslDSN 选项,允许通过普通 HTTP 而非 HTTPS 发送请求(Add the ssl DSN option to send requests over plain HTTP)。
这两个条目分别对应桥接组件的诞生与传输层协议的扩展能力,是理解该组件如何工作、如何配置的两条主线。下面结合仓库源码逐一深挖。
一、Pushy Bridge 是什么:基于 DSN 的传输层接入
Pushy 桥接组件为 Symfony Notifier 提供了 Pushy 推送服务的集成。它不是一个独立的应用,而是 Notifier 组件体系中的一个"传输层"(Transport),职责是把 PushMessage 封装成 Pushy 的 HTTP API 请求并发送出去。
DSN 配置
Pushy Bridge 的 README 给出了最小 DSN 示例:
PUSHY_DSN=pushy://API_KEY@default
其中 API_KEY 是你在 Pushy 控制台(Dashboard)中可见的应用 API Key。该 DSN 通常写入 .env 文件,随后由 Notifier 组件自动装配到 PushyTransport。
从 PushyTransportFactory 的源码可以看出 DSN 各段的解析规则:
final class PushyTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
if ('pushy' !== $dsn->getScheme()) {
throw new UnsupportedSchemeException($dsn, 'pushy', $this->getSupportedSchemes());
}
$apiKey = $dsn->getUser();
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
$port = $dsn->getPort();
return (new PushyTransport($apiKey, $this->client, $this->dispatcher))
->setHost($host)
->setPort($port)
->setSsl($this->getSsl($dsn));
}
protected function getSupportedSchemes(): array
{
return ['pushy'];
}
}
各段含义归纳如下:
| DSN 段 | 示例值 | 解析结果 | 说明 |
|---|---|---|---|
| scheme | pushy |
传输层标识 | 不匹配 pushy 时抛出 UnsupportedSchemeException |
| user | API_KEY |
传输层 API Key | 作为 api_key 查询参数拼入请求 URL |
| host | default |
null |
使用 PushyTransport 默认主机 api.pushy.me;也可显式指定自定义主机 |
| port | 可选 | 端口号 | 显式指定端口时追加到主机后 |
| option | ssl |
布尔值 | 8.2 新增,控制使用 HTTPS 还是 HTTP(见下节) |
默认端点与传输层标识
PushyTransport 中定义了默认主机与字符串化形式:
final class PushyTransport extends AbstractTransport
{
protected const HOST = 'api.pushy.me';
public function __toString(): string
{
return \sprintf('pushy://%s', $this->getEndpoint());
}
}
当 DSN 中 host 为 default 时,工厂传入 null,getEndpoint() 会回落到 api.pushy.me,因此 __toString() 输出形如 pushy://api.pushy.me。这一点也在 PushyTransportFactoryTest 与 PushyTransportTest 中被验证。
二、8.2 核心变更:ssl DSN 选项详解
CHANGELOG 8.2 条目是本仓库最值得展开的变更:ssl DSN 选项让开发者可以决定传输层使用 HTTPS 还是普通 HTTP。
底层实现
该选项的解析位于 AbstractTransportFactory::getSsl():
protected function getSsl(Dsn $dsn): ?bool
{
return null === $dsn->getOption('ssl') ? null : $dsn->getBooleanOption('ssl');
}
即 DSN 中未声明 ssl 时返回 null(交给传输层默认值),声明时按布尔值解析。
而协议的选择发生在 AbstractTransport::getHttpScheme():
protected function getHttpScheme(): string
{
return ($this->ssl ?? static::SSL) ? 'https' : 'http';
}
PushyTransport 自身没有覆盖 SSL 常量,因此默认走 HTTPS。当 DSN 显式给出 ssl=false 时,请求协议切换为 HTTP。
配置示例
# 默认:HTTPS 加密发送(8.2 之前唯一可选)
PUSHY_DSN=pushy://API_KEY@default
# 显式开启 HTTPS(等价于默认行为)
PUSHY_DSN=pushy://API_KEY@default?ssl=1
# 关闭 TLS,改用普通 HTTP 明文发送
PUSHY_DSN=pushy://API_KEY@default?ssl=0
使用场景与安全边界
- 该选项主要面向自建/代理 Pushy 网关、本地联调、内网测试环境等不希望走 TLS 加密链路的场景;
- 在 PushyTransport::doSend() 中,端点按
%s://%s?api_key=%s拼接,getHttpScheme()的返回值直接决定了请求协议; - 需要明确的是:关闭 ssl 意味着 API Key 与推送内容以明文在网络中传输,生产环境务必保持 HTTPS(即不要设置
ssl=0)。这一安全前提由源码结构直接决定。
三、PushyOptions:从收件人到中断级别的消息定制
除了 CHANGELOG 提到的 DSN 选项,桥接组件的实用价值更多体现在 PushyOptions 上。它实现了 MessageOptionsInterface,以链式调用方式构建 Pushy 请求体。
完整选项一览
| 方法 | 对应 JSON 字段 | 说明与约束 |
|---|---|---|
to(string|array) |
to |
目标设备 ID 或设备 ID 数组,必填,见发送流程一节 |
contentAvailable(bool) |
content_available |
iOS 静默推送标记 |
mutableContent(bool) |
mutable_content |
iOS 可变内容标记(配合通知服务扩展) |
ttl(int) |
time_to_live |
消息有效期(秒),上限 365 天,超出抛 InvalidArgumentException |
schedule(int) |
schedule |
定时发送的 Unix 时间戳,必须是合法时间戳且不得晚于一年后 |
collapseKey(string) |
collapse_key |
折叠键,最长 32 字符,用于合并待机设备上的多条消息 |
body(string) |
notification.body |
通知正文 |
badge(int) |
notification.badge |
iOS 角标数字 |
threadId(int) |
notification.thread_id |
iOS 线程 ID,用于通知分组 |
interruptionLevel(InterruptionLevel) |
notification.interruption_level |
iOS 中断级别 |
参数校验的源码依据
- TTL 上限:
PushyOptions::ttl()中$seconds > 86400 * 365时抛异常(PushyOptions.php#L99-L108); - 调度时间:先用
\DateTime::createFromFormat('U', $seconds)校验 Unix 时间戳合法性,再校验不得早于一年后(PushyOptions.php#L115-L128); - 折叠键长度:
strlen($collapseKey) > 32时抛异常(PushyOptions.php#L135-L144)。
这些校验均有对应测试用例覆盖,见 PushyOptionsTest。
中断级别枚举
中断级别使用原生字符串枚举 InterruptionLevel:
enum InterruptionLevel: string
{
case ACTIVE = 'active';
case CRITICAL = 'critical';
case PASSIVE = 'passive';
case TIME_SENSITIVE = 'time-sensitive';
}
这些值对应 iOS 的 interruptionLevel 语义:passive(被动提醒)、active(默认活跃提醒)、time-sensitive(即时敏感提醒)、critical(紧急提醒,需苹果特殊授权)。
从 Notification 自动映射
PushyOptions::fromNotification() 会把 Symfony Notification 的紧急程度映射为中断级别(PushyOptions.php#L31-L44):
| Notification 紧急程度 | 映射到的中断级别 |
|---|---|
IMPORTANCE_URGENT |
CRITICAL |
IMPORTANCE_HIGH |
TIME_SENSITIVE |
IMPORTANCE_MEDIUM |
ACTIVE |
IMPORTANCE_LOW |
PASSIVE |
这使开发者无需手动维护级别映射,直接复用 Notifier 的语义化紧急程度即可。
四、发送流程:PushyTransport 底层调用链
PushyTransport::doSend() 完整展示了消息的发送过程:
protected function doSend(MessageInterface $message): SentMessage
{
if (!$message instanceof PushMessage) {
throw new UnsupportedMessageTypeException(__CLASS__, PushMessage::class, $message);
}
$options = $message->getOptions()?->toArray() ?? [];
$options['data'] = $message->getContent();
$options['notification']['title'] = $message->getSubject();
$options['to'] ??= $message->getRecipientId();
if (!$options['to']) {
throw new InvalidArgumentException(...);
}
$endpoint = \sprintf('%s://%s?api_key=%s', $this->getHttpScheme(), $this->getEndpoint(), $this->apiKey);
$response = $this->client->request('POST', $endpoint, [
'headers' => ['Accept' => 'application/json', 'Content-Type' => 'application/json'],
'json' => array_filter($options),
]);
...
}
关键步骤拆解:
- 类型检查:只接受
PushMessage,ChatMessage等其他消息类型会抛出UnsupportedMessageTypeException(PushyTransportTest 中验证了 ChatMessage 与 DummyMessage 均不被支持); - 请求体组装:
data字段取自消息内容,notification.title取自消息主题,to若未在选项中指定则回退到消息的收件人 ID; - 必填校验:
to为空时直接抛异常,拒绝发送; - 协议与端点拼接:
getHttpScheme()决定https/http(即上文 ssl 选项的生效点),getEndpoint()决定主机与端口,API Key 以查询参数api_key附加; - 发送与响应处理:POST JSON 请求,
array_filter剔除空值;网络层异常包装为TransportException;非 200 状态码同样抛异常;成功响应必须包含id字段,其值写入SentMessage::setMessageId()。
使用示例
use Symfony\Component\Notifier\Bridge\Pushy\PushyOptions;
use Symfony\Component\Notifier\Bridge\Pushy\PushyTransport;
use Symfony\Component\Notifier\Message\PushMessage;
$options = (new PushyOptions())
->to('device-token-xxx') // 目标设备
->badge(1) // iOS 角标
->interruptionLevel(\Symfony\Component\Notifier\Bridge\Pushy\Enum\InterruptionLevel::TIME_SENSITIVE)
->ttl(3600); // 1 小时有效期
$message = new PushMessage('订单已送达', '您的包裹已到达取件点', $options);
$transport = new PushyTransport($_ENV['PUSHY_API_KEY']);
$sent = $transport->send($message);
echo $sent->getMessageId(); // 输出 Pushy 返回的消息 ID
五、测试验证:桥接组件如何自证正确性
该桥接组件自带三个测试文件,构成完整的回归防线:
- PushyTransportFactoryTest:验证
pushy://scheme 支持、pushy://apiKey@api.pushy.me的 DSN 生成,以及非pushyscheme 的拒绝逻辑; - PushyTransportTest:用
MockHttpClient断言实际发出的 JSON 请求体(to、data、notification.title),并验证响应id被回填为SentMessage的消息 ID,同时覆盖支持/不支持的消息类型矩阵; - PushyOptionsTest:验证全部选项的序列化结果,以及 TTL 超 365 天、调度超 1 年、折叠键超 32 字符三种异常场景。
其中 PushyTransportTest::testSendWithOptions 展示了"构造 PushMessage → 发送 → 断言请求体 → 断言消息 ID"的完整闭环,可作为自行扩展该桥接的测试范本。
六、安装与运行前提
从 Pushy Bridge 的 composer.json 可以看到本桥接的依赖与运行前提:
- PHP 版本要求
>= 8.4.1; - 依赖
symfony/http-client(^7.4|^8.0)与symfony/notifier(^8.2); - 包名为
symfony/pushy-notifier,属于symfony-notifier-bridge类型; - 开发环境还需要
symfony/event-dispatcher。
安装后只需在 .env 中配置 PUSHY_DSN(格式见第一节),并在配置文件中启用 Notifier 的 pushy 传输即可使用。整个桥接组件结构紧凑:工厂解析 DSN、传输层负责 HTTP 交互、选项类负责请求体定制,三者职责清晰,易于阅读和扩展。
小结
Pushy Bridge 从 7.1 引入至今,用最简的 DSN 抽象封装了 Pushy 推送 API 的全部核心能力:8.2 的 ssl 选项为传输协议提供了弹性(生产保持 HTTPS 是安全底线),PushyOptions 覆盖了目标设备、TTL、定时、折叠键与 iOS 通知行为等完整参数,而 PushyTransport 的请求组装与响应解析逻辑清晰、可测试。无论你是要接入 Pushy 推送、定制 iOS 中断级别,还是想研究 Symfony Notifier 传输层的实现范式,这个桥接组件都是一份值得精读的样板。