PHPのプレースホルダとは、SQL文の中に用意する値の仮置き場所のことです。
PHPでMySQLなどのデータベースを操作するとき、SQL文の中に直接変数を埋め込むのではなく、? や :id のような記号を置いておき、あとから値を安全に渡します。
たとえば、ユーザーIDを指定してデータを取得したい場合、次のように書きます。
$sql = "SELECT * FROM users WHERE id = :id";
この :id がプレースホルダです。
実際の値は、あとから execute() などで渡します。
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
つまり、プレースホルダは次のような役割を持っています。
$sql = "SELECT * FROM users WHERE id = :id";
この時点では、まだ :id に具体的な値は入っていません。
その後、
$stmt->execute([
'id' => 1
]);
のように値を渡すことで、id が 1 のユーザーを取得できます。
ただし、厳密にはプレースホルダは単なる文字列置換ではありません。
SQL文に文字列として値を埋め込むのではなく、SQL文と値を分けてデータベースに渡す仕組みです。
この仕組みによって、SQLインジェクションなどの危険な攻撃を防ぎやすくなります。
プレースホルダを使う理由
PHPでデータベースを扱うときにプレースホルダを使う主な理由は、SQLインジェクション対策です。
SQLインジェクションとは、ユーザーがフォームやURLパラメータなどから悪意のある文字列を入力し、SQL文を不正に操作する攻撃のことです。
たとえば、次のようにSQL文へ変数を直接埋め込むコードがあったとします。
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
一見すると、URLから受け取った id を使ってユーザー情報を取得しているだけに見えます。
しかし、もしURLに次のような値が渡されたらどうなるでしょうか。
?id=1 OR 1=1
すると、SQL文は次のような意味になってしまう可能性があります。
SELECT * FROM users WHERE id = 1 OR 1=1
1=1 は常に真です。
そのため、本来取得するべきではないデータまで取得される危険があります。
このような問題を防ぐために、プレースホルダを使います。
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
$user = $stmt->fetch();
このように書くことで、$id の値はSQL文の一部ではなく、パラメータとして安全に渡される値として扱われます。
プレースホルダの基本的な考え方
プレースホルダは、文章でいう「穴埋め」に近いイメージです。
たとえば、次のような文章があるとします。
こんにちは、〇〇さん
この 〇〇 の部分にあとから名前を入れると、
こんにちは、田中さん
になります。
SQLでも考え方は似ています。
$sql = "SELECT * FROM users WHERE name = :name";
この :name が仮置き場所です。
あとから次のように値を渡します。
$stmt = $pdo->prepare($sql);
$stmt->execute([
'name' => '田中'
]);
ただし、PHPのプレースホルダは単なる文字列の置き換えではありません。
ここを誤解しないことが大切です。
悪いイメージは、次のような単純な置換です。
// :name を '田中' に文字列として置き換えるだけ
$sql = "SELECT * FROM users WHERE name = '田中'";
実際のプリペアドステートメントでは、SQL文と値を分けて扱います。
$sql = "SELECT * FROM users WHERE name = :name";
SQL文の構造はSQL文として準備し、田中 という値はパラメータとして別に渡します。
そのため、ユーザーがSQLのような文字列を入力しても、SQL文そのものを壊されにくくなります。
PHPで使うプレースホルダの種類
PHPのPDOで使うプレースホルダには、主に次の2種類があります。
疑問符プレースホルダ
疑問符プレースホルダは、? を使う書き方です。
$sql = "SELECT * FROM users WHERE email = ? AND status = ?";
値を渡すときは、execute() に配列で渡します。
$stmt = $pdo->prepare($sql);
$stmt->execute([
$email,
$status
]);
この場合、値は順番で対応します。
$sql = "SELECT * FROM users WHERE email = ? AND status = ?";
1つ目の ? に $email、2つ目の ? に $status が入ります。
具体例は次の通りです。
$email = 'test@example.com';
$status = 'active';
$sql = "SELECT * FROM users WHERE email = ? AND status = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([
$email,
$status
]);
$users = $stmt->fetchAll();
疑問符プレースホルダは短く書けるのがメリットです。
ただし、プレースホルダが増えると、どの値がどの ? に対応しているのか分かりにくくなる場合があります。
たとえば、次のようなコードでは順番を間違えると意図しない検索になります。
$sql = "SELECT * FROM users WHERE email = ? AND status = ?";
$stmt->execute([
$status,
$email
]);
この場合、本来メールアドレスを入れる場所にステータスが入り、ステータスを入れる場所にメールアドレスが入ってしまいます。
正しくは次のようにします。
$stmt->execute([
$email,
$status
]);
疑問符プレースホルダを使う場合は、値の順番に注意しましょう。
名前付きプレースホルダ
名前付きプレースホルダは、:id や :email のように名前を付ける書き方です。
$sql = "SELECT * FROM users WHERE email = :email AND status = :status";
値を渡すときは、次のように書きます。
$stmt = $pdo->prepare($sql);
$stmt->execute([
'email' => $email,
'status' => $status
]);
具体例は次の通りです。
$email = 'test@example.com';
$status = 'active';
$sql = "SELECT * FROM users WHERE email = :email AND status = :status";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'email' => $email,
'status' => $status
]);
$users = $stmt->fetchAll();
名前付きプレースホルダのメリットは、どの値がどこに入るのか分かりやすいことです。
$sql = "SELECT * FROM users WHERE email = :email AND status = :status";
このSQLを見れば、:email にはメールアドレス、:status にはステータスが入ると直感的に分かります。
初心者のうちは、名前付きプレースホルダを使うほうが理解しやすいでしょう。
疑問符プレースホルダと名前付きプレースホルダの違い
疑問符プレースホルダと名前付きプレースホルダの違いをまとめると、次のようになります。
| 種類 | 書き方 | 特徴 |
|---|---|---|
| 疑問符プレースホルダ | ? | 短く書けるが、値の順番に注意が必要 |
| 名前付きプレースホルダ | :id, :email | 少し長いが、値の意味が分かりやすい |
どちらを使ってもよいですが、1つのSQL文の中で疑問符プレースホルダと名前付きプレースホルダを混ぜるのは避けましょう。
避けたい例です。
$sql = "SELECT * FROM users WHERE id = ? AND status = :status";
このように混在させると、読みづらくなります。
書くなら、次のように統一します。
$sql = "SELECT * FROM users WHERE id = ? AND status = ?";
または、
$sql = "SELECT * FROM users WHERE id = :id AND status = :status";
記事や実務コードでは、どちらか一方に統一したほうが保守しやすくなります。
プレースホルダを使った基本的な書き方
PDOでプレースホルダを使う基本的な流れは、次の3ステップです。
SQL文を用意する
まず、プレースホルダを含むSQL文を作ります。
$sql = "SELECT * FROM users WHERE id = :id";
この時点では、:id に具体的な値は入っていません。
prepareでSQLを準備する
次に、prepare() でSQL文を準備します。
$stmt = $pdo->prepare($sql);
prepare() は、SQL文を実行する前に準備するための処理です。
プレースホルダを使う場合は、基本的にこの prepare() を使います。
executeで値を渡して実行する
最後に、execute() でプレースホルダに対応する値を渡して実行します。
$stmt->execute([
'id' => $id
]);
全体では次のようになります。
$id = 1;
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
$user = $stmt->fetch();
このように、SQL文と値を分けて書くのが基本です。
SELECT文でプレースホルダを使う例
データを取得する SELECT 文でプレースホルダを使う例を見てみましょう。
$id = $_GET['id'] ?? null;
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
$user = $stmt->fetch();
このコードでは、URLなどから受け取った $id をSQL文に直接埋め込んでいません。
悪い例は次のような書き方です。
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
このように変数をSQLに直接入れると、SQLインジェクションの危険があります。
安全に書くなら、次のようにプレースホルダを使います。
$id = $_GET['id'] ?? null;
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
特に $_GET や $_POST など、ユーザーが送信できる値をSQLで使う場合は、基本的にプレースホルダを使うようにしましょう。
INSERT文でプレースホルダを使う例
データを登録する INSERT 文でも、プレースホルダを使います。
$name = '山田太郎';
$email = 'yamada@example.com';
$sql = "INSERT INTO users (name, email) VALUES (:name, :email)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'name' => $name,
'email' => $email
]);
このSQLでは、:name と :email がプレースホルダです。
$sql = "INSERT INTO users (name, email) VALUES (:name, :email)";
そして、次の配列で実際の値を渡しています。
[
'name' => $name,
'email' => $email
]
INSERT 文では、フォームから送信された名前やメールアドレスなどを登録することが多いため、プレースホルダを使う重要性が高いです。
たとえば、お問い合わせフォームや会員登録フォームなどでは、ユーザー入力をそのままSQLに連結しないようにしましょう。
UPDATE文でプレースホルダを使う例
データを更新する UPDATE 文でも、プレースホルダを使います。
$id = 1;
$name = '佐藤花子';
$email = 'sato@example.com';
$sql = "UPDATE users
SET name = :name, email = :email
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id,
'name' => $name,
'email' => $email
]);
このコードでは、更新する値にも、対象を指定するIDにもプレースホルダを使っています。
:name
:email
:id
UPDATE 文では、どのデータを更新するのかを指定する WHERE 条件が重要です。
たとえば、次のように WHERE がないと、全ユーザーのデータが更新されてしまう可能性があります。
$sql = "UPDATE users SET name = :name";
プレースホルダの話とは少し別ですが、UPDATE 文を書くときは、WHERE 条件の有無にも注意しましょう。
DELETE文でプレースホルダを使う例
データを削除する DELETE 文でも、プレースホルダを使います。
$id = 1;
$sql = "DELETE FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
このコードでは、:id に削除対象のIDを渡しています。
$sql = "DELETE FROM users WHERE id = :id";
削除処理でも、ユーザー入力をそのままSQLに連結するのは危険です。
悪い例です。
$id = $_GET['id'];
$sql = "DELETE FROM users WHERE id = $id";
安全な例です。
$id = $_GET['id'];
$sql = "DELETE FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
DELETE 文も UPDATE 文と同じく、WHERE 条件がないと大量のデータを削除する危険があります。
bindValueを使って値を渡す方法
プレースホルダに値を渡す方法は、execute() に配列を渡す方法だけではありません。
bindValue() を使って、プレースホルダに値を設定することもできます。
bindValueの基本形
$id = 1;
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch();
このコードでは、次の部分で :id に $id をバインドしています。
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
PDO::PARAM_INT は、整数として扱うという指定です。
IDのように数値として扱いたい値には、PDO::PARAM_INT を指定するとよいです。
executeの配列渡しとの違い
通常の検索や登録では、次のように execute() に配列で値を渡す書き方で問題ありません。
$stmt->execute([
'id' => $id
]);
一方で、値の型を明確に指定したい場合は、bindValue() が便利です。
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
特に、LIMIT や OFFSET に値を渡す場合は、整数として扱う必要があるため、bindValue() を使うことがあります。
$limit = 10;
$offset = 20;
$sql = "SELECT * FROM users LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
execute() の配列渡しでは、環境や設定によって値が文字列として扱われやすいことがあります。
そのため、LIMIT や OFFSET のように整数指定が必要な場面では、bindValue() で PDO::PARAM_INT を指定すると安心です。
bindValueとbindParamの違い
PDOには、bindValue() と似たメソッドとして bindParam() があります。
名前が似ていますが、動きが異なります。
bindValueはその時点の値を渡す
bindValue() は、呼び出した時点の値をプレースホルダに渡します。
$id = 1;
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $id);
$id = 2;
$stmt->execute();
この場合、bindValue() を呼び出した時点の $id は 1 です。
そのため、基本的には 1 が使われます。
bindParamは変数を参照として渡す
一方、bindParam() は、値そのものではなく変数を参照としてバインドします。
$id = 1;
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':id', $id);
$id = 2;
$stmt->execute();
この場合、execute() した時点の $id は 2 です。
そのため、:id には 2 が使われます。
初心者のうちは、特別な理由がなければ、次のどちらかを使うと分かりやすいです。
$stmt->execute([
'id' => $id
]);
または、
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
bindParam() は、同じSQLを変数の値を変えながら何度も実行したい場合などに使うことがあります。
プレースホルダを使うときの注意点
プレースホルダは便利ですが、万能ではありません。
特に重要なのは、プレースホルダは値に対して使うものだという点です。
テーブル名には使えない
次のような書き方は基本的にできません。
$sql = "SELECT * FROM :table WHERE id = :id";
:table を users のようなテーブル名に置き換えたい、という意図かもしれません。
しかし、プレースホルダはテーブル名のようなSQL構造を置き換えるためのものではありません。
プレースホルダで渡せるのは、基本的に次のような「値」です。
WHERE id = 1
WHERE name = '田中'
WHERE status = 'active'
テーブル名やカラム名は値ではなく、SQL文の構造にあたります。
そのため、プレースホルダでは扱えません。
カラム名には使えない
カラム名にもプレースホルダは使えません。
たとえば、次のようなコードは意図通りに動きません。
$column = 'created_at';
$sql = "SELECT * FROM users ORDER BY :column";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'column' => $column
]);
この場合、:column は created_at というカラム名として扱われるのではなく、値として扱われます。
つまり、期待している次のSQLとは意味が違います。
SELECT * FROM users ORDER BY created_at
ORDER BY の対象カラムを動的に切り替えたい場合は、プレースホルダではなく、ホワイトリストを使います。
$allowedSorts = ['id', 'name', 'created_at'];
$sort = $_GET['sort'] ?? 'id';
if (!in_array($sort, $allowedSorts, true)) {
$sort = 'id';
}
$sql = "SELECT * FROM users ORDER BY {$sort}";
このように、許可したカラム名だけを使うようにします。
並び順もユーザーが指定できるようにする場合は、並び順もホワイトリストでチェックします。
$allowedSorts = ['id', 'name', 'created_at'];
$allowedOrders = ['asc', 'desc'];
$sort = $_GET['sort'] ?? 'id';
$order = strtolower($_GET['order'] ?? 'asc');
if (!in_array($sort, $allowedSorts, true)) {
$sort = 'id';
}
if (!in_array($order, $allowedOrders, true)) {
$order = 'asc';
}
$sql = "SELECT * FROM users ORDER BY {$sort} {$order}";
ポイントは、ユーザー入力をそのままSQLに入れないことです。
値にはプレースホルダを使い、カラム名や並び順のようなSQL構造にはホワイトリストを使いましょう。
プレースホルダをクォートで囲まない
プレースホルダを使うとき、次のようにシングルクォートで囲んではいけません。
$sql = "SELECT * FROM users WHERE email = ':email'";
このように書くと、:email がプレースホルダではなく、ただの文字列として扱われる可能性があります。
正しくは次のように書きます。
$sql = "SELECT * FROM users WHERE email = :email";
文字列の値を渡す場合でも、プレースホルダ自体をクォートで囲む必要はありません。
$stmt = $pdo->prepare($sql);
$stmt->execute([
'email' => $email
]);
値が文字列か数値かは、PDO側で適切に扱われます。
名前付きプレースホルダと配列キーを合わせる
名前付きプレースホルダを使う場合、SQL内の名前と execute() に渡す配列のキーを対応させる必要があります。
正しい例です。
$sql = "SELECT * FROM users WHERE email = :email";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'email' => $email
]);
間違った例です。
$sql = "SELECT * FROM users WHERE email = :email";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'mail' => $email
]);
SQLでは :email と書いているのに、配列では 'mail' になっています。
この場合、:email に対応する値が渡されません。
名前を付けるときは、SQL側とPHP側で同じ名前を使いましょう。
同じ名前付きプレースホルダの再利用に注意する
次のように、同じ名前付きプレースホルダを複数回使いたくなることがあります。
$sql = "SELECT * FROM users
WHERE name LIKE :keyword
OR email LIKE :keyword";
環境によっては動く場合もありますが、PDOの設定やデータベースドライバによって挙動が変わる可能性があります。
初心者向けには、別々の名前を付けるほうが安全です。
$keyword = 'tanaka';
$sql = "SELECT * FROM users
WHERE name LIKE :keyword_name
OR email LIKE :keyword_email";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'keyword_name' => "%{$keyword}%",
'keyword_email' => "%{$keyword}%"
]);
少し長くなりますが、どの値がどのプレースホルダに対応しているか明確になります。
IN句に配列をそのまま渡せない
IN 句を使うときにも注意が必要です。
たとえば、複数のIDを指定してデータを取得したい場合、次のように書きたくなるかもしれません。
$ids = [1, 2, 3];
$sql = "SELECT * FROM users WHERE id IN (:ids)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'ids' => $ids
]);
しかし、1つのプレースホルダに配列をそのまま渡すことは基本的にできません。
この場合は、配列の要素数に合わせてプレースホルダを作ります。
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$sql = "SELECT * FROM users WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute($ids);
このコードでは、$ids の数に合わせて次のようなプレースホルダを作っています。
?, ?, ?
結果として、SQLは次のような形になります。
SELECT * FROM users WHERE id IN (?, ?, ?)
複数の値を扱う場合は、値の数に応じてプレースホルダを作る必要があります。
ログイン処理でプレースホルダを使う場合の注意点
プレースホルダの説明では、ログイン処理を例にすることがあります。
たとえば、次のようなSQLです。
$sql = "SELECT * FROM users WHERE email = :email AND password = :password";
プレースホルダの説明としては分かりやすいですが、実務のログイン処理としては注意が必要です。
実際のログイン処理では、パスワードを平文で保存したり、SQLでそのまま比較したりしてはいけません。
実務では、登録時に password_hash() でパスワードをハッシュ化し、ログイン時に password_verify() で確認します。
例としては、次のような流れです。
$email = $_POST['email'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE email = :email";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'email' => $email
]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
echo 'ログイン成功';
} else {
echo 'メールアドレスまたはパスワードが違います';
}
このように、メールアドレスの検索にはプレースホルダを使い、パスワードの照合には password_verify() を使います。
プレースホルダはSQLインジェクション対策として重要ですが、パスワード管理には別のセキュリティ対策も必要です。
プレースホルダを使った実務的なPDOコード例
ここでは、PDOでデータベースに接続し、プレースホルダを使ってデータを取得する例を紹介します。
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=test_db;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
$id = $_GET['id'] ?? null;
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch();
var_dump($user);
} catch (PDOException $e) {
echo 'DBエラー: ' . $e->getMessage();
}
このコードのポイントは次の通りです。
PDOでデータベースに接続している
$pdo = new PDO(
'mysql:host=localhost;dbname=test_db;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
charset=utf8mb4 を指定することで、日本語や絵文字なども扱いやすくなります。
また、次の設定でエラーを例外として扱えます。
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
取得結果を連想配列で受け取るために、次の設定もしています。
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
SQLに変数を直接埋め込んでいない
次のような書き方はしていません。
$sql = "SELECT * FROM users WHERE id = $id";
代わりに、プレースホルダを使っています。
$sql = "SELECT * FROM users WHERE id = :id";
bindValueで整数として値を渡している
IDは整数として扱いたいため、bindValue() で PDO::PARAM_INT を指定しています。
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
このようにすることで、:id に渡す値を整数として扱いやすくなります。
プレースホルダとエスケープの違い
SQLインジェクション対策の話では、「エスケープ」という言葉も出てきます。
エスケープとは、SQL文の中で特別な意味を持つ文字を無害化する処理のことです。
たとえば、文字列の中にシングルクォート ' が含まれていると、SQL文の構造が壊れることがあります。
そのため、危険な文字を適切に処理する必要があります。
ただし、PHPでPDOを使う場合、基本的には手動でエスケープするよりも、プレースホルダ付きのプリペアドステートメントを使うほうが安全で管理しやすいです。
避けたい書き方です。
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
推奨される書き方です。
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = :email";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'email' => $email
]);
プレースホルダを使うことで、SQL文と値を分けて扱えます。
そのため、ユーザー入力にSQLのような文字列が含まれていても、SQL文そのものを壊されにくくなります。
プレースホルダのメリット
プレースホルダには、主に次のようなメリットがあります。
SQLインジェクション対策になる
最大のメリットは、SQLインジェクション対策です。
ユーザー入力をSQLに直接連結せず、パラメータとして渡せるため、悪意のある文字列によってSQL文を壊されるリスクを下げられます。
SQL文と値を分けて管理できる
プレースホルダを使うと、SQL文と値を分けて書けます。
$sql = "SELECT * FROM users WHERE email = :email";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'email' => $email
]);
SQL文の構造と、実際に渡す値が分かれているため、コードの見通しがよくなります。
コードが読みやすくなる
名前付きプレースホルダを使うと、どの値をどこに渡しているか分かりやすくなります。
$sql = "SELECT * FROM users WHERE email = :email AND status = :status";
このように書けば、:email にはメールアドレス、:status にはステータスが入ると分かります。
同じSQLを再利用しやすい
プリペアドステートメントでは、同じSQLに対して値を変えながら実行することもできます。
たとえば、複数のユーザーを順番に登録するような処理では、SQL文を使い回しやすくなります。
$sql = "INSERT INTO users (name, email) VALUES (:name, :email)";
$stmt = $pdo->prepare($sql);
$users = [
['name' => '山田太郎', 'email' => 'yamada@example.com'],
['name' => '佐藤花子', 'email' => 'sato@example.com'],
];
foreach ($users as $user) {
$stmt->execute([
'name' => $user['name'],
'email' => $user['email']
]);
}
このように、SQL文を一度準備し、値を変えながら実行できます。
プレースホルダのデメリット・注意点
プレースホルダは便利ですが、注意点もあります。
SQLの構造そのものは置き換えられない
プレースホルダで扱えるのは基本的に値です。
次のようなものには使えません。
- テーブル名
- カラム名
- 並び順の
ASC/DESC - SQLキーワード
- SQL文の一部
たとえば、これは適切ではありません。
$sql = "SELECT * FROM :table WHERE id = :id";
テーブル名を動的に変えたい場合は、許可するテーブル名をあらかじめ決めておく必要があります。
疑問符プレースホルダは順番ミスが起きやすい
疑問符プレースホルダは短く書けますが、値の順番を間違えるとバグにつながります。
$sql = "SELECT * FROM users WHERE email = ? AND status = ?";
$stmt->execute([
$status,
$email
]);
このように順番を間違えると、正しく検索できません。
プレースホルダが多い場合は、名前付きプレースホルダを使うと読みやすくなります。
配列をそのまま1つのプレースホルダに渡せない
IN 句で複数の値を扱う場合、配列をそのまま1つのプレースホルダに渡すことはできません。
$ids = [1, 2, 3];
$sql = "SELECT * FROM users WHERE id IN (:ids)";
このように書くのではなく、配列の要素数分だけプレースホルダを作ります。
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$sql = "SELECT * FROM users WHERE id IN ($placeholders)";
この点は実務でもよくつまずくポイントです。
プレースホルダを使うべき場面
PHPでデータベースを扱う場合、基本的には外部から受け取った値をSQLに使う場面ではプレースホルダを使うと考えておくとよいです。
フォーム入力を使う場合
お問い合わせフォーム、ログインフォーム、検索フォームなど、ユーザーが入力した値をSQLで使う場合はプレースホルダを使います。
$keyword = $_GET['keyword'] ?? '';
$sql = "SELECT * FROM posts WHERE title LIKE :keyword";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'keyword' => "%{$keyword}%"
]);
URLパラメータを使う場合
URLの id などを使ってデータを取得する場合も、プレースホルダを使います。
$id = $_GET['id'] ?? null;
$sql = "SELECT * FROM posts WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
Cookieやセッションの値を使う場合
Cookieやセッションの値をSQLに使う場合も、値を直接SQLに連結せず、プレースホルダを使いましょう。
$userId = $_SESSION['user_id'] ?? null;
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $userId, PDO::PARAM_INT);
$stmt->execute();
セッションの値はユーザー入力より安全に見えるかもしれませんが、SQLに値を渡すときは一貫してプレースホルダを使うのが基本です。
プレースホルダを使わないほうがよい例
反対に、次のようなコードは避けましょう。
変数をSQLに直接埋め込む
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
この書き方は、SQLインジェクションの危険があります。
安全に書くなら、次のようにします。
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = :email";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'email' => $email
]);
数値だから安全だと思って直接埋め込む
数値のIDだから大丈夫だと思って、次のように書くのも避けたほうがよいです。
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
id は数値のつもりでも、URLパラメータから受け取る値はユーザーが変更できます。
安全に書くなら、次のようにします。
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
必要に応じて、事前に数値チェックを入れるとさらに安心です。
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
exit('不正なIDです');
}
まとめ
PHPのプレースホルダとは、PDOなどのプリペアドステートメントで使う値の仮置き場所です。
$sql = "SELECT * FROM users WHERE id = :id";
この :id がプレースホルダです。
実際の値は、次のようにあとから渡します。
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
プレースホルダを使うことで、SQL文に変数を直接連結せず、SQL文と値を分けて扱えます。
そのため、SQLインジェクション対策として非常に重要です。
PHPでデータベースを扱うときは、次の考え方を覚えておきましょう。
// 避けたい書き方
$sql = "SELECT * FROM users WHERE id = $id";
// 推奨される書き方
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $id
]);
また、プレースホルダはあくまで値に使うものです。
テーブル名やカラム名、ORDER BY の並び替え対象など、SQLの構造そのものを置き換えることはできません。
値を安全に渡す場合はプレースホルダを使い、カラム名や並び順を動的にしたい場合はホワイトリストで許可したものだけを使う。
この考え方を押さえておくと、PHPで安全なデータベース処理を書きやすくなります。
以上、PHPのプレースホルダについてでした。
最後までお読みいただき、ありがとうございました。










