dcat-x / laravel-xlswriter-export
A high-performance Excel export package for Laravel based on xlswriter extension, supporting large datasets with low memory usage.
Installs: 3
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/dcat-x/laravel-xlswriter-export
Requires
- php: ^8.2
- ext-xlswriter: *
- laravel/framework: ^12.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- viest/php-ext-xlswriter-ide-helper: dev-master
Suggests
- ext-swoole: Required for async export support in Swoole environments
README
Laravel Xlswriter Export
基于 xlswriter 扩展的 Laravel 高性能 Excel 导出工具
目录
特性
- 高性能 - 基于 C 扩展 xlswriter,导出速度快,内存占用低
- 大数据支持 - 支持 50 万+ 行数据导出,分块处理避免内存溢出
- 多数据源 - 支持 Query Builder、Collection、Array 等多种数据源
- 样式定制 - 支持单元格合并、自定义样式、冻结窗格等
- Swoole 兼容 - 完美支持 Swoole 协程环境
- 链式调用 - 优雅的 API 设计,支持链式配置
环境要求
- PHP >= 8.2
- Laravel >= 12.0
- xlswriter PHP 扩展
安装
1. 安装 xlswriter 扩展
在安装此包之前,需要先安装 xlswriter PHP 扩展。
Linux (推荐)
# 使用 PECL 安装 pecl install xlswriter # 或者从源码编译 git clone https://github.com/viest/php-ext-xlswriter.git cd php-ext-xlswriter phpize ./configure make && make install
添加到 php.ini:
extension=xlswriter.so
macOS
pecl install xlswriter
如果使用 Homebrew 安装的 PHP:
# 确保 pecl 可用
brew install php
pecl install xlswriter
Windows
- 访问 xlswriter releases
- 下载对应 PHP 版本的 DLL 文件
- 将 DLL 放入 PHP 的
ext目录 - 在
php.ini添加:extension=xlswriter
Docker
FROM php:8.2-fpm RUN pecl install xlswriter \ && docker-php-ext-enable xlswriter
安装后运行 php -m | grep xlswriter 或查看 phpinfo() 确认扩展已启用。
2. 安装扩展包
composer require dcat-x/laravel-xlswriter-export
快速开始
基础导出
创建一个继承 BaseExport 的导出类:
<?php namespace App\Exports; use Aoding9\Laravel\Xlswriter\Export\BaseExport; class UserExport extends BaseExport { public $header = [ ['column' => 'a', 'width' => 8, 'name' => 'ID'], ['column' => 'b', 'width' => 15, 'name' => '姓名'], ['column' => 'c', 'width' => 10, 'name' => '性别'], ['column' => 'd', 'width' => 20, 'name' => '创建时间'], ]; public $fileName = '用户导出'; public $tableTitle = '用户导出表'; public function eachRow($row) { return [ $row->id, $row->name, $row->gender, $row->created_at->toDateTimeString(), ]; } }
在控制器中使用:
use App\Exports\UserExport; use App\Models\User; public function export() { $query = User::query(); return UserExport::make($query)->export(); }
从 Collection/Array 导出
$data = [ ['id' => 1, 'name' => '张三', 'created_at' => now()->toDateString()], ['id' => 2, 'name' => '李四', 'created_at' => now()->toDateString()], ]; UserExport::make($data)->export();
或者重写 buildData() 方法实现分块处理:
public function buildData(?int $page = null, ?int $perPage = null) { // 从 API 或其他数据源获取数据 return collect($this->fetchDataFromApi($page, $perPage)); }
链式配置
UserExport::make($query) ->setMax(100000) // 最大导出 10 万行 ->setChunkSize(2000) // 每块 2000 条 ->useFreezePanes(true) // 启用冻结窗格 ->setFontFamily('宋体') // 设置字体 ->export();
配置选项
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
$fileName |
string | '文件名' | 导出文件名(不含扩展名) |
$tableTitle |
string | '表名' | 首行合并标题 |
$sheetName |
string | 'Sheet1' | 工作表名称 |
$useTitle |
bool | true | 是否显示首行标题 |
$useFreezePanes |
bool | false | 是否启用冻结窗格 |
$useGlobalStyle |
bool | true | 使用全局样式(末尾无边框) |
$fontFamily |
string | '微软雅黑' | 默认字体 |
$rowHeight |
int | 40 | 数据行高 |
$headerRowHeight |
int | 40 | 表头行高 |
$titleRowHeight |
int | 50 | 标题行高 |
$max |
int | 500000 | 最大导出行数 |
$chunkSize |
int | 5000 | 每块数据量 |
$debug |
bool | false | 启用调试日志 |
$useSwoole |
bool | false | Swoole 模式 |
$shouldDelete |
bool | true | 下载后删除临时文件 |
高级用法
单元格合并
行级合并(在每行插入后)
public function afterInsertEachRowInEachChunk($row) { // 每两行合并一次 B 列 if ($this->index % 2 === 1 && $this->getCurrentLine() < $this->completed + $this->startDataRow) { $range = "B{$this->getCurrentLine()}:B" . ($this->getCurrentLine() + 1); $nextRow = $this->getRowInChunkByIndex($this->index + 1); $value = $row->id . '---' . ($nextRow ? $nextRow->id : ''); $this->excel->mergeCells($range, $value, $this->getNormalStyle()); } }
静态合并(所有数据插入后)
public function mergeCellsAfterInsertData() { return [ ['range' => "A1:{$this->end}1", 'value' => $this->getTableTitle(), 'formatHandle' => $this->titleStyle], ['range' => "A2:A3", 'value' => '序号', 'formatHandle' => $this->headerStyle], ['range' => "B2:B3", 'value' => 'ID', 'formatHandle' => $this->headerStyle], ['range' => "C2:E2", 'value' => '基本信息', 'formatHandle' => $this->headerStyle], ]; }
自定义单元格样式
use Vtiful\Kernel\Format; public function insertCellHandle($currentLine, $column, $data, $format, $formatHandle) { // 为特定列设置高亮样式 if ($this->getColumn($column) === 'E' && $data instanceof Carbon) { if ($data->isToday()) { $formatHandle = $this->getHighlightStyle(); } $data = $data->toDateTimeString(); } return $this->excel->insertText($currentLine, $column, $data, $format, $formatHandle); } protected function getHighlightStyle() { return (new Format($this->fileHandle)) ->background(Format::COLOR_YELLOW) ->fontSize(10) ->bold() ->align(Format::FORMAT_ALIGN_CENTER, Format::FORMAT_ALIGN_VERTICAL_CENTER) ->border(Format::BORDER_THIN) ->toResource(); }
Swoole 支持
在 Swoole 环境中,不能使用 exit() 终止请求:
class UserExport extends BaseExport { public $useSwoole = true; // ... } // 控制器中必须 return public function export() { return UserExport::make($query)->export(); }
自定义数据源
class ApiExport extends BaseExport { protected string $apiUrl; public function __construct($apiUrl) { $this->apiUrl = $apiUrl; parent::__construct(null); // 传入 null,使用 buildDataFromOther } public function buildDataFromOther(?int $page = null, ?int $perPage = null) { $response = Http::get($this->apiUrl, [ 'page' => $page, 'per_page' => $perPage, ]); return collect($response->json('data')); } }
性能基准
导出 4 列数据表,使用分块查询的测试结果:
| 行数 | 分块大小 | 耗时 | 内存占用 |
|---|---|---|---|
| 10,000 | 2,000 | ~2s | ~15MB |
| 100,000 | 5,000 | ~10s | ~25MB |
| 500,000 | 50,000 | ~45s | ~50MB |
测试环境: PHP 8.2, Laravel 12, MySQL 8.0, 8GB RAM
性能优化建议
- 合理设置 chunkSize - 根据服务器内存调整,一般 2000-10000 为宜
- 使用 Query Builder - 避免一次性加载全部数据到内存
- 禁用不必要的样式 - 设置
$useGlobalStyle = false可略微提升性能 - 关联数据预加载 - 使用
with()预加载避免 N+1 查询
API 参考
构造方法
| 方法 | 说明 |
|---|---|
make($dataSource, $time = null) |
静态构造函数 |
__construct($dataSource, $time = null) |
构造函数,$time 用于调试计时 |
导出方法
| 方法 | 说明 |
|---|---|
export() |
执行导出并触发下载 |
store() |
仅保存文件,不触发下载 |
download($filePath = null) |
下载指定文件 |
配置方法
| 方法 | 说明 |
|---|---|
setMax(int $max) |
设置最大导出行数 |
setChunkSize(int $size) |
设置分块大小 |
setDebug(bool $debug) |
启用/禁用调试模式 |
setFontFamily(string $font) |
设置默认字体 |
setUseTitle(bool $use) |
是否显示首行标题 |
useFreezePanes(bool $use) |
启用/禁用冻结窗格 |
shouldDelete(bool $delete) |
下载后是否删除文件 |
setSheet(string $name) |
设置工作表名称 |
属性
| 属性 | 类型 | 说明 |
|---|---|---|
$index |
int | 当前数据行索引(从 1 开始) |
$currentLine |
int | 当前 Excel 行(从 0 开始) |
$completed |
int | 已导出的总行数 |
$chunkData |
Collection | 当前分块数据 |
$excel |
Excel | xlswriter 实例 |
$fileHandle |
resource | 文件句柄 |
$filePath |
string | 导出文件路径 |
辅助方法
| 方法 | 说明 |
|---|---|
getCurrentLine() |
获取当前 Excel 行号(从 1 开始) |
getIndex() |
获取当前数据索引 |
getColumn(int $index) |
列索引转字母(0 → A) |
getColumnIndexByName(string $name) |
字母转列索引(A → 0) |
getCellName(int $line, int $col) |
获取单元格名称(如 A1) |
getRowInChunkByIndex(int $index) |
从当前分块获取指定索引的行数据 |
生命周期钩子
| 方法 | 调用时机 |
|---|---|
beforeInsertData() |
数据插入前 |
afterInsertEachRowInEachChunk($row) |
每行插入后 |
afterInsertData() |
所有数据插入后 |
beforeOutput() |
文件输出前 |
afterStore() |
文件保存后 |
常见问题
Q: 大数字显示为科学计数法怎么办?
A: 将数字转为字符串:
public function eachRow($row) { return [ $row->id, (string) $row->phone, // 转为字符串 (string) $row->id_card, // 身份证号等长数字 ]; }
Q: 如何导出带公式的单元格?
A: 使用 xlswriter 的 insertFormula 方法:
public function insertCellHandle($currentLine, $column, $data, $format, $formatHandle) { if ($this->getColumn($column) === 'F') { return $this->excel->insertFormula($currentLine, $column, '=SUM(D' . ($currentLine + 1) . ':E' . ($currentLine + 1) . ')'); } return parent::insertCellHandle($currentLine, $column, $data, $format, $formatHandle); }
Q: 如何设置列宽自适应?
A: xlswriter 不支持真正的自适应,需要预估宽度:
public $header = [ ['column' => 'a', 'width' => 10, 'name' => 'ID'], // 短内容 ['column' => 'b', 'width' => 30, 'name' => '描述'], // 长内容 ['column' => 'c', 'width' => 20, 'name' => '创建时间'], // 日期时间 ];
Q: 导出时内存不足怎么办?
A:
- 减小
$chunkSize值 - 确保使用 Query Builder 而非 Collection
- 增加 PHP 内存限制(临时方案)
UserExport::make($query) ->setChunkSize(1000) // 减小分块 ->setMax(100000) // 限制最大行数 ->export();
Q: 如何导出多个 Sheet?
A: 当前版本暂不支持多 Sheet,可以考虑导出多个文件后合并。
故障排查
扩展未加载
Class 'Vtiful\Kernel\Excel' not found
解决: 检查 xlswriter 扩展是否正确安装:
php -m | grep xlswriter
文件无法写入
Unable to open file for writing
解决: 检查临时目录权限:
// 查看临时目录 echo sys_get_temp_dir(); // 确保目录可写 chmod 777 /tmp
内存溢出
Allowed memory size of xxx bytes exhausted
解决:
- 减小
$chunkSize - 使用 Query Builder 替代 Collection
- 临时增加内存限制:
ini_set('memory_limit', '512M')
Swoole 环境报错
exit() not allowed in Swoole
解决: 设置 $useSwoole = true 并在控制器中 return 导出结果。
贡献指南
欢迎贡献代码!请查看 CONTRIBUTING.md 了解详情。
本地开发
# 克隆仓库 git clone https://github.com/dcat-x/laravel-xlswriter-export.git cd laravel-xlswriter-export # 安装依赖 composer install # 运行测试 composer test # 代码格式化 composer format # 静态分析 composer analyse # 运行所有检查 composer check
许可证
MIT 许可证。详情请参阅 LICENSE 文件。