因为预处理语句要求SQL结构固定,字符串拼接会提前污染SQL模板,使占位符失效;必须用?占位并严格匹配bind_param的类型、顺序和引用传递。

为什么 mysqli_prepare() 不能直接用字符串拼接参数
因为 mysqli_prepare() 的第二个参数是 SQL 模板,其中的占位符(? 或命名参数)必须在执行前绑定变量,而不是把值拼进 SQL 字符串里。一旦你写成 "SELECT * FROM users WHERE id = " . $_GET['id'],再套 prepare() 也毫无意义——SQL 已经被污染了。
常见错误现象:mysqli_prepare() 返回 false,但没检查就继续调用 bind_param(),导致致命错误;或者绑定了变量却忘了调用 execute(),结果查不到数据。
- 必须用
?占位,且数量、类型、顺序要和bind_param()严格一致 -
bind_param()第一个参数是类型字符串:"i"(整型)、"s"(字符串)、"d"(浮点)、"b"(blob),不能写错 - 绑定的变量必须是引用(PHP 5.6+ 支持传值,但老项目仍常见
&$var写法)
如何用 PDO::prepare() 处理不同数据类型和 NULL 值
PDO::prepare() 更灵活,支持命名参数(如 :name)和位置参数(?),但类型推断靠 bindValue() 或 bindParam() 显式控制。尤其注意 NULL 值——如果直接传 null,PDO 默认当字符串处理,可能触发类型不匹配或隐式转换。
使用场景:用户搜索带空值的字段(如可选的年龄范围)、更新时允许某些字段置空。
立即学习“PHP免费学习笔记(深入)”;
- 用
bindValue(':age', $age, PDO::PARAM_INT)显式声明类型,避免自动转字符串 - 传
NULL时,PDO::PARAM_NULL是安全写法,比如bindValue(':city', null, PDO::PARAM_NULL) - 不要混用命名参数和位置参数在同一语句中,PDO 会报
SQLSTATE[HY093] - 批量插入时,
execute()接收数组即可,但数组键名必须和命名参数完全一致
预处理语句失效的三种典型情况
不是用了 prepare() 就万事大吉。以下情况会让预处理退化为普通查询,失去防注入能力:
- 表名、列名、排序字段(
ORDER BY)用参数绑定——语法错误,?只能代入值,不能代入标识符 - SQL 拼接发生在
prepare()调用之前,比如$sql = "SELECT * FROM " . $_GET['table']; $pdo->prepare($sql) - 使用
mysql_real_escape_string()或addslashes()后再进prepare()——多余且危险,可能双重转义或漏逃
正确做法:表名/列名只能白名单校验(如 in_array($table, ['users', 'posts'])),不能靠绑定。
调试时怎么确认预处理真生效了
最直接的办法是看 MySQL 的 general_log 或用 SHOW PROCESSLIST 观察实际执行的语句。但开发期更实用的是检查绑定是否成功、参数是否被替换。
- 对
mysqli:调用prepare()后立刻检查返回值,if (!$stmt = $mysqli->prepare($sql)) { die($mysqli->error); } - 对
PDO:启用异常模式$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION),让错误中断流程 - 执行后用
get_result()(mysqli)或fetch()(PDO)取结果,别只依赖execute()返回true——它只表示语句发出去了,不保证有数据 - 在测试用例里故意传含单引号的字符串(如
O'Reilly),看是否报错或查出意外记录
容易被忽略的是连接字符集——如果 PHP 连接 MySQL 时没设 utf8mb4,而用户输入了 emoji 或特殊符号,即使用了预处理,也可能因编码不一致导致截断或乱码,间接绕过校验逻辑。



















