PHPExcel 合併與拆分儲存格完整指南:mergeCells、unmergeCells 實戰與樣式保留技巧
PHPExcel (現併入 PhpSpreadsheet) 處理合併儲存格是報表匯出常見需求。本文詳解 mergeCells、unmergeCells 用法、合併前清空值避免資料殘留、拆分後樣式還原、批量合併效能優化,並提供遷移 PhpSpreadsheet 的對照表。
一、核心 API 速查
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\IOFactory;
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 合併儲存格區域 (左上:右下)
$sheet->mergeCells('A1:C1'); // A1:C1 合併為單一儲存格
$sheet->mergeCells('A2:B3'); // 2x2 區域合併
// 設置合併後儲存格的值 (只能寫左上角儲存格)
$sheet->setCellValue('A1', '標題橫跨三欄');
$sheet->setCellValue('A2', '2x2 區域合併');
// 取得合併區域陣列
$merges = $sheet->getMergeCells(); // ['A1:C1', 'A2:B3']
// 拆分合併 (還原為獨立儲存格)
$sheet->unmergeCells('A1:C1');
$sheet->unmergeCells('A2:B3');
// 拆分後:原合併區域所有儲存格值清空,樣式重置為預設
?>
二、實戰場景:報表標題跨欄 + 明細分組合併
<?php
function exportReport(array $data): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 1. 標題列合併
$sheet->mergeCells('A1:F1');
$sheet->setCellValue('A1', '2024 年度銷售報表');
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(16);
$sheet->getStyle('A1')->getAlignment()->setHorizontal('center');
// 2. 表頭
$headers = ['部門', '銷售額', '訂單數', '客單價', '轉化率', '備註'];
foreach ($headers as $i => $h) {
$col = chr(ord('A') + $i);
$sheet->setCellValue($col . '2', $h);
}
$sheet->getStyle('A2:F2')->getFont()->setBold(true);
// 3. 明細資料 + 部門分組合併 (A欄相同部門合併)
$row = 3;
$deptStartRow = 3;
$prevDept = null;
foreach ($data as $idx => $record) {
$col = 0;
foreach (['dept', 'amount', 'orders', 'avg_price', 'conv_rate', 'remark'] as $field) {
$cell = chr(ord('A') + $col++) . $row;
$sheet->setCellValue($cell, $record[$field] ?? '');
}
// 部門變更時合併上一組 A欄
if ($prevDept !== null && $record['dept'] !== $prevDept) {
$sheet->mergeCells("A{$deptStartRow}:A" . ($row - 1));
$sheet->getStyle("A{$deptStartRow}:A" . ($row - 1))
->getAlignment()->setVertical('center');
$deptStartRow = $row;
}
$prevDept = $record['dept'];
$row++;
}
// 最後一組
if ($deptStartRow < $row) {
$sheet->mergeCells("A{$deptStartRow}:A" . ($row - 1));
$sheet->getStyle("A{$deptStartRow}:A" . ($row - 1))
->getAlignment()->setVertical('center');
}
// 4. 自動欄寬
foreach (range('A', 'F') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
// 5. 輸出下載
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="report.xlsx"');
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$writer->save('php://output');
}
// 測試資料
$data = [
['dept'=>'華東', 'amount'=>100, 'orders'=>10, 'avg_price'=>10, 'conv_rate'=>'5%', 'remark'=>''],
['dept'=>'華東', 'amount'=>200, 'orders'=>15, 'avg_price'=>13.3, 'conv_rate'=>'6%', 'remark'=>''],
['dept'=>'華南', 'amount'=>150, 'orders'=>12, 'avg_price'=>12.5, 'conv_rate'=>'5.5%', 'remark'=>''],
];
exportReport($data);
?>
三、合併儲存格的 5 大坑與解法
| 坑點 | 現象 | 解法 |
| 合併前儲存格有值 | 非左上角值被隱藏,匯出檔案打開顯示空白 | 合併前 只保留左上角值,其餘 setCellValue('', '') 清空 |
| 拆分後值/樣式丟失 | unmergeCells 會清空整個區域值並重置樣式 | 拆分前 備份值與樣式,拆分後逐一寫回 |
| 重疊合併 | 拋出 Exception: "Cell range overlaps" | 邏輯層檢查 getMergeCells() 避免重疊 |
| 合併區域寫入非左上角 | 值寫入無效,讀取時為空 | 嚴格只操作左上角儲存格 |
| 大量合併效能極差 | 幾千次 mergeCells 導致 XML 膨脹、寫入緩慢 | 批量構建合併陣列,一次性 setMergeCells($arr) (PhpSpreadsheet 1.18+) |
四、PhpSpreadsheet 遷移對照表 (PHPExcel 已停止維護)
| PHPExcel | PhpSpreadsheet | 備註 |
PHPExcel_IOFactory | PhpOffice\PhpSpreadsheet\IOFactory | 命名空間變更 |
$objPHPExcel->getActiveSheet() | $spreadsheet->getActiveSheet() | 類名 Spreadsheet |
mergeCells('A1:B2') | $sheet->mergeCells('A1:B2') | API 相容 |
setCellValue('A1', 'val') | $sheet->setCellValue('A1', 'val') | 相容 |
$style->getFont()->setBold(true) | $style->getFont()->setBold(true) | 相容 |
| 無 | $sheet->setMergeCells(['A1:B2','C1:D2']) | 新增批量設置 |
五、效能優化:批量合併 + 串流寫入
<?php
// PhpSpreadsheet 1.18+ 支援批量設置合併
$merges = [];
// ... 邏輯計算所有合併區域 ...
$sheet->setMergeCells($merges); // 一次性寫入 XML,避免重複 DOM 操作
// 超大報表 (>5萬行) 改用串流寫入
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$writer->setUseDiskCaching(true); // 使用臨時檔緩存,降低記憶體
$writer->setPreCalculateFormulas(false);
$writer->save('php://output');
?>