1. 導入
PHPでデータベース操作を行う際、PDOのプリペアドステートメントはSQLインジェクションを防ぐための必須ツールです。しかし、プレースホルダに値をバインドする際、bindParam() を安易に使うと、意図しない値がクエリにセットされるというバグに直面することがあります。本記事では、この「参照渡し」に起因するトラブルの原因と、現場で推奨される安全な実装方法を解説します。
2. 基礎知識
PDOには値をバインドするメソッドとして、bindParam() と bindValue() の2種類があります。
bindParam() は、変数を「参照(リファレンス)」としてバインドします。つまり、SQLが実行される(execute)瞬間の変数の値が使用されます。
対して bindValue() は、バインドした時点での値をコピーして保持します。そのため、その後に変数の値を変更しても、SQLの内容には影響しません。
3. 実装/解決策
多くの開発者が陥る問題は、同じ変数名を使い回した際に、最後の値がすべてのプレースホルダに適用されてしまう現象です。これを防ぐには以下の3つの戦略があります。
1. bindValue() を使用する: ループ内で処理する場合や、変数を使い回す場合はこちらが最も安全です。
2. 変数名を分ける: プレースホルダごとに個別の変数を用意すれば参照による上書きは発生しません。
3. unset() を使う: bindParam() を使用後に unset() を実行することで、参照を解除します。
4. サンプルプログラム
以下のコードは、bindParam() 使用時のバグを再現し、それを回避する安全な実装例です。
prepare($sql);
$value = “foo”;
$stmt->bindParam(1, $value); // 参照としてバインド
$value = “bar”;
$stmt->bindParam(2, $value); // 参照としてバインド
// この時点で $value は “bar” なので、両方のプレースホルダが “bar” になる
// $stmt->execute();
// 【推奨】bindValue() を使用した安全な実装
$stmt = $db->prepare($sql);
$value = “foo”;
$stmt->bindValue(1, $value); // 値としてコピーされる
$value = “bar”;
$stmt->bindValue(2, $value); // 値としてコピーされる
$stmt->execute(); // 正しく “foo” と “bar” がバインドされる
// 【ループ内での推奨実装】
$params = [‘foo’, ‘bar’];
foreach ($params as $index => $val) {
// bindValueなら変数の参照を気にせず安全にループできる
$stmt->bindValue($index + 1, $val);
}
?>
5. 応用・注意点
現場のコードレビューで最も見かけるミスは、ループ内で foreach(&$value) のように参照渡しを使い、その後の処理で同じ $value を再利用してバグを発生させるケースです。
ループ処理を行う場合は、可能な限り bindValue() を使用することを強く推奨します。もし、パフォーマンス上の理由や特定の要件でどうしても bindParam() を使う必要がある場合は、処理の直後に必ず unset() を呼ぶ癖をつけ、不要な参照を残さないように徹底してください。小さなミスが大きなデータ不整合を招かないよう、設計段階から「値のコピー」か「参照」かを意識するようにしましょう。