PDO 驱动子类
概述
PHP 8.4 引入了 PDO 驱动子类功能,允许通过 PDO::connect() 工厂方法和 PDO::getDriver() 获取驱动特定的子类实例。这使得框架可以扩展 PDO 类,添加驱动级别的功能而不会破坏类型提示。
版本要求
PDO::connect() 和 PDO::getDriver() 是 PHP 8.4+ 的新特性。使用前请确认 PHP 版本 >= 8.4。
基础概念
传统 PDO vs 新式 PDO
| 特性 | 传统 new PDO() | PHP 8.4+ PDO::connect() |
|---|---|---|
| 连接方式 | 构造函数 | 静态工厂方法 |
| 返回类型 | PDO | PDO 或自定义子类 |
| 驱动获取 | 不可用 | PDO::getDriver() |
| 子类化支持 | 需要覆盖构造函数 | 原生支持 |
| 可扩展性 | 有限 | 框架友好 |
驱动子类的作用
PDO (基类)
├── PDO_MySQL (MySQL 驱动子类)
├── PDO_PGSQL (PostgreSQL 驱动子类)
├── PDO_SQLite (SQLite 驱动子类)
├── PDO_SQLSRV (SQL Server 驱动子类)
└── PDO_OCI (Oracle 驱动子类)语法与代码
PDO::connect() 工厂方法
php
<?php
declare(strict_types=1);
// PHP 8.4+ 使用 PDO::connect() 创建连接
$dsn = 'mysql:host=localhost;dbname=app_db;charset=utf8mb4';
$username = 'user';
$password = 'pass';
// 方式1: 静态工厂方法(PHP 8.4+)
$pdo = PDO::connect($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// 方式2: 传统构造函数(兼容所有版本)
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// PDO::connect() 的优势:
// 1. 可以被子类覆盖,实现自定义连接逻辑
// 2. 更好的可测试性(可以注入 mock)
// 3. 框架可以扩展返回自定义的 PDO 子类PDO::getDriver() — 获取驱动子类
php
<?php
declare(strict_types=1);
$pdo = PDO::connect('mysql:host=localhost;dbname=app', 'user', 'pass');
// 获取驱动子类实例
$driver = $pdo->getDriver();
var_dump(get_class($driver)); // string(9) "PDO_MySQL"
// 驱动子类提供特定功能
// 例如 MySQL 驱动可以访问 MySQL 特有的功能自定义 PDO 子类
php
<?php
declare(strict_types=1);
// 自定义 PDO 类 — 添加应用级功能
class AppPDO extends PDO
{
public function __construct(string $dsn, string $username = '', string $password = '', array $options = [])
{
parent::__construct($dsn, $username, $password, $options);
}
/**
* 覆盖 connect 方法 — PHP 8.4+
*/
public static function connect(
string $dsn,
?string $username = null,
?string $password = null,
?array $options = null
): static {
// 可以添加自定义逻辑: 日志、指标采集等
$instance = parent::connect($dsn, $username, $password, $options);
return $instance;
}
/**
* 便捷查询方法
*/
public function fetchAll(string $sql, array $params = []): array
{
$stmt = $this->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function fetchOne(string $sql, array $params = []): ?array
{
$stmt = $this->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row ?: null;
}
/**
* 事务闭包
*/
public function transaction(callable $callback): mixed
{
$this->beginTransaction();
try {
$result = $callback($this);
$this->commit();
return $result;
} catch (Exception $e) {
$this->rollBack();
throw $e;
}
}
}
// 使用自定义子类
$pdo = AppPDO::connect('mysql:host=localhost;dbname=app', 'user', 'pass');
$users = $pdo->fetchAll('SELECT * FROM users WHERE status = ?', ['active']);实战示例
多驱动管理器
php
<?php
declare(strict_types=1);
class MultiDriverManager
{
/** @var array<string, PDO> */
private array $connections = [];
/** @var array<string, array> */
private array $configs = [];
/**
* 注册数据库配置
*/
public function register(string $name, array $config): void
{
$this->configs[$name] = array_merge([
'driver' => 'mysql',
'host' => 'localhost',
'port' => 3306,
'database' => '',
'username' => '',
'password' => '',
'charset' => 'utf8mb4',
'options' => [],
], $config);
}
/**
* 获取连接(PHP 8.4+)
*/
public function getConnection(string $name): PDO
{
if (!isset($this->connections[$name])) {
$config = $this->configs[$name] ?? throw new RuntimeException("配置 '{$name}' 不存在");
$dsn = $this->buildDsn($config);
$options = array_merge([
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
], $config['options']);
// PHP 8.4: 使用 PDO::connect()
if (PHP_VERSION_ID >= 80400) {
$this->connections[$name] = PDO::connect($dsn, $config['username'], $config['password'], $options);
} else {
$this->connections[$name] = new PDO($dsn, $config['username'], $config['password'], $options);
}
}
return $this->connections[$name];
}
/**
* 获取驱动类型
*/
public function getDriverName(string $name): string
{
$pdo = $this->getConnection($name);
if (PHP_VERSION_ID >= 80400) {
return get_class($pdo->getDriver());
}
return $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
}
private function buildDsn(array $config): string
{
$driver = $config['driver'];
return match ($driver) {
'mysql' => sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$config['host'], $config['port'], $config['database'], $config['charset']
),
'pgsql' => sprintf(
'pgsql:host=%s;port=%d;dbname=%s',
$config['host'], $config['port'], $config['database']
),
'sqlite' => sprintf('sqlite:%s', $config['database']),
default => throw new RuntimeException("不支持的驱动: {$driver}"),
};
}
}
// 使用示例
$manager = new MultiDriverManager();
$manager->register('primary', [
'driver' => 'mysql',
'host' => 'db-master.local',
'database' => 'app_db',
'username' => 'app_user',
'password' => 'secret',
]);
$manager->register('read_replica', [
'driver' => 'mysql',
'host' => 'db-replica.local',
'database' => 'app_db',
'username' => 'readonly_user',
'password' => 'secret',
]);
$manager->register('cache', [
'driver' => 'sqlite',
'database' => '/tmp/cache.db',
]);
$primary = $manager->getConnection('primary');
$replica = $manager->getConnection('read_replica');
$cache = $manager->getConnection('cache');
echo "主库驱动: " . $manager->getDriverName('primary') . "\n";
echo "缓存驱动: " . $manager->getDriverName('cache') . "\n";驱动特定功能封装
php
<?php
declare(strict_types=1);
class DriverFeatureDetector
{
private PDO $pdo;
private string $driverName;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->driverName = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
}
/**
* MySQL 特有: 获取最后插入 ID
*/
public function lastInsertId(?string $name = null): string
{
if ($this->driverName === 'mysql') {
// MySQL 支持传入序列名(表名)
return $this->pdo->lastInsertId($name);
}
return $this->pdo->lastInsertId();
}
/**
* MySQL 特有: 执行 SHOW FULL PROCESSLIST
*/
public function showProcesslist(): array
{
if ($this->driverName !== 'mysql') {
throw new RuntimeException('仅支持 MySQL 驱动');
}
return $this->pdo->query('SHOW FULL PROCESSLIST')->fetchAll();
}
/**
* PostgreSQL 特有: LISTEN/NOTIFY
*/
public function listen(string $channel, callable $callback): void
{
if ($this->driverName !== 'pgsql') {
throw new RuntimeException('仅支持 PostgreSQL 驱动');
}
$this->pdo->exec("LISTEN {$channel}");
// 需要配合轮询机制
}
/**
* SQLite 特有: 启用 WAL 模式
*/
public function enableWalMode(): void
{
if ($this->driverName !== 'sqlite') {
throw new RuntimeException('仅支持 SQLite 驱动');
}
$this->pdo->exec('PRAGMA journal_mode = WAL');
}
/**
* 检测功能支持
*/
public function supportsTransactions(): bool
{
return $this->pdo->inTransaction() !== false
|| method_exists($this->pdo, 'beginTransaction');
}
public function supportsSavepoints(): bool
{
return match ($this->driverName) {
'mysql', 'pgsql', 'sqlite' => true,
default => false,
};
}
public function getDriverInfo(): array
{
$info = [
'driver' => $this->driverName,
'serverVersion' => $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION),
'clientVersion' => $this->pdo->getAttribute(PDO::ATTR_CLIENT_VERSION),
'connectionStatus' => $this->pdo->getAttribute(PDO::ATTR_CONNECTION_STATUS),
];
return $info;
}
}注意事项
PHP 版本兼容性
php
<?php
// 兼容不同 PHP 版本的连接方式
function createConnection(string $dsn, string $user, string $pass): PDO
{
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
if (PHP_VERSION_ID >= 80400) {
return PDO::connect($dsn, $user, $pass, $options);
}
return new PDO($dsn, $user, $pass, $options);
}
// 检测 PDO::connect 是否可用
if (method_exists(PDO::class, 'connect')) {
$pdo = PDO::connect($dsn, $user, $pass);
} else {
$pdo = new PDO($dsn, $user, $pass);
}子类化注意事项
php
<?php
// 注意1: PDO 构造函数在子类中需要特殊处理
// 因为 PDO 使用特殊的初始化方式
// 错误方式
class MyPDO extends PDO
{
public function __construct(string $dsn, string $user = '', string $pass = '')
{
parent::__construct($dsn, $user, $pass);
$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
}
// 注意2: PDO 对象序列化(__serialize/__unserialize)行为
// PDO 不能被序列化,连接会在序列化时断开
// 尝试序列化 PDO 会导致 PHP 8+ 抛出异常
// 注意3: 克隆 PDO 对象
// PDO 可以被克隆,但得到的是一个新的连接
$pdo1 = new PDO($dsn, $user, $pass);
$pdo2 = clone $pdo1; // $pdo2 是一个独立的新连接PDO 克隆行为
克隆 PDO 对象会创建一个新连接,但不会复制 DSN 中的连接参数。在 PHP 8.1+ 中,尝试使用未设置属性可能导致异常。
最佳实践
1. 框架级 PDO 封装
php
<?php
// 在框架中推荐的做法: 使用组合而非继承
class Database
{
private PDO $pdo;
private string $driverName;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->driverName = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
}
public function getPdo(): PDO
{
return $this->pdo;
}
public function getDriverName(): string
{
return $this->driverName;
}
}2. 连接池模式
php
<?php
// 应用层连接池 — 使用连接复用减少开销
class ConnectionPool
{
/** @var array<string, PDO> */
private static array $pool = [];
public static function get(string $key, callable $factory): PDO
{
if (!isset(self::$pool[$key])) {
self::$pool[$key] = $factory();
}
// 检查连接是否仍然有效
try {
self::$pool[$key]->query('SELECT 1');
} catch (PDOException) {
self::$pool[$key] = $factory();
}
return self::$pool[$key];
}
}3. DSN 构建器
php
<?php
class DsnBuilder
{
private string $driver;
private array $params = [];
public static function mysql(): self
{
return new self('mysql');
}
public static function pgsql(): self
{
return new self('pgsql');
}
public static function sqlite(string $path): self
{
return new self('sqlite', ['path' => $path]);
}
private function __construct(string $driver, array $params = [])
{
$this->driver = $driver;
$this->params = $params;
}
public function host(string $host): self
{
$this->params['host'] = $host;
return $this;
}
public function port(int $port): self
{
$this->params['port'] = $port;
return $this;
}
public function database(string $database): self
{
$this->params['dbname'] = $database;
return $this;
}
public function charset(string $charset): self
{
$this->params['charset'] = $charset;
return $this;
}
public function build(): string
{
$parts = [$this->driver . ':'];
foreach ($this->params as $key => $value) {
$parts[] = "{$key}={$value}";
}
return implode(';', $parts);
}
}
// 使用
$dsn = DsnBuilder::mysql()
->host('localhost')
->port(3306)
->database('app_db')
->charset('utf8mb4')
->build();
// mysql:host=localhost;port=3306;dbname=app_db;charset=utf8mb4