Context::set 和 Context::get 是协程级数据隔离的唯一安全方式,所有请求级状态必须通过二者存取,避免静态变量等导致的数据串扰,且仅限单请求生命周期内有效。

Context::set 和 Context::get 是唯一安全的协程级存取方式
Hyperf 的 Context 不是“可选工具”,而是协程数据隔离的强制基础设施。所有请求级状态(如用户信息、trace_id、权限上下文)必须通过 Context::set() 写入、Context::get() 读取,任何用类属性、静态变量、全局数组存储的行为都会在并发下串数据。
常见错误现象:$this->userId = 123 后,另一个协程调用同一 Service 实例的 getUser() 方法,拿到的却是 123 而不是它自己的 ID;static $cache = [] 导致缓存污染,A 用户查到 B 用户的订单。
-
Context::set()写入的数据仅对当前协程可见,协程结束自动释放(无需手动清理) -
Context::get()第二个参数可设默认值,避免null判空:Context::get('user_id', 0) - 键名建议加命名空间前缀,比如
'auth.user'或'request.trace_id',避免不同模块冲突 - 不支持嵌套 set,例如
Context::set('a.b.c', 'val')无效,必须扁平化键名
中间件里存、控制器里取是最典型使用路径
Context 数据不会跨请求自动传递,必须在每个请求生命周期内显式注入。最稳妥的做法是在中间件中完成初始化写入,在后续任意位置读取。
例如在 JWT 验证中间件中:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$token = $request->getAttribute('token');
$user = $this->jwtService->verify($token);
Context::set('auth.user', $user); // ✅ 正确:写入当前协程上下文
return $handler->handle($request);
}
控制器中直接读取:
public function index()
{
$user = Context::get('auth.user'); // ✅ 安全获取,与中间件写入的是同一个协程实例
return $this->response->success($user);
}
- 不要在构造函数里
Context::get()—— 此时请求尚未进入中间件链,数据还没写入 - 不要依赖
$request->withAttribute()替代 Context —— 它只在 PSR-7 层有效,无法穿透到 Service 层或异步任务 - 若需在子协程中访问父协程 Context,必须显式传递:
go(function () { Context::set('auth.user', Context::get('auth.user')); });
Context 存的是引用还是副本?注意对象修改陷阱
Context::set() 存的是 PHP 变量的值,对数组和对象的处理有本质区别:数组会深拷贝(实际是写时复制),对象存的是引用。这意味着——
- 存数组:
Context::set('data', ['id' => 1]);→ 后续修改该数组不影响 Context 中原始值 - 存对象:
Context::set('user', $userObj);→ 若在别处调用$userObj->setName('x'),Context::get('user')拿到的就是已修改的对象 - 若需确保对象不可变,应存
clone $userObj或转为数组:(array) $userObj - 避免存闭包、资源句柄(如
Redis连接)、Swoole 对象(如Co\Http\Client),它们无法被 Context 安全序列化或跨协程复用
Context 不能替代 Session 或 Redis,它的边界很明确
Context 只解决“单次请求内协程间数据共享”问题,生命周期极短:从请求接入到响应发出即销毁。它不是持久化机制,也不跨进程、不跨机器。
- 需要跨请求保持(如登录态)→ 必须用
SessionInterface(Redis/DB 驱动) - 需要跨 Worker 进程广播(如 WebSocket 下线通知)→ 必须走 Redis Pub/Sub 或消息队列
- 需要记录日志关联 trace_id →
Context::get('trace_id')可用,但必须确保中间件已注入且 logger 实例本身不缓存该值 - 最容易被忽略的是:异步任务(
go()/defer())默认不继承父协程 Context,必须手动Context::set()注入关键字段,否则日志断链、权限丢失


















