laravel 9+ 应使用 streamDownload 实现流式 CSV 导出,配合 DB::cursor() 或 CursorPaginator、手动设置响应头、写入 bom 头(xEFxBBxBF)、用 fputcsv 处理字段转义,避免 toArray()、chunk()、Storage::put() 等导致 OOM 或乱码的操作。

用 streamDownload 避免内存爆掉
导出大表时直接 toArray() + fputcsv 很容易 OOM。Laravel 9+ 内置的 streamDownload 是唯一靠谱选择,它不把全量数据载入内存,而是边查边写。
常见错误是先 get() 再遍历——哪怕加了 chunk(),只要结果集没流式处理,PHP 进程仍会累积大量对象引用。
- 必须用
CursorPaginator或原生DB::cursor()拉取数据,不能用paginate()或chunkById()(后者仍会缓存 chunk) - 响应头要手动设
Content-Type: text/csv; charset=utf-8和Content-Disposition,否则浏览器可能当 HTML 打开 - CSV 中文乱码?不是编码问题,是没加 BOM 头——在
fputcsv前写入xEFxBBxBF
Storage::disk('local')->put() 不适合导出场景
有人习惯先生成文件再 return response()->download(),这在并发高或磁盘慢的环境会卡住请求,还留下临时文件风险。
真正要导出,就别落地文件。除非你要做异步报表(比如后台任务生成后发邮件),否则所有「导出」动作都该走流式响应。
-
Storage::put()写完还得校验路径、权限、清理,多三步操作,全是故障点 - 如果非要用本地存储,至少用
temporaryUrl()+ 前端跳转,但依然绕不开过期和并发覆盖问题 - 云存储(如 S3)更不行:
put()后再temporaryUrl()有延迟,用户点击时链接可能已失效
字段名含逗号、换行、双引号?fputcsv 自动处理,别自己 str_replace
看到 CSV 打开错列,第一反应常是“我得把逗号替换成顿号”,这是典型误区。fputcsv 本职就是处理这些边界字符:自动加双引号、转义内部双引号、保留换行符。
手动预处理反而破坏格式,比如把换行替成空格,等于丢数据;把双引号替成两个双引号又可能和 fputcsv 的转义冲突。
- 确保传给
fputcsv的是纯数组,不要提前json_encode或implode - 字段值里有双引号?
fputcsv会自动变成"",不用管 - 首行标题也走
fputcsv,别用fwrite单独写,否则编码/换行不一致
excel 不是 CSV,别用 maatwebsite/excel 导小 CSV
为导一个带标题的 500 行 CSV 装 maatwebsite/excel,等于杀鸡用核弹。它默认生成二进制 .xlsx,依赖 PHPZip 和内存缓冲,启动慢、报错多、兼容差。
真要 Excel 格式,再上它;只要 CSV,原生 PHP 函数足够,且更可控。
-
maatwebsite/excel的export()方法底层还是走fputcsv,只是包了一层,徒增配置项(WithHeadings、ShouldAutoSize)和异常分支 - 它不支持流式导出大 CSV,
FromQuery也是先查后导,OOM 风险照旧 - 升级 Laravel 版本时,这个包经常因 Illuminate 接口变动而报
Call to undefined method
复杂点在于:BOM、流式、字段转义这三件事必须同时做对,少一个,用户双击打开就是乱码或错列。没人会去查 CSV 编码设置,他们只看第一眼。