詳細ページを作成するには、SELECT * FROM reviews WHERE id = 5
のように、URLから受け取った特定のid
を持つレコードをデータベースから取得する必要があります。
このとき、$id = $_GET['id']; $sql = "SELECT * FROM reviews WHERE id = $id";
のように、受け取った変数を直接SQL文に埋め込むのは絶対に行ってはいけません。これはSQLインジェクションという、Webアプリケーションで最も深刻な脆弱性の一つを生み出す、非常に危険な書き方です。
この記事では、この危険を回避し、安全にデータベースを操作するための現代的な手法である「プリペアドステートメント」と「プレースホルダ」の使い方を解説します。
1. なぜプレースホルダが必要なのか?SQLインジェクションの脅威
SQLインジェクションとは、攻撃者が入力フォームやURLパラメータに不正なSQL文の一部を紛れ込ませることで、データベースを不正に操作する攻撃です。
もし変数を直接SQL文に埋め込むと、データベースはどこまでが本来のSQLで、どこからがユーザーの入力値なのかを区別できません。これにより、データの改ざん、情報漏洩、データの全削除といった壊滅的な被害に繋がる可能性があります。
「プレースホルダ」は、この問題を解決するための仕組みです。SQL文のテンプレート(雛形)と、そこにはめ込む実際の「値」を完全に分離してデータベースに伝えることで、ユーザーの入力値をただの「データ」として扱うよう強制し、不正なSQLとして実行されるのを防ぎます。
2. プレースホルダを使ったSQLの準備 (prepare
)
PDOでプリペアドステートメントを利用するには、まずprepare()
メソッドでSQL文のテンプレートを準備します。このとき、後から値をはめ込みたい部分をプレースホルダに置き換えます。
プレースホルダには2種類の書き方があります。
?
(クエスチョンマークプレースホルダ): 名前のないプレースホルダ。順番が重要になります。PHP$sql = "SELECT * FROM reviews WHERE id = ?"; $stmt = $pdo->prepare($sql);
:name
(名前付きプレースホルダ)::id
のようにコロンから始まる名前を付けます。パラメータが多い場合に、どのプレースホルダにどの値が入るのかが分かりやすくなります。PHP$sql = "SELECT * FROM reviews WHERE rating >= :rating AND genre = :genre"; $stmt = $pdo->prepare($sql);
3. プレースホルダに値を渡す(バインドする)方法
準備したSQL文のプレースホルダに実際の値を渡す(バインドする)方法は、主に2つあります。
方法1(推奨): execute()
に配列を渡す
最もシンプルで一般的な方法です。execute()
メソッドの引数に配列を渡すことで、プレースホルダに値をバインドできます。
?
プレースホルダの場合:?
の順番に対応する値を入れた、添字配列を渡します。PHP$id = 5; $stmt->execute([$id]);
- 名前付きプレースホルダの場合: プレースホルダ名をキーとする連想配列を渡します。PHP
$stmt->execute([ ':rating' => 4, ':genre' => 'ビジネス' ]);
方法2(明示的): bindValue()
を使う
execute()
の前にbindValue()
メソッドを使って、一つずつ値をバインドする方法もあります。この方法では、値のデータ型を明示的に指定することができます。
bindValue(プレースホルダ名/位置, 値, データ型)
$id = 5;
// ? プレースホルダの場合(1番目の?に値をバインド)
$stmt->bindValue(1, $id, PDO::PARAM_INT);
// 名前付きプレースホルダの場合
// $stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
PDO::PARAM_INT
は値が整数であることを、PDO::PARAM_STR
は文字列であることを示します。型を明示することで、より厳密な処理が可能になります。
実装例:IDを指定して特定のレビューを取得する
それでは、URLから受け取ったIDを安全に使い、特定のレビュー1件を取得する、実践的なコードを見てみましょう。
detail.php
のサンプルコード:
<?php
// データベース接続情報
$dsn = 'mysql:host=localhost;dbname=my_first_db;charset=utf8mb4';
$user = 'root';
$password = 'root';
try {
// IDのバリデーション
if (empty($_GET['id']) || !ctype_digit($_GET['id'])) {
throw new Exception('IDが不正です。');
}
$id = (int)$_GET['id'];
// データベースに接続
$pdo = new PDO($dsn, $user, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// プレースホルダを用いたSQLを準備
$sql = "SELECT * FROM reviews WHERE id = ?";
$stmt = $pdo->prepare($sql);
// execute()の引数に配列で値を渡す
$stmt->execute([$id]);
// 結果を1件だけ連想配列として取得
$review = $stmt->fetch(PDO::FETCH_ASSOC);
// 結果が存在しない場合の処理
if (!$review) {
echo "指定されたIDのレビューは見つかりませんでした。";
exit();
}
// 取得したデータを安全に出力
echo "<h1>" . htmlspecialchars($review['book_title'], ENT_QUOTES, 'UTF-8') . "</h1>";
echo "<p>評価: " . htmlspecialchars($review['rating'], ENT_QUOTES, 'UTF-8') . "</p>";
echo "<div>" . nl2br(htmlspecialchars($review['review_text'], ENT_QUOTES, 'UTF-8')) . "</div>";
} catch (Exception $e) {
echo "エラー: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
die();
}
?>
まとめ
- ユーザー入力などの変動する値をSQLに含める際は、絶対に直接変数を埋め込んではいけない。
- **
prepare()
でSQLのテンプレートを準備し、変動する部分はプレースホルダ (?
or:name
)**にする。 - **
execute()
に配列を渡すか、bindValue()
**を使って、安全に値をプレースホルダに渡す(バインドする)。
この「プリペアドステートメント」は、PHPでデータベースを扱う上での最も重要なセキュリティ対策です。必ずマスターして、安全なWebアプリケーションを開発しましょう。