將備份邏輯封裝為可重用類別,支援指定資料表、僅結構/僅資料、分塊寫入避免記憶體溢出、可選 gzip 壓縮與 OpenSSL 加密。
<?php
class DatabaseBackup {
private PDO $pdo;
private string $outputDir;
private array $options = [
'tables' => null, // null = 全部
'structureOnly' => false,
'dataOnly' => false,
'chunkSize' => 1000, // 每次 SELECT/INSERT 筆數
'compress' => false, // gzip
'encrypt' => false, // OpenSSL AES-256
'encPass' => '',
'addDropTable' => true,
'singleTransaction' => true,
];
public function __construct(PDO $pdo, string $outputDir, array $options = []) {
$this->pdo = $pdo;
$this->outputDir = rtrim($outputDir, '/');
$this->options = array_merge($this->options, $options);
if (!is_dir($this->outputDir)) mkdir($this->outputDir, 0750, true);
}
public function backup(): string {
$filename = $this->options['database'] . '_' . date('Ymd_His') . '.sql';
if ($this->options['compress']) $filename .= '.gz';
if ($this->options['encrypt']) $filename .= '.enc';
$filepath = $this->outputDir . '/' . $filename;
$outFp = $this->openOutput($filepath);
try {
$this->writeHeader($outFp);
$tables = $this->getTables();
foreach ($tables as $table) {
if (!$this->options['dataOnly']) {
$this->writeTableStructure($outFp, $table);
}
if (!$this->options['structureOnly']) {
$this->writeTableData($outFp, $table);
}
}
$this->writeFooter($outFp);
} finally {
$this->closeOutput($outFp, $filepath);
}
return $filepath;
}
private function getTables(): array {
if ($this->options['tables']) return $this->options['tables'];
$stmt = $this->pdo->query("SHOW TABLES");
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
private function writeHeader($fp) {
fwrite($fp, "-- Database Backup " . date('Y-m-d H:i:s') . "\n");
fwrite($fp, "SET FOREIGN_KEY_CHECKS=0;\n");
fwrite($fp, "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';\n");
fwrite($fp, "SET time_zone='+00:00';\n\n");
}
private function writeTableStructure($fp, string $table) {
if ($this->options['addDropTable']) {
fwrite($fp, "DROP TABLE IF EXISTS `$table`;\n");
}
$stmt = $this->pdo->query("SHOW CREATE TABLE `$table`");
$row = $stmt->fetch(PDO::FETCH_NUM);
fwrite($fp, $row[1] . ";\n\n");
}
private function writeTableData($fp, string $table) {
$countStmt = $this->pdo->query("SELECT COUNT(*) FROM `$table`");
$total = (int)$countStmt->fetchColumn();
if ($total === 0) return;
$chunkSize = $this->options['chunkSize'];
$colsStmt = $this->pdo->query("SHOW COLUMNS FROM `$table`");
$columns = array_column($colsStmt->fetchAll(PDO::FETCH_ASSOC), 'Field');
$colList = '`' . implode('`,`', $columns) . '`';
for ($offset = 0; $offset < $total; $offset += $chunkSize) {
$stmt = $this->pdo->prepare("SELECT $colList FROM `$table` LIMIT $chunkSize OFFSET $offset");
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$values = array_map(fn($v) => $v === null ? 'NULL' : $this->pdo->quote($v), $row);
fwrite($fp, "INSERT INTO `$table` ($colList) VALUES (" . implode(',', $values) . ");\n");
}
}
fwrite($fp, "\n");
}
private function writeFooter($fp) {
fwrite($fp, "SET FOREIGN_KEY_CHECKS=1;\n");
}
private function openOutput(string $filepath) {
if ($this->options['encrypt'] && !$this->options['encPass']) {
throw new InvalidArgumentException('Encryption requires password');
}
if ($this->options['compress'] && $this->options['encrypt']) {
// pipe: gzip -> openssl -> file
$cmd = "gzip -c | openssl enc -aes-256-cbc -pbkdf2 -pass pass:" . escapeshellarg($this->options['encPass']) . " > " . escapeshellarg($filepath);
return popen($cmd, 'w');
} elseif ($this->options['compress']) {
return gzopen($filepath, 'wb9');
} elseif ($this->options['encrypt']) {
$cmd = "openssl enc -aes-256-cbc -pbkdf2 -pass pass:" . escapeshellarg($this->options['encPass']) . " > " . escapeshellarg($filepath);
return popen($cmd, 'w');
}
return fopen($filepath, 'wb');
}
private function closeOutput($fp, string $filepath) {
if (is_resource($fp)) {
$meta = stream_get_meta($fp);
if (isset($meta['mode']) && strpos($meta['mode'], 'p') !== false) {
pclose($fp);
} elseif (get_resource_type($fp) === 'stream' && function_exists('gzclose')) {
gzclose($fp);
} else {
fclose($fp);
}
}
// 權限收斂
chmod($filepath, 0600);
}
}
// 使用範例
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$backup = new DatabaseBackup($pdo, '/backups', [
'database' => 'test',
'chunkSize' => 500,
'compress' => true,
'encrypt' => true,
'encPass' => 'StrongPass123!',
]);
$file = $backup->backup();
echo "備份完成: $file\n";
quote() 處理特殊字元。LIMIT ... OFFSET 避免一次載入大量資料。popen('gzip | openssl') 邊讀邊壓縮加密,不落地中間檔。# 解密 -> 解壓 -> 還原 openssl enc -d -aes-256-cbc -pbkdf2 -pass pass:StrongPass123! -in backup.sql.gz.enc | gunzip | mysql -u user -p test
--where 條件備份增量資料。