Slide 1

Slide 1 text

Webアプリケーション におけるPDOの使い方入門 PHPカンファレンス小田原2024

Slide 2

Slide 2 text

株式会社PR TIMES Backend Engineer (PHP/Python/Go) X: @app1e_s GitHub: @meihei3 Bluesky: @meihei.bsky.social 直近の登壇 meihei / 江間 洋平 自己紹介 2

Slide 3

Slide 3 text

※1 2023年11月自社調べ。「利用企業社数」とは、過去にサービスを利用した実績がある社数の累計を指します。国内主要プレスリリース配信サービス5社を比較。他社データは、2023年11月時点で開示している情報より取得。 ※2 2023年8月実績。「サイト閲覧数」とは、配信されたプレスリリースが表示されるサイトにおける、月間のサイト閲覧数を指します。国内主要プレスリリース配信サービス5社を比較。サイト閲覧数は、自社含めて、Similarwebを用いて推定、比較を実施。 ※3 2023年11月自社調べ。「提携メディア数」とは、登録したプレスリリース情報を原文のまま掲載することが可能なメディアの件数を指します。国内主要プレスリリース配信サービス5社を比較、他社データは、2023年11月時点で開示している情報より取得。

Slide 4

Slide 4 text

私と小田原 実家から今住んでいるマン ションまで、小田急を使っ た移動が一番安い。 乗り換えで何度か小田原に 降りていた。 ※画像はYahoo!マップより

Slide 5

Slide 5 text

本日のゴール PDOの基本的な使い方を知って 実際のWebアプリケーションで使われているような PDOの使い方・Repositoryの実装の仕方を学ぼう

Slide 6

Slide 6 text

アジェンダ 1. PDOを触ってみよう 2. Webアプリケーションの中のPDO 3. まとめ

Slide 7

Slide 7 text

アジェンダ 1. PDOを触ってみよう 2. Webアプリケーションの中のPDO 3. まとめ

Slide 8

Slide 8 text

PDOとは

Slide 9

Slide 9 text

PDO (PHP Data Objects) ● PDOとはデータアクセスの抽象化レイヤを提供 する、PHPの拡張モジュールです。 ● クエリの発行やデータの取得が同じメソッドで 行える ● データベースの抽象化を行うのではない ○ DBごとの差分を吸収してくれるわけではない ○ 同じメソッドでもDBごとで挙動が変わる事がある

Slide 10

Slide 10 text

データアクセスの抽象化 共通のインターフェース が用意されている SQLはDB特有の ものが必要

Slide 11

Slide 11 text

LaravelのEloquentと、どう違うのか ● EloquentはORM(Object-Relational Mapping) ○ オブジェクト指向なメソッド呼び出しから、SQL発行 を自動的に生成する ■ データベースの抽象化を行っていて、基本的にはデー タベースを意識したコードを書かなくても良い ● 対してPDOは、SQLを自分で書く ● 必要最低限の機能しかなく、軽量に動く

Slide 12

Slide 12 text

データベースへ接続する

Slide 13

Slide 13 text

データベース接続で必要な情報 ● dsn - Data Source Name ● username / password - DNS接続の認証情報 ● options - ドライバ固有の接続オプション

Slide 14

Slide 14 text

// SQLiteへ接続する $dsn = 'sqlite::memory:'; $pdo = new PDO(dsn: $dsn); SQLiteへ接続する(インメモリ)

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

// SQLiteへ接続する $dsn = 'sqlite:./phpoda.sqlite3'; $pdo = new PDO(dsn: $dsn); SQLiteへ接続する(ファイル) 例えばファイルに変えてみる

Slide 17

Slide 17 text

// 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へ接続する

Slide 18

Slide 18 text

// 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を書く

Slide 19

Slide 19 text

// 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へ接続するための認証情報

Slide 20

Slide 20 text

// 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へ接続する

Slide 21

Slide 21 text

// 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へ接続する

Slide 22

Slide 22 text

// 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を指定する

Slide 23

Slide 23 text

// 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へ接続する ドライバオプションを書く

Slide 24

Slide 24 text

// 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が推奨

Slide 25

Slide 25 text

// 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未満はエラーモードに注意

Slide 26

Slide 26 text

// 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なの で、ちゃんと指定する必要がある

Slide 27

Slide 27 text

CRUD処理を書いてみよう

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

// 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件ずつ取得する 書き方も出来る

Slide 33

Slide 33 text

// 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件取得

Slide 34

Slide 34 text

// 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に直接変数を入れず?に 置き換えている(プレースホルダ)

Slide 35

Slide 35 text

// 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件取得 ここで値をバインドしている

Slide 36

Slide 36 text

// 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のタイミングで実行

Slide 37

Slide 37 text

// 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件取得 プレースホルダは名前を つける事が可能

Slide 38

Slide 38 text

// 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件挿入

Slide 39

Slide 39 text

// 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件挿入

Slide 40

Slide 40 text

// 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件更新

Slide 41

Slide 41 text

// 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件更新

Slide 42

Slide 42 text

// 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で実際に作用した行数 を取得し、期待通りであるか確認

Slide 43

Slide 43 text

rowCountはドライバ依存 ● PDOStatement::rowCount — 直近の SQL ステート メントによって作用した行数を返す ●

Slide 44

Slide 44 text

// 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件削除

Slide 45

Slide 45 text

// 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件削除

Slide 46

Slide 46 text

// 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; } トランザクションを利用する

Slide 47

Slide 47 text

// 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)

Slide 48

Slide 48 text

// 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; } トランザクションを利用する トランザクションを初期化 トランザクション内の操作を 反映させる トランザクション内の操作を キャンセルし、元の状態に戻す

Slide 49

Slide 49 text

// 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がトランザクションをサポートしていたら) 暗黙的なトランザクションのもと実行される

Slide 50

Slide 50 text

アジェンダ 1. PDOを触ってみよう 2. Webアプリケーションの中のPDO 3. まとめ

Slide 51

Slide 51 text

安全にPDOをつかう

Slide 52

Slide 52 text

$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インジェクションの危険性があるコード

Slide 53

Slide 53 text

$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インジェクションの危険性があるコード

Slide 54

Slide 54 text

$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文 を組み立てている

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

// 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インジェクションの対策がされたコード

Slide 57

Slide 57 text

配列の可能性を収束させる

Slide 58

Slide 58 text

🙅 好ましくない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); } }

Slide 59

Slide 59 text

🙅 好ましくない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 * はデータの 無限の可能性を持つ

Slide 60

Slide 60 text

🙅 好ましくない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); } } キー(プロパティ)の 情報が無い =静的解析が効かない

Slide 61

Slide 61 text

どう辛いのか $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'], ... ]);

Slide 62

Slide 62 text

どう辛いのか $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するぞ!?

Slide 63

Slide 63 text

連想配列の解像度 // 連想配列② $user = [ 'id' => ..., 'name' => ..., 'email' => ..., ]; // カラムを知っている人 から見たuserの連想配列 // 連想配列③ $user = [...]; // 何も知らない人か ら見たuserの連想配 列 // 連想配列① $user = [ 'id' => 100, 'name' => 'odawara', 'email' => '[email protected]', ]; // データを知っている人から見たuser の連想配列

Slide 64

Slide 64 text

連想配列に旅をさせない ● 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

Slide 65

Slide 65 text

🙆 DTOに詰め替えて、型を定義する。 // 連想配列 $user = [ 'id' => ..., 'name' => ..., 'email' => ..., ]; // DTO class UserDto { public function __construct( public int $id, public string $name, public string $email, ) {} }

Slide 66

Slide 66 text

🙆 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; } }

Slide 67

Slide 67 text

🙆 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; } } 取りうるデータ (カラム)を制限する

Slide 68

Slide 68 text

🙆 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に詰め替える

Slide 69

Slide 69 text

PDOの表現力を上げる

Slide 70

Slide 70 text

PDOが苦手な事 ● IN句などの動的に複数のパラメータを渡したい時 ● テーブル名を安全に渡したい時 ● ORDER BYのASC/DESC

Slide 71

Slide 71 text

PDOが苦手な事 ● IN句などの動的に複数のパラメータを渡したい時 ● テーブル名を安全に渡したい時 ● ORDER BYのASC/DESC 面倒だし、文字列結合した SQLを使おうかな…

Slide 72

Slide 72 text

私たちの答え: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

Slide 73

Slide 73 text

TetoSQLで使える型・制御構文 ● @bool ● @int ● @int[] ● @string ● @string[] ● @lob ● @ascdesc ● %if ... %endif ● %for ... %endfor

Slide 74

Slide 74 text

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);

Slide 75

Slide 75 text

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); 配列を引数にとって 値をバインド出来る

Slide 76

Slide 76 text

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); とは言え、まだ脆弱なコード を書くことが出来る

Slide 77

Slide 77 text

TetoSQL と PHPStan の合せ技をする ● TetoSQLへの入力が文字列である限りは、変数の展 開や文字列結合が可能 ○ そもそも、TetoSQLはSQLインジェクションを防ぐた めの機能を提供しているわけではない ● 静的解析とCIで、脆弱なコードが混入出来ない仕組み を作る ○ PHPStanのカスタムルールで実現

Slide 78

Slide 78 text

● PHPStanは抽象構文木に解析する ○ カスタムルールはTetoSQLの引数を対象に作成するこ とができる。 ● TetoSQLへの入力は文字列リテラルのみに制限する ○ シングルクォートの文字列、Nowdocのみ ○ 文字列結合や変数展開が出来なくなる TetoSQL 用の PHPStan のカスタムルール PHPStanのカスタムルールを導入しました  https://developers.prtimes.jp/2024/02/21/introduce_phpstan_custom_rule/

Slide 79

Slide 79 text

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 []; } ... }

Slide 80

Slide 80 text

アジェンダ 1. PDOを触ってみよう 2. Webアプリケーションの中のPDO 3. まとめ

Slide 81

Slide 81 text

まとめ ● PDOの基本的な使い方(CRUD)を学びました ● プリペアドステートメントを必ず使用して、SQLイ ンジェクション対策をする ● Repositoryの実装は連想配列を外に出さないように する ● PDOだけでは出来ないことは、SQL Templateなライ ブラリを使うことも検討。PHPStanも使うと効果的

Slide 82

Slide 82 text

参考文献 ● 安全なウェブサイトの作り方 ○ 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

Slide 83

Slide 83 text

余談:もっと色々な実装を見てみたい場合 ● ISUCONの問題を見てみよう ○ PHPはPDOで実装されている ○ 多言語も同じSQLを書くので、比較しながら読める ● PDO, Eloquent, DoctrineORM の比較(SELECT) ○ https://blog.okashoi.net/entry/2023/12/09/060000 ○ ISUCONのPHP移植の実装者の記事

Slide 84

Slide 84 text

余談: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 となっていた