核心原理:HTTP Header 告訴瀏覽器「這是附件」
// 通用必要 Header
header('Content-Type: application/octet-stream'); // 二進位流
header('Accept-Ranges: bytes'); // 支援範圍請求
header('Content-Disposition: attachment; filename="export.doc"'); // 下載檔名
// 輸出內容...
導出 Word (.doc)
function exportWord(string $htmlContent, string $filename = 'document.doc'): void
{
// 處理中文檔名
$encodedName = rawurlencode($filename);
header("Content-Disposition: attachment; filename*=UTF-8''{$encodedName}; filename=\"{$encodedName}\"");
header('Content-Type: application/msword'); // .doc MIME
header('Content-Length: ' . strlen($htmlContent));
// 簡單 HTML 內容(Word 可開啟)
echo $htmlContent;
exit;
}
// 使用
$html = '
測試報告
這是 粗體 與 紅字。
';
exportWord($html, '測試報告.doc');
導出 Excel (.xls)
function exportExcel(array $rows, string $filename = 'data.xls'): void
{
$encodedName = rawurlencode($filename);
header("Content-Disposition: attachment; filename*=UTF-8''{$encodedName}; filename=\"{$encodedName}\"");
header('Content-Type: application/vnd.ms-excel');
// 輸出 HTML 表格(Excel 可開啟)
echo '';
echo '';
echo '';
foreach ($rows as $i => $row) {
echo '';
foreach ($row as $cell) {
$tag = $i === 0 ? 'th' : 'td';
echo "<{$tag}>" . htmlspecialchars($cell) . "{$tag}>";
}
echo '
';
}
echo '
';
exit;
}
// 使用
$data = [
['姓名', '年齡', '身分證字號'],
['王小明', 25, 'A123456789'],
['李大華', 30, 'B987654321'],
];
exportExcel($data, '會員名單.xls');
常見坑點與解決方案
1. Excel 科學記號問題(長數字/身分證自動變 1.23E+10)
// 解決:在數值前加 \t(Tab)或空格,強制視為文字
// 修改輸出邏輯:
foreach ($row as $cell) {
// 判斷是否為純數字/長數字
if (preg_match('/^\d{11,}$/', $cell)) {
$cell = "\t" . $cell; // 前置 Tab
}
echo "| {$cell} | ";
}
// 更完善:針對特定欄位(如身分證欄)強制文字格式
// 在 加上 x:str 樣式(Office XML 格式)
echo ' | ' . htmlspecialchars($cell) . ' | ';
2. 中文檔名亂碼
// 統一使用 RFC 5987 格式
function setDownloadHeader(string $filename, string $mime): void
{
$encoded = rawurlencode($filename);
header("Content-Type: {$mime}; charset=UTF-8");
header("Content-Disposition: attachment; filename*=UTF-8''{$encoded}; filename=\"{$encoded}\"");
}
3. 輸出前已有輸出(Headers already sent)
// 解決:ob_start() 緩衝、或確保 header 前無任何 echo/空白/UTF-8 BOM
ob_start();
// ... 邏輯處理 ...
exportExcel($data); // 內含 exit
// 或手動:
ob_end_clean(); // 清除前置輸出
header(...);
echo $content;
4. 大資料量記憶體耗盡
// 分塊輸出,避免一次佔用大量記憶體
function exportLargeExcel(Generator $dataGenerator, string $filename): void
{
setDownloadHeader($filename, 'application/vnd.ms-excel');
echo '';
$first = true;
foreach ($dataGenerator as $row) {
if ($first) {
echo '';
foreach ($row as $cell) echo "| {$cell} | ";
echo '
';
$first = false;
} else {
echo '';
foreach ($row as $cell) echo "| {$cell} | ";
echo '
';
}
// 定期 flush
if (++$count % 1000 === 0) { @ob_flush(); @flush(); }
}
echo '
';
}
// 使用:資料庫游標逐行產生
function getDataGenerator(PDO $pdo): Generator
{
$stmt = $pdo->query("SELECT * FROM large_table");
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
yield $row;
}
}
進階建議:使用專用庫
| 需求 | 推薦庫 | 特點 |
| 真實 .xlsx/.docx、樣式、圖表、公式 | PhpSpreadsheet / PHPWord | 功能完整、記憶體較大、支援模板 |
| 大量資料、串流輸出、低記憶體 | Box/Spout | 只寫入、極省記憶體、速度快 |
| 簡單匯出、無依賴、快速開發 | HTML + Header(本文方法) | 零依賴、相容性好、功能有限 |
快速遷移:從 HTML 導出到 PhpSpreadsheet
// composer require phpoffice/phpspreadsheet
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray($data, null, 'A1'); // 直接寫入陣列
// 設定欄位格式:身分證欄為文字
$sheet->getColumnDimension('C')->setWidth(20);
$sheet->getStyle('C2:C' . (count($data)))->getNumberFormat()->setFormatCode('@');
$writer = new Xlsx($spreadsheet);
$writer->save('php://output'); // 直接輸出下載
exit;