StreamedResponse 边生成边输出大文件可避免内存溢出,关键在于正确处理缓冲、响应头、流生成和异常边界:需调用 ob_end_clean() 清空缓冲,每次 echo 后 flush(),手动设置 Content-Type 与 Content-Disposition,用 Generator 逐行处理数据,禁止在回调中抛异常或重定向。

直接用 StreamedResponse 传入回调函数,就能边生成边输出大文件,避免内存爆掉。关键不是“怎么写”,而是“怎么不卡住”——多数失败都栽在缓冲、头信息或流中断上。
回调里必须清空输出缓冲并手动 flush
PHP 默认开启输出缓冲(output_buffering),不主动清理会导致数据滞留在缓冲区,客户端收不到任何内容,甚至超时断连。
- 在回调开头调用
ob_end_clean(),确保之前没残留输出 - 每次
echo后紧跟flush(),强制把当前 chunk 推到客户端 - 不要依赖
ob_implicit_flush(true),它受 php.ini 中implicit_flush设置影响,线上环境常被禁用 - 若用
fpassthru()输出文件句柄,也要在之前ob_end_clean(),否则可能截断前几 KB
Content-Type 和 Content-Disposition 必须手动设对
StreamedResponse 不像 BinaryFileResponse 那样自动推断 MIME 类型和文件名,漏设或设错会导致浏览器乱码、无法下载或默认保存为 download.bin。
- CSV 导出:设
Content-Type: text/csv; charset=utf-8,加 BOM 头防 Excel 乱码(\xEF\xBB\xBF) - ZIP 下载:设
Content-Type: application/zip,Content-Disposition: attachment; filename="export.zip" - JSON 流:设
Content-Type: application/json,但注意 JSON 流需保证格式合法(如数组外层用[开头,每条记录后加逗号,最后补]) - 禁止设
Cache-Control: no-cache以外的缓存头,否则某些代理或浏览器会拒绝流式响应
大数据集务必用 Generator 而非全量数组
哪怕只是 yield 一行 CSV,也比 getIterator() 加载全部实体省内存。Doctrine 的 iterate() 或原生 PDO 游标才是安全选择。
- 避免
$em->getRepository(User::class)->findAll()—— 这会把所有对象实例加载进内存 - 改用
$em->getRepository(User::class)->createQueryBuilder(...)->getQuery()->iterate(),配合foreach逐条处理 - Generator 函数里每
yield一行,就echo一行再flush(),内存占用基本恒定 - 若用
maennchen/zipstream-php打包多个文件,直接往php://output写,别先生成临时 ZIP 文件
流式响应不能 throw 异常或 exit
一旦回调中抛出未捕获异常或调用 exit/die,PHP 会终止脚本,TCP 连接直接断开,客户端收到不完整响应(常见于 Chrome 报 “ERR_INCOMPLETE_CHUNKED_ENCODING”)。
- 所有数据库查询、文件读取、序列化逻辑,必须包裹
try/catch,错误时echo可读错误信息并flush(),再让回调自然结束 - 不要在回调里调用
$this->redirectToRoute()或其他重定向逻辑 —— 此时响应头已发,重定向无效且会报错 - 若需鉴权或校验,必须在创建
StreamedResponse之前完成,而不是塞进回调里 - 流式代理第三方接口时,用
$client->stream($response)循环$chunk->getContent(),别在循环里throw网络异常
真正难的不是写出第一个 StreamedResponse,而是让每一行 echo 都稳稳落到客户端手里——缓冲、头信息、生成逻辑、异常边界,四者缺一不可。线上跑着跑着突然卡住,八成是某处 flush() 没跟上,或者 ob_end_clean() 被跳过了。


















