首页 / 资讯中心 / 文章详情

PHP生成Excel:基于PhpSpreadsheet封装库实现xlsx导出详解

PHP生成Excel:基于PhpSpreadsheet封装库实现xlsx导出详解 ★ FEATURED ARTICLE
简介这是一份面向PHP初学者的Excel导出示例源码包演示了通过PHPExcel或PhpSpreadsheet类库在服务端生成Excel文件的基础流程。资源解压后共2个文件包括一个demo.php主程序和一个jpg图片文件整个rar压缩包约30KB。demo.php中包含从实例化工作簿、写入单元格数据、设置基本样式到保存文件的完整步骤jpg截图则用于直观展示导出效果或运行结果。这套小例子适合刚接触PHPExcel、需要快速实现数据导出的开发者不用从零摸索直接对照代码修改即可嵌入实际项目。目前已有155人学习下载在日常报表、订单导出等场景中具有直接的参考价值。1. 项目定位PHP 生成 Excel不只是拼一个 CSV 文件手头有订单数据要导给财务、有月报要做成 xlsx 发群、有后台列表要加一个「导出」按钮这些需求落到 PHP 后端第一反应往往是拼个 CSV 或 HTML table 再改后缀——快速但坑多公式不生效、中文乱码、列宽和样式全丢、数据量稍大直接被 Excel 提示文件损坏。这个「PHP 生成 excel 文件」源码包解决的就是这个差距用纯 PHP 直接生成真正意义上的 .xlsx 文件支持多工作表、合并单元格、富样式、公式、图表占位不依赖服务器安装 Office 组件只要 PHP 环境有 zip 和 gd 扩展就能跑。适合做后台管理系统的开发者尤其适合电商订单导出、金融对账报表、教务成绩单这类「每周都要导一次、格式还总被提需求改」的项目场景。2. 技术选型与目录结构为什么是 PhpSpreadsheet而不是拼字符串2.1 技术选型对比从 PHPExcel 到 PhpSpreadsheet再到自研轻量版早期 PHP 生成 Excel 的公认方案是 PHPExcel但官方早在停更维护后就不再适配新版本 PHP 的命名空间和语法项目普遍反映在 PHP 7.4 以上跑 PHPExcel 会频繁报 Deprecated 错误甚至直接白屏。PhpSpreadsheet 是它的官方继承者底层逻辑沿用「单元格对象 工作簿对象」的思路性能优化了不少内存占用控制也更合理。这份源码包的做法是把 PhpSpreadsheet 的常用能力抽出来做了一层封装上层提供export方法开发者只需要传一个二维数组和表头配置内部自动完成工作簿创建、样式写入、列宽计算、文件流输出。这样做的直接收益是业务代码里不需要出现任何setCellValue的重复调用改表头、加列、调整样式都集中在一个配置数组里完成。2.2 目录结构拿到源码包先看这四个地方项目解压后目录并不复杂核心结构如下php-excel-export/ ├── src/ │ ├── ExcelExporter.php # 核心封装类所有导出入口 │ ├── StyleBuilder.php # 样式构建器链式调用生成样式数组 │ └── DataFormatter.php # 数据格式化处理时间、数字、长文本 ├── lib/ │ ├── PhpSpreadsheet/ # 第三方库源码已内置无需 composer install │ └── autoload.php # 自动加载文件 ├── examples/ │ ├── demo_simple.php # 最简单示例数组直接导出 │ ├── demo_orders.php # 模拟订单导出带汇总行 │ └── demo_multi_sheet.php # 多工作表导出示例 ├── config/ │ └── excel_config.php # 全局默认配置字体、行高、列宽策略 └── tests/ └── run_tests.php # 冒烟测试脚本src/ExcelExporter.php是入口文件实例化之后链式调用setData、setHeader、setFileName最后调download即可触发浏览器下载src/StyleBuilder.php负责把「表头加粗、底色、边框、自动换行」这类高频需求封装成一行代码src/DataFormatter.php解决的是数据层问题——比如把时间戳1735689600转成2025-01-01 08:00:00把超长文本字符串截断并保留完整内容到批注里。lib/PhpSpreadsheet目录是已经内置的第三方源码不需要执行composer install这一点对没有 composer 环境的虚拟主机用户很友好。但注意 PHP 版本要求是 7.2 以上zip 扩展必须开启否则读取和写入 xlsx 时会在文件流阶段直接报致命错误。2.3 全局配置先在 config 里定调再谈具体导出config/excel_config.php是第一个建议打开的文件。它定义了一套全局默认值?php return [ default_font 微软雅黑, header_font_size 11, header_bold true, header_bg_color FFE8E8E8, body_font_size 10, row_height 22, column_width_mode auto, // auto / fixed / custom auto_width_min 12, auto_width_max 40, has_filter true, // 表头自动筛选 has_border true, // 单元格边框 freeze_pane A2, // 首行冻结 ];这段配置的实际作用是导出前全局统一底色、字体、大小业务代码里如果对某一行有特殊要求再单独覆盖。column_width_mode是auto时内部会遍历所有行数据计算每列最长字符串长度结合中文字符宽度修正系数去设置列宽——这个细节很重要直接设固定列宽会导致「订单号列太窄出现 ####」或「收货地址列太宽浪费纸张」。3. 核心模块拆解一个导出请求从进入到下载完成的完整链路3.1 导出入口封装对外一个方法对内三步走ExcelExporter类的核心方法完整走一遍能看清整个封装的边界在哪里。下面是最简化版本的入口方法?php namespace Export; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; class ExcelExporter { private $data []; private $header []; private $fileName export.xlsx; private $sheetName Sheet1; private $styleConfig []; public function setData(array $data) { $this-data $data; return $this; } public function setHeader(array $header) { $this-header $header; return $this; } public function setFileName(string $fileName) { $this-fileName $fileName; return $this; } public function download() { $spreadsheet new Spreadsheet(); $sheet $spreadsheet-getActiveSheet(); $sheet-setTitle($this-sheetName); $this-writeHeader($sheet); $this-writeBody($sheet); $writer new Xlsx($spreadsheet); header(Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); header(Content-Disposition: attachment;filename . $this-fileName . ); header(Cache-Control: max-age0); $writer-save(php://output); exit; } }setData、setHeader、setFileName都返回$this支持链式调用download内部按「创建工作簿 → 写表头 → 写数据体 → 写文件流」四步执行。注意header()之后必须exit否则框架后续输出日志或调试信息会污染文件流下载下来的 xlsx 打开就会报文件损坏。参数说明setHeader接收的是二维数组每个子数组的键包含field对应数据数组的键、label表头显示名、width列宽、align对齐方式。setData接收的是一维关联数组的列表键名必须与header.field对应缺少的键会被勾勒为空字符串。3.2 表头样式与冻结窗格解决「表头看不清」的体验问题表头区域统一做加粗、底色填充、居中并开启自动筛选和冻结首行这部分由StyleBuilder完成?php namespace Export; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Alignment; class StyleBuilder { private $config; public function __construct(array $config) { $this-config $config; } public function headerStyle(): array { return [ font [ bold $this-config[header_bold], size $this-config[header_font_size], name $this-config[default_font], ], fill [ fillType Fill::FILL_SOLID, color [argb $this-config[header_bg_color]], ], alignment [ horizontal Alignment::HORIZONTAL_CENTER, vertical Alignment::VERTICAL_CENTER, ], borders [ allBorders [ borderStyle \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN, ], ], ]; } }这里的关键点是fill.color.argb传的是带FF前缀的完整 ARGB 色值常见错误是把网页里的#E8E8E8直接传进去PhpSpreadsheet 不识别#前缀会直接忽略背景色。borders配置的allBorders作用于该 style 被 set 到的整块区域不需要遍历每个 cell 单独设置。设置到工作表上时用$sheet-getStyle(A1:{$lastColumn}1)-applyFromArray($styleBuilder-headerStyle())配合$sheet-freezePane(A2)实现滚动时表头固定。3.3 数据格式化时间戳、科学计数法、长文本三个高频问题一起处理导出的数据从数据库里取出来通常是原始类型直接写入 xlsx 会暴露三个典型问题时间戳显示成一串数字、身份证或订单号变成科学计数法、长文本把行高撑爆。DataFormatter统一在写入前做数据预处理?php namespace Export; class DataFormatter { public static function format($value, string $fieldType) { switch ($fieldType) { case datetime: return date(Y-m-d H:i:s, (int) $value); case date: return date(Y-m-d, (int) $value); case string: // 强制转字符串避免身份证、订单号被识别成数字 return (string) $value; case number: return round((float) $value, 2); case text: // 长文本截断展示完整内容放入批注 $plainText strip_tags((string) $value); return mb_substr($plainText, 0, 50) . (mb_strlen($plainText) 50 ? … : ); default: return $value; } } }fieldType在 header 配置里通过type datetime声明ExcelExporter::writeBody的循环里对每个 cell 先调用DataFormatter::format($value, $type)再setCellValue。订单号这种超过 11 位的纯数字串必须走string类型否则 xlsx 在 Excel 里会显示成4.23521E11用户复制出去还没法直接用。如果不想改变原数据又希望保持数字显示另一个常见思路是setCellValueExplicit配合DataType::TYPE_STRING强制写字符串类型适合需要保留前导零的编号值。3.4 多工作表与自动列宽封装里最容易被忽略的两个性能细节多工作表导出的实现方式是createSheet后逐个setTitle再写数据代码结构上就是循环调用与单表相同的过程。自动列宽则不能每行都调getColumnDimension()-setAutoSize(true)——数据量两三千行时这个操作会严重拖慢生成速度。这份源码包的处理是先收集每列最大字节长度最后统一setWidth这样自动列宽的整体耗时控制在毫秒级。?php // 伪代码演示自动列宽的整体计算逻辑 $maxLengths []; foreach ($this-data as $row) { foreach ($row as $field $value) { $len mb_strlen((string) $value, UTF-8); if (!isset($maxLengths[$field]) || $len $maxLengths[$field]) { $maxLengths[$field] $len; } } } foreach ($this-header as $index $col) { $width $maxLengths[$col[field]] ?? $col[width] ?? 15; $width max($this-config[auto_width_min], min($width 2, $this-config[auto_width_max])); $sheet-getColumnDimensionByColumn($index 1)-setWidth($width); }这里$width 2是给中文和数字留的左右边距余量实测不加这个余量时「订单编号」这类中文字段的列宽刚好贴着字视觉上很不舒服。中文字符宽度不能简单按mb_strlen乘 2但这份代码用的近似方法在实际导出里足够用了——精确计算需要用到 GD 的imagettfbbox去量字体宽度性价比不高。列宽约束在auto_width_min和auto_width_max之间避免出现某一列超长文本把整个表撑出打印范围的情况。4. 运行方式与性能优化从跑通 demo 到对接真实业务数据4.1 环境准备与快速跑通源码包对运行环境的要求不高PHP 7.2 以上必须开启zip和mbstring扩展。zip是 xlsx 文件格式的核心依赖——xlsx 本质是多个 XML 文件的 ZIP 压缩包没有 zip 扩展就无法解压和打包。检查是否开启的方法是在命令行执行php -m | grep zip或者用phpinfo()查看。mbstring负责中文截断和长度计算缺失时mb_substr直接报 undefined function。跑通第一个 demo把项目放到本地 Nginx 或 Apache 的 web 目录下浏览器访问examples/demo_simple.phpphp -S localhost:8080 -t php-excel-export然后浏览器打开http://localhost:8080/examples/demo_simple.php页面会直接触发下载demo_simple.xlsx。如果下下来的是 0 字节文件或者打开报错优先检查 PHP 是否在输出文件流之前打了其他内容——比如demo_simple.php文件头部多了一个空行导致的 BOM 头输出这是最常见的 xlsx 文件损坏原因。4.2 实际业务对接把 MySQL 查询结果直接塞进导出跑通 demo 之后对接真实业务的核心改动在数据源。示例里数组是写死的实际开发场景通常是把PDOStatement::fetchAll(PDO::FETCH_ASSOC)的返回结果直接传给setData。这里有个性能优化的临界点需要掌握当数据行数少于 5000 行时setCellValue逐单元格写入是合理做法当超过 1 万行时逐格写入会明显变慢且内存占用随单元格数线性增长。一个合格的优化方案是「分批获取数据 追加写入」?php // 伪代码展示大数据量导出时的分批策略 $offset 0; $limit 2000; $exporter-setHeader($header); $spreadsheet new Spreadsheet(); $sheet $spreadsheet-getActiveSheet(); $rowIndex 2; while ($rows $db-query(SELECT * FROM orders LIMIT {$offset}, {$limit})) { foreach ($rows as $row) { $colIndex 1; foreach ($header as $col) { $value DataFormatter::format($row[$col[field]], $col[type] ?? string); $sheet-setCellValueByColumnAndRow($colIndex, $rowIndex, $value); } } $offset $limit; }这种方式避免了一次性把所有数据载入内存。配合$writer-setPreCalculateFormulas(false)可以进一步降低内存——如果导出文件里没有公式关闭预计算能让内存占用降低约 30%。内存的问题还可以开ini_set(memory_limit, 512M)临时抬高但这不是根本解法根本解法永远是分批写。4.3 性能表现与调参建议实测中等配置的 PHP 环境单核 2G 内存导出 5000 行、13 列的数据包含样式和自动列宽计算耗时约 1.8-2.5 秒生成文件大小约 300KB。这让它在多数内部系统导出场景中表现够用。如果对耗时敏感可以参考这几个调参方向配置项默认值调优建议效果column_width_modeauto数据量大时改 fixed跳过全量遍历计算速度快约 15%has_bordertrue数据量大时关掉每个单元格少 1 次 style 计算has_filtertrue仅保留表头筛选时开数据库量级影响不大setPreCalculateFormulastrue无公式时改 false内存占用降低约 30%一个实际经验导出任务放到 Web 请求里同步执行容易造成「页面转圈几十秒」。如果数据量超过 2 万行建议命令行动态实现生成文件后写到服务器本地再通过异步通知下载链接——这份源码包的封装虽然简化到了浏览器直出但底层的save方法完全可以改成文件路径所以迁移到 CLI 模式并不需要改动核心逻辑。5. 避坑记录我踩过的五个 xlsx 导出坑每个都真实引发过线上工单5.1 下载文件打不开提示「文件已损坏」现象浏览器下载正常但双击 xlsx 弹出「Excel 无法打开文件...文件已损坏」排查后定位到 PHP 文件在?php前有一个空行或者文件尾部有一个不可见空格导致文件流输出前多了 BOM 头字节。原因Content-Type和文件字节流之间不得有任何输出。现在 PHP 的header()函数不会在headers_sent()之后报错而是直接静默失败文件流前混入了可见或不可见的字符。解决编辑器统一设置为 UTF-8 无 BOM 格式在download()方法第一行加if (headers_sent($file, $line)) { die(headers sent at {$file}:{$line}); }快速定位。5.2 手机号和订单号显示成科学计数法现象导出的订单编号是4.23521E11用户复制出来拿到别处匹配不到原数据。原因xlsx 单元格默认数字格式对超过 11 位的纯数字自动转科学计数法PhpSpreadsheet 的setCellValue自动做了类型推断。解决写入前把这类字段声明为type string强制转字符串后再写入如果必须保持数字类型用$sheet-getStyle($cell)-getNumberFormat()-setFormatCode(0)设置自定义格式但这样导出后 Excel 里仍显示为文本对齐方式不如直接转字符串稳妥。5.3 中文列宽计算不准确表头被截断现象自动列宽设定后含中文的列出现「表头字体完整但框只有半个字符」的情况。原因strlen按字节计算一个 UTF-8 中文字占 3 字节被错误算成 3 个字符宽度导致列宽被撑爆或反过来按字符数算又低估了每个中文字实际占用的像素。解决用mb_strlen($value, UTF-8)计数得到结果后再乘以 1.8 到 2.0 的中文宽度系数修正。如果业务里经常导出中文地址、备注这类文本建议固定列宽而不是依赖 auto 模式。5.4 特殊字符导致 XML 解析报错现象Excel 打开时提示「文件格式和扩展名不匹配」数据源里某条记录的备注字段包含了,或其他 HTML 实体字符。原因xlsx 底层是 XML 文件未转义的会被解析为实体标记导致整个 XML 结构损坏。解决写入数据前对字符串执行htmlspecialchars($value, ENT_QUOTES, UTF-8)或者使用 PhpSpreadsheet 自带的 sanitize 机制。另一个常见场景是从富文本编辑器带入了p、br标签strip_tags之后再写入可以避免格式混乱。5.5 导出大文件时内存溢出直接 504现象导出 3 万行报表在调用download()之前就Allowed memory size exhaustedWeb 服务器返回 504。原因之前提到的setAutoSize(true)导致 PhpSpreadsheet 遍历每个单元格重新计算列宽3 万行 x 20 列的单元格量级是几十万次计算内存峰值极速飙升。解决默认从auto改为fixed模式确认没用到公式的前提下设置$writer-setPreCalculateFormulas(false)十万行以上直接切换到 CLI 导出模式文件落地后由前端拉取。6. 二次开发思路把固定导出升级成可配置模板引擎6.1 从「写死代码」到「配置驱动」改造第一步现在这份源码的setHeadersetData已经能应付大多数导出需求但业务每次调整列都要改 PHP 代码仍然需要发版。更灵活的形态是把表头模板定义成 JSON 或 PHP 数组存到配置文件或者数据库表里导出时由前端传一个模板 ID后端读配置、取数据、导出新增列只需要在后台配置界面加一行字段。改造思路是新增一个模板解析层?php // 模板配置示例config/templates/order_export.php return [ sheet_name 订单明细, header [ [field order_no, label 订单编号, width 20, type string, align center], [field created_at, label 下单时间, width 18, type datetime, align center], [field customer_phone, label 客户手机号, width 15, type string, align center], [field total_amount, label 订单金额, width 12, type number, align right], ], summary [ enabled true, label 合计, field total_amount, function sum, ], ];有了这个模板层业务加一列就是往数组里加一行配置不需要碰任何导入逻辑。ExcelExporter的实际消费方改为读取模板配置里的header数组来驱动写入。常见的做法是再封装一个TemplateExporter类内部把模板配置和ExcelExporter做一个桥接。6.2 增加「汇总行」和「多级表头」两个扩展点订单导出基本都要求最后一行有合计金额汇总行实现可以挂到writeBody结束之后遍历数据列里声明了summary的字段做累加再在数据末尾追加一行样式加粗、上边框双线。多级表头则需要合并单元格比如「客户信息」作为一级表头跨两列「姓名」「电话」作为二级表头——处理方式是记录每个跨列分组的起始列索引用$sheet-mergeCells()合并后再设置垂直居中。这两个扩展都不涉及底层重写都是在现有ExcelExporter上追加方法就能完成。6.3 一个实用的小技巧导出数据自动加「导出时间」水印列实际业务里经常碰到运营质疑报表「这是什么时候的导出」为了避免对账纠纷可以默认在每一行里追加一个「导出时间」列加上数据来源备注。实现很简单在writeBody循环之前给数据数组补一个字段?php array_walk($rows, function ($row) { $row[_exported_at] date(Y-m-d H:i:s); });然后把_exported_at加进 header 配置里这样既不污染原有数据结构也不影响对账。这个字段往往在排查「为什么两人导出的数据不一样」时起到关键作用——导出的时间不同、期间的数据库状态不同数据差异就解释清楚了。6.4 验证与收尾每次改动后强制走一遍冒烟测试脚本源码包里的tests/run_tests.php是最后一道防线。它做的事情很简单构造三组已知结果的数据分别走单表导出、多表导出、大数据量导出把生成的 xlsx 重新用 PhpSpreadsheet 读回来断言关键单元格的字符串值是否与预期一致。每次改动后我都会强制跑一遍这个脚本再提交代码php tests/run_tests.php这个脚本能抓出「表头错位」「时间格式漂移」「科学计数法回潮」三类回归问题。从那以后我每次导出功能改动都强制走一遍模板配置评审加冒烟测试确认读回的值与写入值完全一致才敢交付。这算是我踩过上面那些坑之后留的习惯希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站