Upgrade to Pro — share decks privately, control downloads, hide ads and more …

WebアプリケーションにおけるPDOの使い方入門 / phpcon odawara 2024

meihei
April 13, 2024

WebアプリケーションにおけるPDOの使い方入門 / phpcon odawara 2024

meihei

April 13, 2024
Tweet

More Decks by meihei

Other Decks in Technology

Transcript

  1. 株式会社PR TIMES Backend Engineer (PHP/Python/Go) X: @app1e_s GitHub: @meihei3 Bluesky:

    @meihei.bsky.social 直近の登壇 meihei / 江間 洋平 自己紹介 2
  2. PDO (PHP Data Objects) • PDOとはデータアクセスの抽象化レイヤを提供 する、PHPの拡張モジュールです。 • クエリの発行やデータの取得が同じメソッドで 行える

    • データベースの抽象化を行うのではない ◦ DBごとの差分を吸収してくれるわけではない ◦ 同じメソッドでもDBごとで挙動が変わる事がある
  3. データベース接続で必要な情報 • dsn - Data Source Name • username /

    password - DNS接続の認証情報 • options - ドライバ固有の接続オプション
  4. // SQLiteへ接続する $dsn = 'sqlite::memory:'; $pdo = new PDO(dsn: $dsn);

    SQLiteへ接続する(インメモリ) 接続先のDSN情報
  5. // SQLiteへ接続する $dsn = 'sqlite:./phpoda.sqlite3'; $pdo = new PDO(dsn: $dsn);

    SQLiteへ接続する(ファイル) 例えばファイルに変えてみる
  6. // PostgreSQLへ接続する $dsn = 'pgsql:host=localhost;dbname=phpoda;options=\'--client_encoding=UTF8\'', $username = 'odawarakko'; $password =

    'odawarakko'; $pdo = new PDO(dsn: $dsn, username: $username, password: $password); PostgreSQLへ接続する
  7. // PostgreSQLへ接続する $dsn = 'pgsql:host=localhost;dbname=phpoda;options=\'--client_encoding=UTF8\'', $username = 'odawarakko'; $password =

    'odawarakko'; $pdo = new PDO(dsn: $dsn, username: $username, password: $password); PostgreSQLへ接続する 接続先のDSN情報 host, dbname, charsetを書く
  8. // PostgreSQLへ接続する $dsn = 'pgsql:host=localhost;dbname=phpoda;options=\'--client_encoding=UTF8\'', $username = 'odawarakko'; $password =

    'odawarakko'; $pdo = new PDO(dsn: $dsn, username: $username, password: $password); PostgreSQLへ接続する DSNへ接続するための認証情報
  9. // PostgreSQLへ接続する $dsn = 'pgsql:host=localhost;dbname=phpoda;options=\'--client_encoding=UTF8\'', $username = 'odawarakko'; $password =

    'odawarakko'; $pdo = new PDO(dsn: $dsn, username: $username, password: $password); PostgreSQLへ接続する
  10. // MySQLへ接続する $dsn = 'mysql:host=localhost;dbname=phpoda;charset=utf8mb4', $username = 'odawarakko'; $password =

    'odawarakko'; $options = [ PDO::ATTR_EMULATE_PREPARES => false, ]; $pdo = new PDO(dsn: $dsn, username: $username, password: $password, options: $options); MySQLへ接続する
  11. // MySQLへ接続する $dsn = 'mysql:host=localhost;dbname=phpoda;charset=utf8mb4', $username = 'odawarakko'; $password =

    'odawarakko'; $options = [ PDO::ATTR_EMULATE_PREPARES => false, ]; $pdo = new PDO(dsn: $dsn, username: $username, password: $password, options: $options); MySQLへ接続する charsetはutf8mb4を指定する
  12. // MySQLへ接続する $dsn = 'mysql:host=localhost;dbname=phpoda;charset=utf8mb4', $username = 'odawarakko'; $password =

    'odawarakko'; $options = [ PDO::ATTR_EMULATE_PREPARES => false, ]; $pdo = new PDO(dsn: $dsn, username: $username, password: $password, options: $options); MySQLへ接続する ドライバオプションを書く
  13. // MySQLへ接続する $dsn = 'mysql:host=localhost;dbname=phpoda;charset=utf8mb4', $username = 'odawarakko'; $password =

    'odawarakko'; $options = [ PDO::ATTR_EMULATE_PREPARES => false, ]; $pdo = new PDO(dsn: $dsn, username: $username, password: $password, options: $options); MySQLへ接続する MySQLの場合、エミュレート モードをOFFが推奨
  14. // MySQLへ接続する $dsn = 'mysql:host=localhost;dbname=phpoda;charset=utf8mb4', $username = 'odawarakko'; $password =

    'odawarakko'; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, ]; $pdo = new PDO($dsn, $username, $password, $options); PHP8.0未満はエラーモードに注意
  15. // MySQLへ接続する $dsn = 'mysql:host=localhost;dbname=phpoda;charset=utf8mb4', $username = 'odawarakko'; $password =

    'odawarakko'; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, ]; $pdo = new PDO($dsn, $username, $password, $options); PHP8.0未満はエラーモードに注意 デフォルトでERRMODE_SILENTなの で、ちゃんと指定する必要がある
  16. // users テーブルから全件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->query('SELECT * FROM users'); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 全件取得
  17. // users テーブルから全件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->query('SELECT * FROM users'); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 全件取得 データベースへ接続
  18. // users テーブルから全件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->query('SELECT * FROM users'); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 全件取得 SQLを発行
  19. // users テーブルから全件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->query('SELECT * FROM users'); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 全件取得 結果を連想配列の形で取得
  20. // users テーブルから全件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->query('SELECT * FROM users'); $data = []; while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $data[] = $row; } 全件取得 1件ずつ取得する 書き方も出来る
  21. // users テーブルから1件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->bindValue(1, $id, PDO::PARAM_INT); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); 変数を指定して1件取得
  22. // users テーブルから1件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->bindValue(1, $id, PDO::PARAM_INT); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); 変数を指定して1件取得 SQLに直接変数を入れず?に 置き換えている(プレースホルダ)
  23. // users テーブルから1件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->bindValue(1, $id, PDO::PARAM_INT); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); 変数を指定して1件取得 ここで値をバインドしている
  24. // users テーブルから1件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->bindValue(1, $id, PDO::PARAM_INT); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); 変数を指定して1件取得 Executeのタイミングで実行
  25. // users テーブルから1件取得 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); 変数を指定して1件取得 プレースホルダは名前を つける事が可能
  26. // users テーブルに1件挿入 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)'); $stmt->bindValue(1, $user->name, PDO::PARAM_STR); $stmt->bindValue(2, $user->email, PDO::PARAM_STR); $stmt->execute(); 1件挿入
  27. // users テーブルに1件挿入 $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)'); $stmt->bindValue(1, $user->name, PDO::PARAM_STR); $stmt->bindValue(2, $user->email, PDO::PARAM_STR); $stmt->execute(); 1件挿入
  28. // users テーブルの1件を更新(idはPK) $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare( 'UPDATE users SET name = :name, email = :email WHERE id = :id'); $stmt->bindValue(':name', $user->name, PDO::PARAM_STR); $stmt->bindValue(':email', $user->email, PDO::PARAM_STR); $stmt->bindValue(':id', $user->id, PDO::PARAM_INT); $stmt->execute(); 1件更新
  29. // users テーブルの1件を更新(idはPK) $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare( 'UPDATE users SET name = :name, email = :email WHERE id = :id'); $stmt->bindValue(':name', $user->name, PDO::PARAM_STR); $stmt->bindValue(':email', $user->email, PDO::PARAM_STR); $stmt->bindValue(':id', $user->id, PDO::PARAM_INT); $stmt->execute(); 1件更新
  30. // users テーブルの1件を更新(idはPK) $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare( 'UPDATE users SET name = :name, email = :email WHERE id = :id'); $stmt->bindValue(':name', $user->name, PDO::PARAM_STR); $stmt->bindValue(':email', $user->email, PDO::PARAM_STR); $stmt->bindValue(':id', $user->id, PDO::PARAM_INT); $stmt->execute(); if (($rowCount = $stmt->rowCount()) > 1) { throw new \RuntimeException( "Expected to update 1 row, but updated {$rowCount} rows"); } 1件更新(例外を投げる) rowCountで実際に作用した行数 を取得し、期待通りであるか確認
  31. // users テーブルの1件を削除(idはPK) $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('DELETE FROM users WHERE id = ?'); $stmt->bindValue(1, $user->id, PDO::PARAM_INT); $stmt->execute(); if (($rowCount = $stmt->rowCount()) > 1) { throw new \RuntimeException( "Expected to delete 1 row, but deleted {$rowCount} rows"); } 1件削除
  32. // users テーブルの1件を削除(idはPK) $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('DELETE FROM users WHERE id = ?'); $stmt->bindValue(1, $user->id, PDO::PARAM_INT); $stmt->execute(); if (($rowCount = $stmt->rowCount()) > 1) { throw new \RuntimeException( "Expected to delete 1 row, but deleted {$rowCount} rows"); } 1件削除
  33. // users テーブルを更新し、 user_name_log に挿入する。 $pdo->beginTransaction(); try { $stmt1 =

    $pdo->prepare('UPDATE users SET name = :name WHERE id = :id'); ... $stmt1->execute(); $stmt2 = $pdo->prepare( 'INSERT INTO user_name_log (user_id, name) VALUES (:user_id, :name)'); ... $stmt2->execute(); return $pdo->commit(); } catch (\PDOException $e) { $pdo->rollBack(); throw $e; } トランザクションを利用する
  34. // users テーブルを更新し、 user_name_log に挿入する。 $pdo->beginTransaction(); try { $stmt1 =

    $pdo->prepare('UPDATE users SET name = :name WHERE id = :id'); ... $stmt1->execute(); $stmt2 = $pdo->prepare( 'INSERT INTO user_name_log (user_id, name) VALUES (:user_id, :name)'); ... $stmt2->execute(); return $pdo->commit(); } catch (\PDOException $e) { $pdo->rollBack(); throw $e; } トランザクションを利用する トランザクションは複数のSQLを まとめて安全に反映すること を保証する(ACID)
  35. // users テーブルを更新し、 user_name_log に挿入する。 $pdo->beginTransaction(); try { $stmt1 =

    $pdo->prepare('UPDATE users SET name = :name WHERE id = :id'); ... $stmt1->execute(); $stmt2 = $pdo->prepare( 'INSERT INTO user_name_log (user_id, name) VALUES (:user_id, :name)'); ... $stmt2->execute(); return $pdo->commit(); } catch (\PDOException $e) { $pdo->rollBack(); throw $e; } トランザクションを利用する トランザクションを初期化 トランザクション内の操作を 反映させる トランザクション内の操作を キャンセルし、元の状態に戻す
  36. // users テーブルの1件を削除(idはPK) $pdo = new PDO($dsn, $username, $password, $opt);

    $stmt = $pdo->prepare('DELETE FROM users WHERE id = ?'); $stmt->bindValue(1, $user->id, PDO::PARAM_INT); $stmt->execute(); if (($rowCount = $stmt->rowCount()) > 1) { throw new \RuntimeException( "Expected to delete 1 row, but deleted {$rowCount} rows"); } ちなみに、通常時は自動コミットモード (もしDBがトランザクションをサポートしていたら) 暗黙的なトランザクションのもと実行される
  37. $pdo = new PDO($dsn, $username, $password, $opt); $stmt = $pdo->query(

    "SELECT * FROM users WHERE id = {$id} LIMIT" . $limit); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 🙅 SQLインジェクションの危険性があるコード
  38. $pdo = new PDO($dsn, $username, $password, $opt); $stmt = $pdo->query(

    "SELECT * FROM users WHERE id = {$id} LIMIT" . $limit); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); SQL内で変数を展開している 🙅 SQLインジェクションの危険性があるコード
  39. $pdo = new PDO($dsn, $username, $password, $opt); $stmt = $pdo->query(

    "SELECT * FROM users WHERE id = {$id} LIMIT" . $limit); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 🙅 SQLインジェクションの危険性があるコード 文字列連結でSQL文 を組み立てている
  40. SQLインジェクションを防ぐ • 文字列連結でSQL文を組み立てない ◦ 文字列リテラルを使う • 値はプレースホルダで渡す • (静的プレースホルダを使う) ◦

    ATTR_EMULATE_PREPARESの設定が出来る OCI, Firebird, MySQL はで、Falseを設定する PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能 https://blog.tokumaru.org/2013/12/pdo-and-mysql-allow-multiple-statements.html 安全なウェブサイトの作り方 https://www.ipa.go.jp/security/vuln/websecurity/index.html
  41. // MySQLへ接続する $pdo = new PDO($dsn, $username, $password, $opt); //

    注意: OCI, Firebird, MySQL でのみ利用可能なオプション $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id LIMIT :limit'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 🙆 SQLインジェクションの対策がされたコード
  42. 🙅 好ましくないRepositoryの実装 class UserRepository implements UserRepositoryInterface { ... public function

    getById( int $id, ): mixed { $stmt = $this->pdo->prepare( 'SELECT * FROM users WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetch(PDO::FETCH_ASSOC); } }
  43. 🙅 好ましくないRepositoryの実装 class UserRepository implements UserRepositoryInterface { ... public function

    getById( int $id, ): mixed { $stmt = $this->pdo->prepare( 'SELECT * FROM users WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetch(PDO::FETCH_ASSOC); } } SELECT * はデータの 無限の可能性を持つ
  44. 🙅 好ましくないRepositoryの実装 class UserRepository implements UserRepositoryInterface { ... public function

    getById( int $id, ): mixed { $stmt = $this->pdo->prepare( 'SELECT * FROM users WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetch(PDO::FETCH_ASSOC); } } キー(プロパティ)の 情報が無い =静的解析が効かない
  45. どう辛いのか $repo = new UserRepository($pdo); $user = $repo->getById($_GET['id'] ?? 0);

    if ($user === false) { // 404 Not Found } echo json_encode([ 'user_id' => $user['id'], 'user_name' => $user['meme'], ... ]);
  46. どう辛いのか $repo = new UserRepository($pdo); $user = $repo->getById($_GET['id'] ?? 0);

    if ($user === false) { // 404 Not Found } echo json_encode([ 'user_id' => $user['id'], 'user_name' => $user['meme'], ... ]); user_nameってKeyあったかな? user_idはINT型にキャスト したほうがいいかな? なぜかここでテストがFailするぞ!?
  47. 連想配列の解像度 // 連想配列② $user = [ 'id' => ..., 'name'

    => ..., 'email' => ..., ]; // カラムを知っている人 から見たuserの連想配列 // 連想配列③ $user = [...]; // 何も知らない人か ら見たuserの連想配 列 // 連想配列① $user = [ 'id' => 100, 'name' => 'odawara', 'email' => '[email protected]', ]; // データを知っている人から見たuser の連想配列
  48. 連想配列に旅をさせない • PDOの返り値は配列(連想配列) ◦ 連想配列は動的(unset, array_push, ...) ◦ 静的解析が効かない •

    DTO(Data Transfer Object)を使う ◦ POPO(Plain Old PHP Object)で型の情報をクラス で表現する ▪ ライブラリなどに依存していない質素なオブジェクト ◦ メソッドの内から連想配列をDTOへ変換 ※1 FETCH MODEによっては連想配列ではない返り値を得られる。 ※2 PHPの最高機能、配列を捨てよう!!を参考に https://speakerdeck.com/uzulla/throw-away-all-php-array-now
  49. 🙆 DTOに詰め替えて、型を定義する。 // 連想配列 $user = [ 'id' => ...,

    'name' => ..., 'email' => ..., ]; // DTO class UserDto { public function __construct( public int $id, public string $name, public string $email, ) {} }
  50. 🙆 DTOに詰め替えて、型を定義する。 class UserRepository implements UserRepositoryInterface { ... public function

    getById( int $id, ): ?UserDto { $stmt = $this->pdo->prepare( 'SELECT id, name, email FROM users WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row !== false ? new UserDto(...$row) : null; } }
  51. 🙆 DTOに詰め替えて、型を定義する。 class UserRepository implements UserRepositoryInterface { ... public function

    getById( int $id, ): ?UserDto { $stmt = $this->pdo->prepare( 'SELECT id, name, email FROM users WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row !== false ? new UserDto(...$row) : null; } } 取りうるデータ (カラム)を制限する
  52. 🙆 DTOに詰め替えて、型を定義する。 class UserRepository implements UserRepositoryInterface { ... public function

    getById( int $id, ): ?UserDto { $stmt = $this->pdo->prepare( 'SELECT id, name, email FROM users WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row !== false ? new UserDto(...$row) : null; } } Repository内で DTOに詰め替える
  53. 私たちの答え:TetoSQL • PDO wrapper and SQL Template for PHP ◦

    https://github.com/BaguettePHP/TetoSQL • PDOへ引き渡すためのSQLを生成するテンプレート として活用 ◦ 可変個の配列使ったSQLが書ける ◦ FORやIFなどの制御構文も使える • 更に型チェックもある 型安全なSQLテンプレートエンジンを構築する  https://fortee.jp/phpcon-2023/proposal/5717d4a6-ebf0-47cb-9964-69cfb92e0f56
  54. TetoSQLで使える型・制御構文 • @bool • @int • @int[] • @string •

    @string[] • @lob • @ascdesc • %if ... %endif • %for ... %endfor
  55. TetoSQLを使ってSQLを実行 $pdo = new PDO($dsn, $username, $password, $opt); $stmt =

    \Teto\SQL\Query::execute( $pdo, 'SELECT * FROM users WHERE id IN (:ids@int[]) ', [ ':ids' => $ids, // [1, 2, 3] ] ); $data = $stmt->fetch(PDO::FETCH_ASSOC);
  56. TetoSQLを使ってSQLを実行 $pdo = new PDO($dsn, $username, $password, $opt); $stmt =

    \Teto\SQL\Query::execute( $pdo, 'SELECT * FROM users WHERE id IN (:ids@int[])', [ ':ids' => $ids, // [1, 2, 3] ] ); $data = $stmt->fetch(PDO::FETCH_ASSOC); 配列を引数にとって 値をバインド出来る
  57. TetoSQLを使ってSQLを実行 $pdo = new PDO($dsn, $username, $password, $opt); $stmt =

    \Teto\SQL\Query::execute( $pdo, "SELECT * FROM {$tablename} WHERE id IN (:ids@int[])", [ ':ids' => $ids, // [1, 2, 3] ] ); $data = $stmt->fetch(PDO::FETCH_ASSOC); とは言え、まだ脆弱なコード を書くことが出来る
  58. • PHPStanは抽象構文木に解析する ◦ カスタムルールはTetoSQLの引数を対象に作成するこ とができる。 • TetoSQLへの入力は文字列リテラルのみに制限する ◦ シングルクォートの文字列、Nowdocのみ ◦

    文字列結合や変数展開が出来なくなる TetoSQL 用の PHPStan のカスタムルール PHPStanのカスタムルールを導入しました  https://developers.prtimes.jp/2024/02/21/introduce_phpstan_custom_rule/
  59. PHPStanのカスタムルール class SqlRule implements Rule { public function processNode(Node $node,

    Scope $scope): array { ... if (!$node->args[1]->value instanceof Node\Scalar\String_) { return ['クエリは文字列リテラルである必要があります。']; } $attributes = $node->args[1]->value->getAttributes(); if (!in_array($attributes['kind'], [String_::KIND_SINGLE_QUOTED, String_::KIND_NOWDOC])) { return ['クエリはシングルクォートかNowdocで記述する必要があります。']; } return []; } ... }
  60. 参考文献 • 安全なウェブサイトの作り方 ◦ https://www.ipa.go.jp/security/vuln/websecurity/index.html • PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び 出しが可能 ◦ https://blog.tokumaru.org/2013/12/pdo-and-mysql-allow-multiple

    -statements.html • PHPでデータベースに接続するときのまとめ ◦ https://qiita.com/mpyw/items/b00b72c5c95aac573b71 • 憂鬱なSQLのためのアレ、またはPDOと仲良くして枕を高くしてねむる ◦ https://qiita.com/tadsan/items/e615a779baa6eabdab47
  61. 余談:PHP8.4でドライバごとのサブクラスが出来る • PHP RFC: PDO driver specific sub-classes https://wiki.php.net/rfc/pdo_driver_specific_subclasses •

    PdoSqlite, PdoPgsql, PdoMySql • PDO::connect() • PHP 8.3 のプロポーザルっぽいが、Implementation は PHP 8.4 となっていた