1. 導入
PHPでデータベース操作を行う際、セキュリティ対策として必須となるプリペアドステートメント。PDOを利用する際に便利なメソッドとして「bindParam」がありますが、その「参照渡し」という特性を深く理解していないと、意図しない値がクエリにセットされるというバグを引き起こすことがあります。本記事では、この挙動の仕組みと、実務で安全に実装するためのベストプラクティスを解説します。
2. 基礎知識
PDOのプレースホルダに値をバインドするメソッドには、主に「bindParam」と「bindValue」の2種類が存在します。
bindParamは、変数を「参照(リファレンス)」としてバインドします。つまり、SQLを実行する瞬間にその変数の値を見に行くため、バインドした後に変数の中身が変われば、SQLの内容も変わってしまいます。
一方、bindValueは、バインドした瞬間の値を固定してセットします。
この違いを理解していないと、ループ処理や変数の再利用時に予期せぬデータがデータベースへ送信されてしまうリスクがあるのです。
3. 実装/解決策
最もシンプルで安全な解決策は、変数の再利用を避け、値を固定できる「bindValue」を選択することです。特にループ処理などでは、変数が参照先のまま書き換わってしまうことが多いため、基本的にはbindValueの使用を推奨します。もしbindParamを使う必要がある場合は、変数のスコープを意識するか、処理が終わった後に「unset」を使用して参照を解除する習慣をつけましょう。
4. サンプルプログラム
以下のコードでは、bindParamの落とし穴を再現し、その回避策を示しています。
prepare($sql);
// 【NGパターン】同じ変数名を使い回すと、最後に上書きされた値が全てのプレースホルダに適用される
$value = “foo”;
$stmt->bindParam(1, $value);
$value = “bar”;
$stmt->bindParam(2, $value);
// execute()実行時、$valueは”bar”のため、SQLは “name = ‘bar’ OR name = ‘bar'” となる
// $stmt->execute();
// 【解決策1】bindValueを使う(推奨)
// 値をその時点で固定するため、後の変数操作の影響を受けない
$stmt->bindValue(1, “foo”);
$stmt->bindValue(2, “bar”);
$stmt->execute();
// 【解決策2】bindParamを使うなら、ループ後にunsetで参照を解除する
$params = [‘foo’, ‘bar’];
foreach ($params as $key => &$val) {
$stmt->bindParam($key + 1, $val);
}
unset($val); // 重要:参照を解除しないと、後の処理で意図せず$valが書き換わるリスクがある
$stmt->execute();
?>
5. 応用・注意点
現場レベルで最も陥りやすいのは、foreach文の中で「&$value」のように参照渡しでループしているケースです。ループ終了後もその変数には参照が残っているため、その後のコードで同じ変数名を使用して別の処理を行うと、予期せずデータベースのクエリに影響を及ぼしたり、他の変数の値が書き換わったりする「壊滅的なバグ」になり得ます。
「ループで参照を使う際は必ず直後にunsetする」、もしくは「可能な限りbindValueを利用する」。この2点を徹底するだけで、PDO関連のデータ不整合バグを劇的に減らすことができます。安全なコードを書くために、ぜひ意識してみてください。