Skip to content

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()
连接方式构造函数静态工厂方法
返回类型PDOPDO 或自定义子类
驱动获取不可用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

参考链接