【PHP8.3】PDOでデータベース固有機能が使えるようになる

[PDO](https://www.php.net/manual/ja/book.pdo.php)は汎用データベースドライバです。

```php
// MySQL
$pdo = new PDO('mysql:host=localhost;dbname=test');

// PostgreSQL
$pdo = new PDO('pgsql:host=localhost;dbname=test');

// SQLite
$pdo = new PDO('sqlite:/path/to/sqlite_db.db');

// 以後は同じように使える
$data = $pdo->query('SELECT * FROM table');
```

接続先のDBがどんな種類であろうと、DSNを変えるだけで全く同じに書くことができます。
素晴らしく便利ですね。

逆にいうと汎用ドライバであるため、それぞれのDBに固有の機能とかは使いづらいです。
そういうのはわざわざ[固有モジュール](https://www.php.net/manual/ja/refs.database.vendors.php)を使わなければならず大変でした。
ということでPDOでもサブクラスを作って対応しようというRFCが提出されました。

```php
// MySQL固有機能が使える
$pdo = new PdoMySql('mysql:host=localhost;dbname=test');
$pdo->getWarningCount();

// PostgreSQL固有機能が使える
$pdo = new PdoPgsql('pgsql:host=localhost;dbname=test');
$pdo->escapeIdentifier($name);

// SQLite固有機能が使える
$pdo = new PdoSqlite('sqlite:/path/to/sqlite_db.db');
$pdo->createCollation($collection);
```

これまでどおり`new PDO()`も使用可能であり、その場合はこれまでと何ひとつ変わりません。
あくまでDB固有機能を使いたい場合にサブクラスを生成することができるという機能です。

既に受理されており、PHP8.3から使用可能です。

以下は該当のRFC、[PDO driver specific sub-classes](https://wiki.php.net/rfc/pdo_driver_specific_subclasses)の紹介です。

# PHP RFC: PDO driver specific sub-classes

## Introduction

PDOは汎用データベースクラスです。
サポートしているデータベースの中には、固有の機能を持つものもあります。
たとえばSQLiteに接続すると、[PDO::sqliteCreateFunction()](https://www.php.net/manual/ja/pdo.sqlitecreatefunction.php)というメソッドが使えるようになります。

しかし、どのデータベースに接続したかによってメソッドが使えたり使えなかったりするのは、非常に不自然です。

PDOのサブクラスとして、それぞれのデータベースに特化したメソッドを用意する方が、コードの見通しがよくなるでしょう。

## Proposal

本RFCは、2つの提案からなっています。

・PDOに、ドライバ固有の機能を使えるサブクラスを追加する。
・ファクトリーメソッド`PDO::connect()`を追加する。

### Add new subclasses of PDO

PHPに含まれるすべてのPDO拡張モジュールに、それぞれのサブクラスを追加します。
たとえば`sqliteCreateFunction()`は、`PDO::sqliteCreateFunction()`にあるべきではなく、`PdoSqlite::createFunction()`にあるべきです。

```php
class PdoDblib extends PDO
{}

class PdoFirebird extends PDO
{}

class PdoMySql extends PDO
{
    public function getWarningCount(): int {}
}

class PdoOci extends PDO
{}

class PdoOdbc extends PDO
{}

class PdoPgsql extends PDO
{
    /**
     * @var int
     * @cname PDO_PGSQL_ATTR_DISABLE_PREPARES
     */
    public const ATTR_DISABLE_PREPARES = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_IDLE
     */
    public const TRANSACTION_IDLE = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_ACTIVE
     */
    public const TRANSACTION_ACTIVE = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_INTRANS
     */
    public const TRANSACTION_INTRANS = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_INERROR
     */
    public const TRANSACTION_INERROR = UNKNOWN;
 
    /**
     * @var int
     * @cname PGSQL_TRANSACTION_UNKNOWN
     */
    public const TRANSACTION_UNKNOWN = UNKNOWN;
 
    public function escapeIdentifier(string $input): string {}
 
    public function copyFromArray(string $tableName, array $rows, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
 
    public function copyFromFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
 
    public function copyToArray(string $tableName, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): array|false {}
 
    public function copyToFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
 
    public function lobCreate(): string|false {}
 
    // Opens an existing large object stream.  Must be called inside a transaction.
    /** @return resource|false */
    public function lobOpen(string $oid, string $mode = "rb"){}
 
    public function lobUnlink(string $oid): bool {}
 
    public function getNotify(int $fetchMode = PDO::FETCH_USE_DEFAULT, int $timeoutMilliseconds = 0): array|false {}
 
    public function getPid(): int {}
}


class PdoSqlite extends PDO
{
    /**
     * @var int
     * @cname SQLITE_DETERMINISTIC
     */
    public const DETERMINISTIC = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_ATTR_OPEN_FLAGS
     */
    public const ATTR_OPEN_FLAGS = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_OPEN_READONLY
     */
    public const OPEN_READONLY = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_OPEN_READWRITE
     */
    public const OPEN_READWRITE = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_OPEN_CREATE
     */
    public const OPEN_CREATE = UNKNOWN;
 
    /**
     * @var int
     * @cname SQLITE_ATTR_READONLY_STATEMENT
     */
    public const ATTR_READONLY_STATEMENT = UNKNOWN;
 
    /**
     * @var int
     * @cname
     */
    public const ATTR_EXTENDED_RESULT_CODES = UNKNOWN;
 
    // 集計関数をユーザ定義
    public function createAggregate(
        string $name,
        callable $step,
        callable $finalize,
        int $numArgs = -1
    ): bool {}
 
    // 照合関数をユーザ定義
    public function createCollation(string $name, callable $callback): bool {}
 
    public function createFunction(
        string $function_name,
        callable $callback,
        int $num_args = -1,
        int $flags = 0
    ): bool {}
 
// コンパイル方法によってSQLITE_OMIT_LOAD_EXTENSIONが定義されているか否かが変わる
#ifndef SQLITE_OMIT_LOAD_EXTENSION
    public function loadExtension(string $name): bool {}
#endif

    public function openBlob(
        string $table,
        string $column,
        int $rowid,
        ?string $dbname = "main", //null,
        int $flags = PdoSqlite::OPEN_READONLY
    ): mixed /* resource|false */ {}
}
```

DB固有の機能は他にもあるかもしれませんが、このRFCでは対象外とします。


### Add a way of creating them through PDO static factory method

静的ファクトリーメソッド`PDO::connect()`を追加します。
DSNから接続先データベースの種類を正確にチェックし、対象が存在する場合は該当のサブクラスを返します。

```php
class PDO
{
    // connectのPHP擬似コード
    public static function connect(string $dsn [, string $username [, string $password [, array $options ]]]) {
        // SQLiteに接続しようとしていたらPdoSqliteを返す
        if (connecting to SQLite DB) {
            return new PdoSqlite(...);
        }
        /* 中略 */
 
        // いずれでもなければPDOを返す
        return new PDO(...);
    }
}
```

`PDO::connect()`は、特定のデータベースに接続する場合に適切なサブクラスを返します。

もしくは、直接サブクラスを生成することも可能です。
ただし接続先データベースが、そのクラスに適したものでない場合は例外が発生します。

```php
$db = new PdoSqlite($dsn, $username, $password, $options);
```

## Backward Incompatible Changes

後方互換性のない変更はありません。

PDOを直接extendsしてDB固有機能を機能を追加していた人にとっては、少々不便かもしれません。


## Proposed PHP Version(s)

PHP8.3

## Unaffected PHP Functionality

PDO以外のPHP機能に影響はありません。

## Frequently asked questions

よくある質問。

### if someone does 'new PDO(...)' will they now get back 'PdoPgsql'

Q: `new PDO(...)`と書いたときに`PdoPgsql`が返ってくることはありますか？

A: ありません。

## Future Scope

この項目は将来の展望であり、このRFCには含まれません。

### When to deprecate old function on PDO

既存の[PDO::sqliteCreateFunction](https://www.php.net/manual/ja/pdo.sqlitecreatefunction.php)などドライバ固有メソッドはいずれ削除されるべきですが、優先度は高くありません。
ドライバによって存在したりしなかったりするメソッドを削除することで、PDOのコードを整理することができます。
ユーザへのメリットはあまりありませんが、メンテナンスの複雑さを軽減できるかもしれません。

### Quoting identifiers

ディスカッションにおいて、現在のPDOには`識別子をエスケープする`機能がないという指摘がありました。
少なくともPostgresには識別子をエスケープする機能があり、PDOクラスにエスケープメソッドを追加する価値はありそうです。
この機能は、あらゆるデータベースドライバに精通した人が行う必要があるでしょう。

### SQLite constants

SQLite3拡張には、現在定義されていない定数が3個存在します。

・SQLITE_DIRECTONLY
・SQLITE_INNOCUOUS
・SQLITE_SUBTYPE

本RFCでは対象外です。

### PdoSqlite aggregations, collations and functions

PdoSqliteのコードはSQLite3拡張からコピーしたものです。
SQLiteではデータの文字コードを表すフラグが存在しますが、SQLite3拡張ではSQLITE_UTF8がハードコードされていました。

SQLITE_UTF16・SQLITE_UTF16BE・SQLITE_UTF16LE・SQLITE_UTF16_ALIGNED・SQLITE_UTF8などのフラグを公開・指定可能にすることは可能だと思われますが、本RFCでは対象外です。


## Proposed Voting Choices

投票期間は2023/07/03～2023/07/17、投票の2/3の賛成で可決されます。

本RFCは賛成23反対0の全員賛成で受理されました。

## Patches and Tests

[https://github.com/php/php-src/pull/8707](https://github.com/php/php-src/pull/8707)


## References

かつて[SQLiteのopenBlob機能を追加するRFC](https://wiki.php.net/rfc/implement_sqlite_openblob_in_pdo)が存在したが却下された。
その際、サブクラスのアプローチのほうが受け入れられそうだとの感触があった。


# 感想

どうして今まで存在しなかったのだろう、と思い返すくらい便利そうですねこれ。
私は主にMySQLを使っているので、MySQL固有機能というのはあまり存在しないのですが、PostgresやSQLiteで固有機能を使いたかった人などには朗報でしょう。
あるいは今後CassandraやRedisなどのNoSQLに対応したドライバが出てきたりすると、もっと楽しくなるかもしれませんね。

唯一の問題点としてはDB間の移植が困難になることですが、どうせDBの移植なんて滅多にないし別にかまわないでしょう。