graphql订阅在php中需分离http层与长连接服务,因php-fpm无状态特性无法维持websocket连接;必须通过swoole/reactphp等异步运行时配合redis pub/sub或消息队列实现事件分发与实时推送。

GraphQL订阅在PHP里根本不是开箱即用的功能
GraphQL规范本身定义了 subscription 操作类型,但它的执行依赖长连接(WebSocket 或 SSE),而标准 PHP-FPM + Apache/Nginx 架构是无状态、短生命周期的——每次 HTTP 请求结束后进程就回收,没法维持连接。所以直接用 graphql-php 库写个 Subscription 类,不配配套服务,请求一发完就断,订阅永远“没反应”。
必须拆成两层:HTTP API 层 + 独立长连接服务
典型可行方案是:前端通过 WebSocket 连到一个独立运行的服务(比如基于 ReactPHP、Swoole 或 Node.js),这个服务负责接收订阅、维护客户端连接、触发事件通知;PHP 的 GraphQL 层只管解析查询、执行业务逻辑、把变更事件推给那个长连接服务(例如发 Redis Pub/Sub、写消息队列)。两者解耦,各司其职。
常见错误现象:subscription 字段返回 null 或直接报 "Subscription root field must return AsyncIterator" —— 这是因为你试图在普通 PHP 请求里返回 AsyncIterator,但没有启用协程或异步运行时(如 Swoole 的 Coroutine 或 ReactPHP 的 Promise)。
- 如果你用的是 Laravel,别指望
lighthouse-php开启subscription就自动有实时推送——它默认只生成 resolver,不启动 WebSocket 服务 -
graphql-phpv15+ 提供了SubscriptionServer基类,但它只是抽象接口,仍需你自己实现底层连接管理 - Redis 是最轻量的事件分发选择:
PUBLISH subscription:order_updated {"id":"123"},长连接服务监听subscription:*模式并广播给对应客户端
Swoole 是目前 PHP 生态最落地的订阅支撑方案
相比 ReactPHP(需要大量 Promise 链)、Workerman(文档松散、类型支持弱),Swoole 提供原生 WebSocket\Server 和协程 Channel/Redis\Coroutine,能真正让 PHP 处理千级并发连接。关键点在于:不能把 GraphQL 解析和 WebSocket 推送混在同一协程上下文里做阻塞操作。
实操建议:
- WebSocket 连接建立后,为每个 client 分配唯一
$clientId,并将其订阅的主题(如user:789)存入 Redis 的SET或HASH - 业务代码中触发更新时,不要调用 GraphQL resolver,而是直接
$redis->publish("topic:user:789", json_encode([...])) - WebSocket 服务端用
go(function () { $redis->subscribe([...], $callback); });监听,收到消息后遍历在线 client 并$server->push($fd, $payload) - 注意:Swoole 的
onMessage回调里不能直接调用sleep()或file_get_contents(),否则阻塞整个协程调度
前端订阅语句和 PHP resolver 的对接容易错位
前端发送的 subscription 请求体里带的是字段名(如 orderUpdated),PHP resolver 对应的函数名也得叫 orderUpdated,但它**不能返回数据**,只能返回一个“可被监听的信号源”。常见写法是返回一个闭包或 Observable 实例,但前提是你的运行时支持。
示例陷阱:
// ❌ 错误:在普通 PHP-FPM 下这么写,$iterator 永远不会被消费
return function () use ($topic) {
yield $this->redis->subscribe([$topic]);
};
正确做法(Swoole 协程下):
use Swoole\Coroutine\Channel;
return function () use ($topic) {
$channel = new Channel(100);
go(function () use ($channel, $topic) {
$redis = new \Swoole\Coroutine\Redis();
$redis->connect('127.0.0.1', 6379);
while (true) {
$msg = $redis->subscribe([$topic]);
if ($msg && $msg[2]) {
$channel->push($msg[2]);
}
}
});
return $channel;
};
这里的关键是:resolver 不做实际推送,只提供一个可被 GraphQL 执行层持续 next() 的 Channel;真正的消息流入由另一个协程保障。
最容易被忽略的一点:GraphQL 订阅的 payload 必须严格匹配 schema 中定义的类型,连字段顺序错位都可能导致客户端解析失败;而 PHP 数组默认不保序,用 ArrayObject 或显式 json_encode($arr, JSON_FORCE_OBJECT) 更稳妥。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











