PDO (PHP Data Objects) 自 PHP 5.1 起提供統一數據庫訪問層,支援 12 種驅動,採用預處理機制原生防禦 SQL 注入,是現代 PHP 開發的標準選擇。本文涵蓋安裝配置、連接模式、核心 API、事務控制、錯誤處理三大模塊。
; php.ini 啟用擴展 (去掉分號)
extension=pdo_mysql
; 如需其他數據庫:
; extension=pdo_pgsql
; extension=pdo_sqlite
; extension=pdo_oci
驗證:php -m | grep pdo 或 phpinfo() 檢查 PDO drivers => mysql。
true]);
// 關鍵屬性設置
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 拋出異常
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // 默認關聯數組
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // 使用原生預處理
echo "Connected successfully";
} catch (PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
?>
// 直接查詢 (無參數)
$stmt = $pdo->query('SELECT * FROM members');
$rows = $stmt->fetchAll(); // 獲取所有行
foreach ($rows as $row) { /* ... */ }
// 單行獲取
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// 逐行迭代 (內存友好)
while ($row = $stmt->fetch()) { /* ... */ }
// 命名佔位符 (推薦,可讀性強)
$sql = 'INSERT INTO `user` (`login`, `password`) VALUES (:login, :password)';
$stmt = $pdo->prepare($sql);
$stmt->execute([':login' => 'kevin', ':password' => password_hash('123', PASSWORD_BCRYPT)]);
$lastId = $pdo->lastInsertId();
// 問號佔位符 (位置綁定)
$stmt = $pdo->prepare('UPDATE `user` SET `password` = ? WHERE `user_id` = ?');
$stmt->execute([password_hash('456', PASSWORD_BCRYPT), 7]);
$affected = $stmt->rowCount();
// 綁定值/參數 (進階場景)
$stmt = $pdo->prepare('SELECT * FROM `user` WHERE `login` LIKE :pattern');
$stmt->bindValue(':pattern', 'kevin%', PDO::PARAM_STR);
$stmt->execute();
$count = $pdo->exec("DELETE FROM `menu` WHERE `mid` = 43"); // 直接執行
// 或預處理
$stmt = $pdo->prepare('DELETE FROM `menu` WHERE `mid` = ?');
$stmt->execute([43]);
$count = $stmt->rowCount();
try {
$pdo->beginTransaction();
$pdo->exec("INSERT INTO `account` (`user`, `amount`) VALUES ('A', 100)");
$pdo->exec("INSERT INTO `account` (`user`, `amount`) VALUES ('B', -100)");
// 模擬業務邏輯失敗
// throw new Exception('Insufficient balance');
$pdo->commit();
echo "Transaction committed";
} catch (Exception $e) {
$pdo->rollBack();
echo "Transaction rolled back: " . $e->getMessage();
}
關鍵點:
beginTransaction() 關閉自動提交;beginTransaction() 會拋出異常。| 模式常量 | 行為 | 適用場景 |
|---|---|---|
PDO::ERRMODE_SILENT (默認) | 設置錯誤碼,需手動調用 errorCode()/errorInfo() | 極少用,不推薦 |
PDO::ERRMODE_WARNING | 觸發 E_WARNING,腳本繼續執行 | 調試階段快速定位 |
PDO::ERRMODE_EXCEPTION | 拋出 PDOException,含 SQLSTATE、驅動錯誤碼、錯誤信息 | 生產環境強制推薦 |
// 異常對象常用屬性/方法
$e->getMessage(); // 錯誤描述
$e->getCode(); // SQLSTATE
$e->getFile(); // 發生文件
$e->getLine(); // 發生行號
$e->getTraceAsString(); // 堆棧追蹤
| 分類 | 常量 | 說明 |
|---|---|---|
| 獲取模式 | PDO::FETCH_ASSOC | 關聯數組 (鍵=列名) |
PDO::FETCH_NUM | 數字索引數組 | |
PDO::FETCH_BOTH | 雙模式 (默認) | |
PDO::FETCH_OBJ | 匿名對象 (屬性=列名) | |
PDO::FETCH_CLASS | 映射到指定類 | |
PDO::FETCH_LAZY | 延遲加載 (PDORow 對象) | |
| 連接屬性 | PDO::ATTR_PERSISTENT | 持久連接 |
PDO::ATTR_ERRMODE | 錯誤模式 | |
PDO::ATTR_DEFAULT_FETCH_MODE | 默認獲取模式 | |
PDO::ATTR_EMULATE_PREPARES | 模擬預處理 (false=原生) | |
| 參數類型 | PDO::PARAM_STR | 字符串 |
PDO::PARAM_INT | 整數 | |
PDO::PARAM_BOOL | 布爾 |
PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
function registerUser(PDO $pdo, string $email, string $plainPass): int {
$pdo->beginTransaction();
try {
// 1. 檢查郵箱唯一
$stmt = $pdo->prepare('SELECT 1 FROM `users` WHERE `email` = ?');
$stmt->execute([$email]);
if ($stmt->fetch()) {
throw new DomainException('Email already registered');
}
// 2. 插入用戶
$hash = password_hash($plainPass, PASSWORD_BCRYPT);
$stmt = $pdo->prepare('INSERT INTO `users` (`email`, `password_hash`, `created_at`) VALUES (?, ?, NOW())');
$stmt->execute([$email, $hash]);
$userId = (int)$pdo->lastInsertId();
// 3. 初始化用戶資源 (同一事務)
$pdo->prepare('INSERT INTO `user_profile` (`user_id`, `nickname`) VALUES (?, ?)')
->execute([$userId, substr($email, 0, strpos($email, '@'))]);
$pdo->prepare('INSERT INTO `user_wallet` (`user_id`, `balance`) VALUES (?, 0)')
->execute([$userId]);
$pdo->commit();
return $userId;
} catch (Exception $e) {
$pdo->rollBack();
throw $e; // 向上拋出,由控制器統一處理
}
}
// 控制器調用
try {
$uid = registerUser($pdo, 'test@example.com', 'secret123');
echo "User created: $uid";
} catch (DomainException $e) {
http_response_code(409);
echo $e->getMessage();
} catch (PDOException $e) {
http_response_code(500);
error_log($e->getMessage());
echo "System error, please try later";
}
?>
charset=utf8mb4,否則存 Emoji 會截斷/報錯。ATTR_EMULATE_PREPARES => false 強制使用 MySQL 原生預處理,避免邊界注入風險。INSERT ... VALUES (),(),() 僅返回第一行 ID,需分批插入或查詢 MAX(id)。ATTR_PERSISTENT 可能導致「MySQL server has gone away」,需配合 ATTR_AUTOCOMMIT、連接池心跳檢測。$stmt->debugDumpParams() 輸出綁定參數與 SQL,開發環境開啟。PDO 以「統一介面、預處理安全、物件導向」三大優勢成為 PHP 數據庫開發標準。掌握異常模式、事務邊界、預處理參數綁定,即可編寫健壯、安全、可維護的數據訪問層代碼。