readfile()不能用于下载超大Excel文件,因其将整个文件一次性加载进内存,易触发memory_limit限制或崩溃,且Apache/Nginx可能因响应体过大或超时断连,属架构级阻塞缺陷。

为什么不能用 readfile() 下载超大 Excel 文件
readfile() 会把整个文件一次性加载进内存再输出,对几百 MB 的 Excel 文件来说,PHP 进程很容易触发 memory_limit 限制或直接崩溃。更隐蔽的问题是:即使你调高了内存限制,Apache/Nginx 也可能因响应体过大或超时主动断连,导致下载中断、文件损坏。这不是“调大配置就能解决”的问题,而是架构层面的阻塞模型缺陷。
用 php://output 边生成边下载才是正解
核心思路是绕过文件落地环节,让 Excel 数据从数据库查出后,直接写入 PHP 输出流,浏览器同步接收并组装成文件。这要求:
- 必须用流式 Excel 库(如
box/spout或原生 CSV +fputcsv()),不能用PhpSpreadsheet全量内存模式 -
header()必须在任何输出前设置,且Content-Type要匹配实际格式(CSV 用text/csv,.xlsx 用application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) - 中文字段名/内容需提前转为 UTF-8,并加 BOM 头(
\xEF\xBB\xBF)否则 Excel 打开乱码 - 每次写入后调用
ob_flush()和flush(),否则数据卡在缓冲区不下发
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="data.csv"');
echo "\xEF\xBB\xBF"; // BOM
$fp = fopen('php://output', 'w');
fputcsv($fp, ['用户ID', '姓名', '时间']);
foreach ($rows as $row) {
fputcsv($fp, $row);
ob_flush();
flush();
}
fclose($fp);
HttpFoundation 的 StreamedResponse 怎么用才不踩坑
Laravel 或 Symfony 项目里,别手动 echo + flush,直接用 StreamedResponse 封装逻辑:
- 回调函数内不能有额外输出(比如
var_dump、未捕获的 warning),否则破坏二进制流 - 务必在回调开头设置 headers(
headers->set()),不能在外部 set,否则可能被覆盖 - 数据库查询要用游标分页(
where id > ? limit 1000),避免offset越往后越慢 - 如果用
yield生成器,确保 generator 不抛异常,否则响应中断且无提示
StreamedResponse 当普通 Response 用,漏掉 headers 设置或在回调里调用 dd() —— 这会导致空文件或 HTTP 500。
Excel 格式选 CSV 还是 XLSX?看这三点
不是“越标准越好”,得权衡:
– CSV:兼容性最强、内存占用最低、生成最快,但不支持样式/公式/多 sheet;适合纯数据导出
– XLSX(用 box/spout):支持基本样式和多 sheet,内存比 PhpSpreadsheet 低 90%,但生成速度略慢,且必须用 php://output 流式写入,不能先存临时文件再读取
– 别碰 .xls(BIFF8):老旧格式,PHP 原生无安全可靠实现,易触发 Excel 兼容警告
真正容易被忽略的是:哪怕选了 XLSX,如果中间某次 fwrite() 失败(比如磁盘满、权限不足),StreamedResponse 不会报错,只会返回截断文件——得靠前端校验文件大小或后端写 checksum 日志。


















