不能复用$request或$response实例,因为Swoole/RoadRunner常驻内存中其状态未自动重置,复用会导致跨请求数据污染;虚拟线程下它们与协程上下文强绑定,需通过RequestStack获取隔离实例,否则引发竞态。

Symfony 7 的 Request 和 Response 对象不是简单的数据容器,而是与内核生命周期、上下文传播、虚拟线程隔离强绑定的运行时实体。直接操作它们的属性或复用实例,在协程/虚拟线程环境下极易引发状态污染或竞态。
为什么不能在控制器里复用 $request 或 $response 实例
在 Swoole 或 RoadRunner 等常驻内存环境中,Request 和 Response 实例会被复用(池化),但其内部状态(如 attributes、cookies、content)未自动重置。若你在中间件或监听器中手动修改了 $request->attributes->set('user_id', 123),下一个请求可能继承该值。
- 复用行为由
Request::createFromGlobals()的底层实现控制,它会尝试复用已存在的Request实例(见Symfony\Component\HttpFoundation\RequestStack) -
Response的headers和content不会自动清空,多次调用$response->setContent()会覆盖而非追加 - 虚拟线程模式下,跨协程共享同一
Request实例等同于跨线程共享内存 —— 框架不保证线程安全
Request 的真正来源:不只是 $_SERVER
在传统 FPM 下,Request 主要来自 $_SERVER 和 $_GET/$_POST;但在协程模式下,它由运行时注入,且可能被上下文封装器包装。
- Swoole 场景中,
Request实际由Swoole\Http\Request转换而来,原始对象通过Request::create()构建,而非createFromGlobals() - RoadRunner 场景中,
Request来自 GRPC 或 HTTP/2 帧解析结果,server_protocol属性可能是http/2或grpc,影响路由匹配逻辑 -
Request::getContentType()在虚拟线程中可能返回application/json; charset=utf-8,但实际解析由Request::getContent()触发 —— 后者在协程中是异步可挂起的
Response 发送前必须调用 send(),但不能重复调用
Response::send() 不仅输出内容,还会触发 Response::closeOutputBuffers()、设置响应头、并标记为“已发送”。在协程中误调两次会导致 headers already sent 错误,且无法捕获 —— 因为底层 Swoole 已关闭连接。
- 常见错误:在异常处理中手动
$response->send(),而内核已在$kernel->handle()后自动调用了一次 - 正确做法:统一交由内核处理,仅在自定义服务器循环中显式调用(如示例中的
$symfonyResponse->send()) -
Response::prepare()必须在send()前调用,它负责补全Content-Length、修正Content-Type,并在虚拟线程中检查是否已挂起过 I/O
虚拟线程下 Request 和 Response 的上下文隔离关键点
框架不会自动将 Request 绑定到 Fiber 或协程上下文。你必须依赖 RequestStack 或显式传递,否则在异步任务中访问 $request 可能拿到上一个请求的残留数据。
- 不要在
go()协程块中直接使用闭包捕获的$request,应改用RequestStack::getCurrentRequest()(它基于 Fiber-local storage 实现) -
Response实例不能跨协程传递 —— 它持有 Swoole$response对象引用,该引用只在当前协程有效 - 自定义服务中若需访问请求上下文,必须声明为
prototype作用域,并通过构造器注入RequestStack,而非Request
最易被忽略的是:即使你没写异步代码,只要启用了 Swoole 协程或 RoadRunner,整个请求生命周期就运行在用户态调度器下。Request 和 Response 的生命周期不再由 PHP 进程边界界定,而由协程栈帧界定 —— 这意味着任何静态缓存、全局变量、或未重置的属性都可能泄漏到下一个请求。


















