業務中常需導出報表、備份數據、生成靜態頁。本文封裝一個 FileExporter 類,支援 TXT、CSV、HTML、JSON、XML 多格式,統一入口、自動編碼轉換、流式寫入大數據不佔記憶體。
<?php
class FileExporter {
private string $dir;
private string $defaultEncoding = 'UTF-8';
public function __construct(string $dir) {
$this->dir = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR;
if (!is_dir($this->dir)) {
mkdir($this->dir, 0755, true);
}
}
/**
* 統一導出入口
*
* @param string $filename 不含副檔名
* @param string $type txt/csv/html/json/xml
* @param array $header 標題行 (關聯陣列 key=>label 或索引陣列)
* @param iterable $rows 數據行 (生成器/陣列/PDOStatement)
* @param array $options 格式專用選項
* @return string|false 完整文件路徑或 false
*/
public function export(string $filename, string $type, array $header, iterable $rows, array $options = []) {
$method = 'export' . ucfirst(strtolower($type));
if (!method_exists($this, $method)) {
throw new InvalidArgumentException("不支援的導出格式: $type");
}
$filepath = $this->dir . $filename . '.' . $type;
return $this->$method($filepath, $header, $rows, $options);
}
// ===== TXT: 固定寬度/分隔符 =====
private function exportTxt(string $path, array $header, iterable $rows, array $opt): string {
$delimiter = $opt['delimiter'] ?? "\t";
$encoding = $opt['encoding'] ?? 'GBK'; // 中文環境常用 GBK 兼容 Excel
$newline = $opt['newline'] ?? PHP_EOL;
$fp = fopen($path, 'wb');
if (!$fp) throw new RuntimeException("無法創建文件: $path");
// 寫入 BOM (GBK/UTF-8 可選)
if ($encoding === 'UTF-8') fwrite($fp, "\xEF\xBB\xBF");
// 標題行
if ($header) {
fputcsv($fp, array_values($header), $delimiter, '"', '\\');
// fputcsv 會自動處理換行/引號轉義,但輸出編碼需手動轉換
// 這裡簡化:實際建議逐行 iconv
}
// 數據行 (流式寫入)
foreach ($rows as $row) {
$line = array_map(function($v) use ($encoding) {
return is_scalar($v) ? @iconv('UTF-8', $encoding . '//IGNORE', (string)$v) : '';
}, array_values((array)$row));
fwrite($fp, implode($delimiter, $line) . $newline);
}
fclose($fp);
return $path;
}
// ===== CSV: 標準 RFC 4180 =====
private function exportCsv(string $path, array $header, iterable $rows, array $opt): string {
$encoding = $opt['encoding'] ?? 'UTF-8';
$delimiter = $opt['delimiter'] ?? ',';
$enclosure = $opt['enclosure'] ?? '"';
$fp = fopen($path, 'wb');
if ($encoding === 'UTF-8') fwrite($fp, "\xEF\xBB\xBF"); // UTF-8 BOM
if ($header) {
fputcsv($fp, array_values($header), $delimiter, $enclosure);
}
foreach ($rows as $row) {
$vals = array_map(function($v) {
return is_scalar($v) ? (string)$v : '';
}, array_values((array)$row));
fputcsv($fp, $vals, $delimiter, $enclosure);
}
fclose($fp);
return $path;
}
// ===== HTML: 簡易表格 =====
private function exportHtml(string $path, array $header, iterable $rows, array $opt): string {
$title = $opt['title'] ?? 'Data Export';
$fp = fopen($path, 'wb');
fwrite($fp, <<
$title
HTML);
foreach ($header as $col) {
fwrite($fp, '' . htmlspecialchars((string)$col, ENT_QUOTES, 'UTF-8') . ' ');
}
fwrite($fp, ' ');
foreach ($rows as $row) {
fwrite($fp, '');
foreach ((array)$row as $cell) {
fwrite($fp, '' . htmlspecialchars((string)$cell, ENT_QUOTES, 'UTF-8') . ' ');
}
fwrite($fp, ' ');
}
fwrite($fp, '
');
fclose($fp);
return $path;
}
// ===== JSON: 逐行 JSON Lines (大數據友好) =====
private function exportJson(string $path, array $header, iterable $rows, array $opt): string {
$pretty = $opt['pretty'] ?? false;
$fp = fopen($path, 'wb');
if ($pretty) fwrite($fp, "[\n");
$first = true;
foreach ($rows as $row) {
$assoc = array_combine($header, array_values((array)$row));
$json = json_encode($assoc, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | ($pretty ? JSON_PRETTY_PRINT : 0));
if (!$first && $pretty) fwrite($fp, ",\n");
elseif (!$first) fwrite($fp, "\n");
fwrite($fp, $json);
$first = false;
}
if ($pretty) fwrite($fp, "\n]");
fclose($fp);
return $path;
}
// ===== XML: 簡單結構 =====
private function exportXml(string $path, array $header, iterable $rows, array $opt): string {
$root = $opt['root'] ?? 'data';
$item = $opt['item'] ?? 'row';
$fp = fopen($path, 'wb');
fwrite($fp, '' . "\n<$root>\n");
foreach ($rows as $row) {
fwrite($fp, " <$item>\n");
foreach (array_combine($header, array_values((array)$row)) as $k => $v) {
$k = preg_replace('/[^a-zA-Z0-9_-]/', '_', $k); // 合法標籤名
fwrite($fp, " <$k>" . htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8') . "$k>\n");
}
fwrite($fp, " $item>\n");
}
fwrite($fp, "$root>\n");
fclose($fp);
return $path;
}
}
?>
<?php
require 'FileExporter.php';
$exporter = new FileExporter(__DIR__ . '/exports');
// 模擬大數據源 (生成器,不佔記憶體)
function dataSource(): Generator {
for ($i = 1; $i <= 100000; $i++) {
yield [
'id' => $i,
'name' => "用戶$i",
'email' => "user$i@example.com",
'created_at' => date('Y-m-d H:i:s', strtotime("-$i days")),
];
}
}
$header = ['id' => 'ID', 'name' => '姓名', 'email' => '郵箱', 'created_at' => '註冊時間'];
// 導出 CSV (流式,常駐記憶體 < 2MB)
$exporter->export('users_2024', 'csv', $header, dataSource(), [
'encoding' => 'UTF-8',
'delimiter' => ',',
]);
// 導出 HTML (帶標題)
$exporter->export('users_report', 'html', $header, dataSource(), [
'title' => '用戶註冊報表 2024',
]);
// 導出 JSON Lines (每行一個 JSON,適合日誌/大數據入湖)
$exporter->export('users_dump', 'json', $header, dataSource(), [
'pretty' => false,
]);
?>
fopen('wb') + fwrite,配合 Generator 數據源,百萬行導出常駐記憶體 < 5MB。export() 分發具體格式方法,新增格式只需加私有方法,符合開閉原則。['field' => '顯示名'],自動對齊數據鍵名。htmlspecialchars,CSV 依賴 fputcsv 內建轉義。exportXlsx():引入 PhpSpreadsheet 僅在調用時 require_once,避免常駐大型庫。exportPdf():HTML 轉 PDF (wkhtmltopdf/Headless Chrome)。ZipArchive 打包,返回 .zip 下載鏈接。