分析 PHPExcel 在單一 HTTP 請求中重複建立多個試算表物件導致記憶體洩漏的根本原因:工作表與儲存格雙向參考形成循環引用,導致 GC 無法回收。提供在 PHPExcel_Worksheet 與 PHPExcel 類別中新增 Destroy() 方法的修正方案,並說明為何不能放在 __destruct()。
PHPExcel + PHPExcel_Writer_Excel5 產生多份報表。memory_limit 錯誤。// 簡化結構示意
PHPExcel
└─ _workSheetCollection[] → PHPExcel_Worksheet
└─ _cellCollection[] → PHPExcel_Cell
└─ _parent → 回指 PHPExcel_Worksheet (循環引用)
└─ _worksheet → 回指 PHPExcel_Worksheet (循環引用)
PHP 的參考計數 GC 無法處理跨物件循環引用(需 gc_collect_cycles()),
但在請求結束前物件仍被變數參考,不會觸發循環 GC。
public function Destroy() {
foreach ($this->_cellCollection as $index => $dummy) {
$this->_cellCollection[$index] = null; // 斷開儲存格參考
}
}
public function Destroy() {
foreach ($this->_workSheetCollection as $index => $dummy) {
$this->_workSheetCollection[$index]->Destroy(); // 遞迴清理工作表
$this->_workSheetCollection[$index] = null; // 斷開工作表參考
}
}
<?php
// 在迴圈中產生多份報表
foreach ($reports as $report) {
$objPHPExcel = new PHPExcel();
// ... 填充資料、設定樣式 ...
$writer = new PHPExcel_Writer_Excel5($objPHPExcel);
$writer->save($filename);
// 關鍵:手動觸發清理
$objPHPExcel->Destroy();
unset($objPHPExcel, $writer);
// 可選:強制觸發循環 GC(PHP 5.3+)
gc_collect_cycles();
}
?>
__destruct() 在物件參考計數歸零時才會被呼叫。__destruct() 不會被觸發。Destroy()。| 方案 | 優點 | 缺點 |
|---|---|---|
| 手動 Destroy() | 即時釋放、可控 | 需修改源碼/繼承覆寫 |
| gc_collect_cycles() | 無需修改庫 | 效能影響大、時機不可控 |
| 分批請求/CLI 腳本 | 進程隔離、自動回收 | 架構調整成本高 |
| 改用 PhpSpreadsheet | 官方繼任者、修復此問題 | 需遷移代碼 |
新專案請直接使用 PhpSpreadsheet(PHPExcel 官方繼承者),已內建解決循環引用問題。維護舊專案時,可套用上述 Destroy() 修正或定期呼叫 gc_collect_cycles() 緩解。