PDO::prepare() / execute()
| 対応: | PHP 5(2004) |
|---|
SQL文をプリペアドステートメントとして準備し、パラメータをバインドして安全に実行します。SQLインジェクション攻撃を防ぐために、ユーザー入力を含むSQLでは必ずプリペアドステートメントを使用してください。
構文
// SQL文を準備する $stmt = $pdo->prepare($sql); // プリペアドステートメントを実行する $stmt->execute($params); // パラメータを名前で紐付ける $stmt->bindParam($param, $variable, $type); // パラメータに値を直接紐付ける $stmt->bindValue($param, $value, $type);
メソッド一覧
| メソッド | 概要 |
|---|---|
| PDO::prepare($sql) | SQL文をプリペアドステートメントとして準備します。プレースホルダー(SQL文の中で値を後から埋め込むための仮のマーク)には『?』または『:name』を使用します。 |
| PDOStatement::execute($params) | プリペアドステートメントを実行します。引数にパラメータの配列を渡すことで値をバインドできます。 |
| PDOStatement::bindParam($param, &$var, $type) | パラメータを変数への参照で紐付けます。『execute()』実行時点の変数の値が使われます。 |
| PDOStatement::bindValue($param, $value, $type) | パラメータに値を直接紐付けます。呼び出し時点の値が固定されます。 |
パラメータの型定数
| 定数 | 概要 |
|---|---|
| PDO::PARAM_STR | 文字列型としてバインドします。デフォルトの型です。 |
| PDO::PARAM_INT | 整数型としてバインドします。 |
| PDO::PARAM_BOOL | 真偽値型としてバインドします。 |
| PDO::PARAM_NULL | NULL型としてバインドします。 |
サンプルで使用するテーブル
以下のサンプルコードでは、次のテーブルとデータを使用します。
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
age INT,
city VARCHAR(50)
);
INSERT INTO users (name, email, age, city) VALUES
('五条悟', 'gojo@jujutsu-high.ac.jp', 28, '東京'),
('両面宿儺', 'sukuna@jujutsu-high.ac.jp', 1000, NULL),
('虎杖悠仁', 'itadori@jujutsu-high.ac.jp', 15, '仙台'),
('伏黒恵', 'fushiguro@jujutsu-high.ac.jp', 15, '東京'),
('釘崎野薔薇', 'kugisaki@jujutsu-high.ac.jp', 15, '東京');
データベース接続
PDOでMySQLに接続します。接続時に『ERRMODE_EXCEPTION』を設定しておくと、SQLエラーが例外として投げられるためデバッグしやすくなります。
<?php
// MySQLへの接続(DSN・ユーザー名・パスワードは環境に合わせて変更してください)
$pdo = new PDO(
'mysql:host=localhost;dbname=sample_db;charset=utf8mb4',
'root', // ユーザー名
'password', // パスワード
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // エラーを例外にする
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 連想配列で取得する
PDO::ATTR_EMULATE_PREPARES => false, // ネイティブプリペアドステートメントを使用する
]
);
接続の詳しい解説は『PDO(データベース接続)』を参照してください。
テーブルの内容を確認します。
SELECT * FROM users; +----+--------------+-------------------------------+------+--------+ | id | name | email | age | city | +----+--------------+-------------------------------+------+--------+ | 1 | 五条悟 | gojo@jujutsu-high.ac.jp | 28 | 東京 | | 2 | 両面宿儺 | sukuna@jujutsu-high.ac.jp | 1000 | NULL | | 3 | 虎杖悠仁 | itadori@jujutsu-high.ac.jp | 15 | 仙台 | | 4 | 伏黒恵 | fushiguro@jujutsu-high.ac.jp | 15 | 東京 | | 5 | 釘崎野薔薇 | kugisaki@jujutsu-high.ac.jp | 15 | 東京 | +----+--------------+-------------------------------+------+--------+ 5 rows in set
サンプルコード
pdo_prepare.php
<?php
// 名前付きプレースホルダーでSELECT文を実行する
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute([':email' => 'user@example.com']);
$user = $stmt->fetch();
echo $user['name']; // 該当ユーザーの名前が出力されます。
// 疑問符プレースホルダーを使用する方法
$stmt = $pdo->prepare("SELECT * FROM users WHERE age > ? AND city = ?");
$stmt->execute([20, '東京']);
$users = $stmt->fetchAll();
// INSERT文を実行する
$stmt = $pdo->prepare("INSERT INTO users (name, email, age) VALUES (:name, :email, :age)");
$stmt->execute([
':name' => '五条悟',
':email' => 'gojo@example.com',
':age' => 28
]);
$newId = $pdo->lastInsertId(); // 挿入された行のIDを取得できる
echo "登録ID: " . $newId;
// UPDATE文を実行する
$stmt = $pdo->prepare("UPDATE users SET name = :name WHERE id = :id");
$stmt->execute([':name' => '両面宿儺', ':id' => 1]);
echo $stmt->rowCount() . "件更新しました。"; // 更新された行数が出力される
// DELETE文を実行する
$stmt = $pdo->prepare("DELETE FROM users WHERE id = :id");
$stmt->execute([':id' => 5]);
// bindValue() で型を明示的に指定する
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id AND active = :active");
$stmt->bindValue(':id', 1, PDO::PARAM_INT);
$stmt->bindValue(':active', true, PDO::PARAM_BOOL);
$stmt->execute();
// bindParam() はループ処理に便利(変数への参照を保持する)
$stmt = $pdo->prepare("INSERT INTO logs (message) VALUES (:message)");
$stmt->bindParam(':message', $msg, PDO::PARAM_STR);
$messages = ['ログイン', 'ページ閲覧', 'ログアウト'];
foreach ($messages as $msg) {
$stmt->execute(); // 各ループでその時点の $msg の値が使われる
}
// LIKE検索でプレースホルダーを使う
$keyword = '虎杖';
$stmt = $pdo->prepare("SELECT * FROM users WHERE name LIKE :keyword");
$stmt->execute([':keyword' => '%' . $keyword . '%']);
php pdo_prepare.php 五条悟 登録ID: 6 1件更新しました。
実践パターン: 安全なユーザー検索
sample_user_search.php
<?php
// 実践パターン: try-catch でエラーハンドリングを含む安全な検索処理
function findUserByEmail(PDO $pdo, string $email): ?array {
try {
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE email = :email LIMIT 1");
$stmt->execute([':email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user !== false ? $user : null;
} catch (PDOException $e) {
// 本番環境ではログに記録し、詳細はユーザーに見せない
error_log("DB error in findUserByEmail: " . $e->getMessage());
return null;
}
}
// 実践パターン: ページネーション付きリスト取得
function getUserList(PDO $pdo, int $page = 1, int $perPage = 20): array {
$offset = ($page - 1) * $perPage;
try {
$stmt = $pdo->prepare("SELECT id, name FROM users ORDER BY id LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("DB error in getUserList: " . $e->getMessage());
return [];
}
}
findUserByEmail() でメールアドレスを検索した場合の例です。
php sample_user_search.php
Array
(
[id] => 1
[name] => 五条悟
[email] => gojo@jujutsu-high.ac.jp
)
よくあるミス1: SQLインジェクション
ユーザー入力を直接SQL文に埋め込むと、SQLインジェクション攻撃を受ける危険があります。必ずプリペアドステートメントを使用してください。
<?php
// NG: ユーザー入力を直接SQL文に埋め込んでいる(SQLインジェクション脆弱性)
$name = $_GET['name'];
$stmt = $pdo->query("SELECT * FROM users WHERE name = '$name'");
// ?name=' OR '1'='1 のような入力で全データが取得されてしまう
<?php
// OK: 必ずプリペアドステートメントを使用する
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_GET['name']]);
よくあるミス2: LIMIT/OFFSETの型指定
LIMIT/OFFSET にプレースホルダーを使う場合、配列でそのまま渡すと文字列型になり、ドライバによってはエラーが発生します。
<?php
// NG: LIMIT/OFFSET にプレースホルダーを使う場合は必ず PARAM_INT を指定する
$stmt = $pdo->prepare("SELECT * FROM users LIMIT :limit");
$stmt->execute([':limit' => 10]); // 文字列として渡されるためエラーになる場合がある
<?php
// OK: bindValue() で型を明示する
$stmt = $pdo->prepare("SELECT * FROM users LIMIT :limit");
$stmt->bindValue(':limit', 10, PDO::PARAM_INT);
$stmt->execute();
LIMIT/OFFSET にプレースホルダーを使う場合は、必ず『bindValue()』で『PDO::PARAM_INT』を指定してください。配列でそのまま渡すと文字列型になり、ドライバによってはエラーが発生します。
概要
プリペアドステートメントは、SQL文とデータを分離して処理する仕組みです。ユーザー入力をSQL文に直接埋め込むのは極めて危険です。必ず『PDO::prepare()』と『execute()』を使用して、プレースホルダー経由でパラメータを渡してください。これによりSQLインジェクション攻撃を根本的に防止できます。
『bindValue()』は呼び出し時点の値が固定され、『bindParam()』は『execute()』実行時点の変数の値が使われます。ループ内で同じステートメントを繰り返し実行する場合は『bindParam()』が便利です。単純なパラメータの受け渡しであれば、『execute()』の引数に配列を渡す方法が最も簡潔に書けます。
データベースへの接続は『new PDO()』で行い、結果の取得は『fetch() / fetchAll()』を使用してください。複数のSQL文をまとめて実行する場合は『トランザクション』の使用を検討してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。