直接在業務代碼中寫原生 PDO/mysqli 不僅繁瑣,還易發生 SQL 注入、資源洩漏、編碼亂碼。本文提供一個生產級 PDO 封裝類,支持預處理、事務、異常處理、調試模式,並說明進階擴展方向。
<?php
declare(strict_types=1);
class Database {
private PDO $pdo;
private bool $debug = false;
private array $queryLog = [];
public function __construct(array $config) {
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset'] ?? 'utf8mb4'}";
$this->pdo = new PDO($dsn, $config['user'], $config['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 真正預處理
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES {$config['charset'] ?? 'utf8mb4'}",
]);
}
public function setDebug(bool $debug): self { $this->debug = $debug; return $this; }
/** 通用查詢:自動預處理綁定參數 */
public function query(string $sql, array $params = []): PDOStatement {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
if ($this->debug) $this->queryLog[] = ['sql' => $sql, 'params' => $params, 'time' => microtime(true)];
return $stmt;
}
/** 獲取單行 */
public function fetchOne(string $sql, array $params = []): ?array {
return $this->query($sql, $params)->fetch() ?: null;
}
/** 獲取所有行 */
public function fetchAll(string $sql, array $params = []): array {
return $this->query($sql, $params)->fetchAll();
}
/** 獲取單值 */
public function fetchColumn(string $sql, array $params = [], int $col = 0): mixed {
return $this->query($sql, $params)->fetchColumn($col);
}
/** 執行增刪改,返回影響行數 */
public function execute(string $sql, array $params = []): int {
return $this->query($sql, $params)->rowCount();
}
/** 返回最後插入 ID */
public function lastInsertId(string $name = null): string {
return $this->pdo->lastInsertId($name);
}
/** 事務執行:自動回滾異常 */
public function transaction(callable $callback): mixed {
$this->pdo->beginTransaction();
try {
$result = $callback($this);
$this->pdo->commit();
return $result;
} catch (Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
}
/** 獲取調試日志 */
public function getQueryLog(): array { return $this->queryLog; }
}
?>
<?php
require 'Database.php';
$config = [
'host' => '127.0.0.1',
'user' => 'root',
'pass' => 'password',
'dbname' => 'myapp',
'charset' => 'utf8mb4',
];
$db = new Database($config);
$db->setDebug(true); // 開發環境開啟
// 參數化查詢(防注入)
$user = $db->fetchOne('SELECT * FROM users WHERE email = ? AND status = ?', [$email, 1]);
// 批量插入
$db->transaction(function($db) use ($users) {
foreach ($users as $u) {
$db->execute('INSERT INTO users (name, email) VALUES (?, ?)', [$u['name'], $u['email']]);
}
});
// 調試輸出
if ($db->getQueryLog()) {
echo '<pre>'; print_r($db->getQueryLog()); echo '</pre>';
}
?>
| 決策點 | 選擇 | 理由 |
|---|---|---|
| 錯誤模式 | PDO::ERRMODE_EXCEPTION | 強制拋出異常,避免靜默失敗,配合 try-catch 統一處理 |
| 預處理模式 | ATTR_EMULATE_PREPARES = false | 使用數據庫原生預處理,防止模擬預處理導致的注入繞過 |
| 默認獲取模式 | FETCH_ASSOC | 關聯數組語義明確,避免數字索引混淆 |
| 字符集 | DSN + INIT_COMMAND 雙重設置 | 確保連接、結果集、錯誤信息全鏈路 UTF-8 |
| 事務 API | transaction(callable) | 自動提交/回滾,業務代碼無需手動管理,異常安全 |
Doctrine DBAL QueryBuilder 或自建輕量 Builder,支持鏈式 ->table('users')->where('id', 1)->get()['write' => ..., 'read' => [...]],SELECT 隨機走從庫PDO::ATTR_PERSISTENT 謹慎使用query() 解析 SQL 自動路由到分表queryLog 定期刷入 Elasticsearch/ClickHouse,支持慢查詢告警生產環境建議:直接使用成熟 ORM(Doctrine/Eloquent)或 Query Builder,手寫封裝僅適合極簡項目、教學或特殊定制需求。核心原理(預處理、異常模式、事務封裝)必須掌握。