核心分界点在于数据规模与格式需求是否越界:fputcsv是纯文本流写入,零依赖、内存恒定;PhpSpreadsheet是完整表格引擎,支持样式公式但内存消耗大、依赖扩展。

fputcsv 与 PhpSpreadsheet 的核心分界点在哪里
不是“用哪个更高级”,而是“数据规模和格式需求是否越界”。fputcsv 本质是纯文本流写入,零依赖、毫秒级响应、内存恒定在几 KB;PhpSpreadsheet 是完整电子表格引擎,支持公式、样式、多 Sheet,但单次导出万行数据就可能吃掉 100MB+ 内存,且初始化耗时明显。
常见错误现象:fputcsv 导出的数字在 Excel 里变成 1.23E+8;PhpSpreadsheet 在 CLI 模式下报 Class 'ZipArchive' not found(缺扩展)或导出空文件(没设 Content-Type)。
- CSV 场景:后台批量下载日志、用户列表、统计原始数据 —— 用
fputcsv+php://output - Excel 场景:需打印带边框/颜色的财务报表、含公式的销售看板、要发给非技术人员且要求“双击就能看” —— 用
PhpSpreadsheet - 混合场景:同一导出接口,用户勾选“仅数据”就走 CSV,“带格式”就走 Excel —— 这才是策略模式真正该介入的地方
策略接口怎么定义才不僵硬
别一上来就抽象出 ExportStrategy 接口然后塞一堆 export()、validate()、getMimeType()。实际只需两个方法:
interface Exporter
{
public function write(mixed $data, resource $output): void;
public function getHeaders(string $filename): array;
}
原因很直接:write() 封装了所有格式差异(fputcsv 写行 vs $sheet->setCellValue() 定位写单元格),getHeaders() 负责告诉浏览器“这是什么文件、叫什么名、怎么下载”。其他如编码转换、BOM 头、数值转字符串,全在具体实现里做,接口不感知。
立即学习“PHP免费学习笔记(深入)”;
使用 qbo-mileage CLI 及用户凭证,从 Airtable、Outlook 或 Google Calendar 记录生成 QuickBooks Online 里程 CSV 文件。
- CSV 实现里
write()直接调fputcsv($output, $row),getHeaders()返回['Content-Type' => 'text/csv; charset=utf-8'] - Excel 实现里
write()创建Spreadsheet对象并填充,getHeaders()返回['Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] - 关键细节:Excel 实现的
write()必须接受resource $output,内部用$writer->save($output),而非先保存到临时文件再读 —— 否则失去流式优势
为什么不能在控制器里 new 出来就用
硬编码 new CsvExporter() 或 new ExcelExporter() 看似简单,但会立刻卡在三个地方:
- 参数传递混乱:CSV 需要控制分隔符、是否加 BOM;Excel 需要指定字体、是否冻结首行 —— 全塞进构造函数?那策略对象就退化成配置容器
- 依赖无法注入:Excel 实现依赖
PhpSpreadsheet\Writer\Xlsx,但测试时你想 mock 它,硬 new 就绕不开真实 IO - 路由决策耦合:
GET /export?format=csv和GET /export?format=xlsx本该由工厂或解析器决定用哪个策略,而不是让控制器 if-else
正确做法是把格式选择逻辑下沉:用一个 ExporterFactory::make($_GET['format'] ?? 'csv'),它只返回符合 Exporter 接口的对象。工厂内部可查配置、读数据库字段、甚至调用策略规则引擎 —— 控制器只管调 $exporter->write($data, $output)。
最容易被忽略的边界:输出前的缓冲区清理与 exit
无论用哪种策略,最终都走 fopen('php://output', 'w') 或 $writer->save('php://output')。但 PHP 默认开启输出缓冲(output_buffering),如果之前有 echo、var_dump、框架日志、甚至 UTF-8 BOM 前的不可见空格,就会导致 header 已发送、fputcsv 报 Warning: fputcsv(): Cannot use a stream opened in append mode,或者 Excel 文件损坏打不开。
必须在打开 php://output 前强制清空:
if (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: ...');
header('Content-Disposition: ...');
$output = fopen('php://output', 'w');
最后别忘 fclose($output); exit; —— 不是 return,因为响应体已直接写入输出流,后续任何代码(包括框架中间件)都可能追加内容破坏文件结构。


















