Symfony Pushy Notifier Bridge 完整指南:DSN 配置、ssl 选项与推送消息定制

原创2026-10-02 10:37:411,345 阅读
文章标签:后端Web框架

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:新增 ssl DSN 选项,允许通过普通 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 中断级别

参数校验的源码依据

这些校验均有对应测试用例覆盖,见 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),
    ]);
    ...
}

关键步骤拆解:

  1. 类型检查:只接受 PushMessage,ChatMessage 等其他消息类型会抛出 UnsupportedMessageTypeException(PushyTransportTest 中验证了 ChatMessage 与 DummyMessage 均不被支持);
  2. 请求体组装:data 字段取自消息内容,notification.title 取自消息主题,to 若未在选项中指定则回退到消息的收件人 ID;
  3. 必填校验:to 为空时直接抛异常,拒绝发送;
  4. 协议与端点拼接:getHttpScheme() 决定 https/http(即上文 ssl 选项的生效点),getEndpoint() 决定主机与端口,API Key 以查询参数 api_key 附加;
  5. 发送与响应处理: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 生成,以及非 pushy scheme 的拒绝逻辑;
  • 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 传输层的实现范式,这个桥接组件都是一份值得精读的样板。

登录后查看全文
symfony