context::set() 存的变量在协程切换后取不到,是因为其依赖 swoole 协程 id 映射的本地存储,跨协程不自动继承;需确保 set/get 在同一协程内,子协程须显式调用 context::copy()。

为什么 Context::set() 存的变量在协程切换后取不到
Hyperf 默认使用 Swoole 协程,而 PHP 原生的全局变量、静态属性、$_SERVER 等在协程间是共享的,不是隔离的。如果你直接用普通变量或静态类存请求状态(比如用户 ID、trace_id),协程一调度,就可能被其他请求覆盖。Hyperf 的 Context 类本质是协程本地存储(Coroutine Local Storage),但必须确保:调用 Context::set() 和 Context::get() 发生在同一个协程内,且没有跨出当前协程生命周期。
常见错误现象:Context::get('uid') 返回 null,即使前面刚 set 过;中间件里设了值,到控制器里却取不到。
- 没在协程入口(如
HttpServer、WebSocketServer启动的协程)中初始化上下文,而是从非协程环境(如命令行脚本、测试用例)直接调用Context::set() - 在异步任务(如
go(function () { ... }))里读取父协程的Context—— 子协程默认不继承父协程的Context,需手动Context::copy() - 用了
defer或after注册回调,但回调执行时原协程已结束,Context已销毁
如何正确在中间件中设置并透传 Context 变量
Hyperf 的 HTTP 中间件天然运行在单个协程内,是最适合注入请求级上下文的地方。关键是把解析出的状态(如 token 解析后的用户信息)及时写入 Context,并在后续所有同协程调用中复用。
示例:在鉴权中间件中设置用户 ID
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$token = $request->getHeaderLine('Authorization');
$uid = $this->parseToken($token); // 假设这个方法返回 int
// ✅ 正确:写入当前协程的 Context
Context::set('uid', $uid);
Context::set('authed_at', time());
return $handler->handle($request);
}
注意:不要在中间件里用 static $uid 或 self::$uid,那会跨协程污染;也不要依赖 Container 里的单例对象来存请求数据——它生命周期是容器级的,不是协程级的。
子协程如何继承父协程的 Context
当你在控制器或服务中启动新协程(比如并发查多个接口),子协程默认没有父协程的 Context 数据。直接调用 Context::get('uid') 会返回 null。
- 必须显式调用
Context::copy()把当前协程的全部上下文复制到新协程中 - 推荐写法:在
go()回调最开始就执行Context::copy() - 如果只复制部分键,可用
Context::set($key, Context::get($key))手动搬运,但容易漏
示例:
go(function () {
// ✅ 必须第一行复制上下文
Context::copy();
$uid = Context::get('uid'); // 现在能取到了
$this->doAsyncJob($uid);
});
Context 与 DI 容器、属性注入混用时的陷阱
有人试图在 Controller 构造函数里通过 $container->get(Context::class) 拿 Context 实例,再 set/get —— 这不可靠。因为 Context 类本身不是按协程隔离设计的“实例”,它的静态方法背后依赖 Swoole 的 Coroutine::getUid() 查找当前协程槽位。直接 new 或从容器取实例,会绕过这套机制。
- 永远只用
Context::set()/Context::get()/Context::has()静态方法 - 不要给
Context绑定到 DI 容器的单例或瞬态生命周期 - 避免在
@Inject属性上声明Context类型,Hyperf 不会自动为你做协程上下文绑定 - 若需封装,可写一个
RequestContext工具类,内部仍调用Context::xxx()静态方法
协程上下文不是魔法,它只是 Swoole 提供的一块内存映射表,键是协程 ID,值是你存的数据。一旦协程结束,这块表就清空——所以别指望它跨请求、跨生命周期存活。











