给文章列表加一个搜索框,看起来只是把关键词放进 SQL。但如果查询语句直接拼接用户输入,后续每加一个筛选项,都要重新考虑字符串怎么转义、哪些位置可能被改变。代码也会越来越难检查。
PDO 预处理查询适合把这件事分清楚:SQL 结构由程序确定,外部传入的值通过参数交给数据库。下面围绕一个公开文章列表演示,不涉及用户隐私查询或订单权限。
先把关键词从 SQL 字符串里拿出来
假设项目已有一个 PDO 连接 $pdo,文章表叫 articles,并且已经完成请求参数的类型检查。下面的关键词是示例数据,表名和字段名需要按自己的项目调整。
$keyword = 'PHP';
$stmt = $pdo->prepare(
'SELECT id, title, published_at FROM articles
WHERE status = ? AND title LIKE ?
ORDER BY published_at DESC, id DESC
LIMIT 20'
);
$stmt->execute(['published', '%' . $keyword . '%']);
$articles = $stmt->fetchAll(PDO::FETCH_ASSOC);
这里问号的位置对应完整的数据值。execute() 中两个值的顺序,分别对应公开状态和搜索条件。不要把问号写成 '?',也不要先把关键词插进 SQL,再调用 prepare():后者仍然把外部输入混进了语句结构。
根据 PHP 官方 PDO::prepare 文档,同一条语句应选择问号或命名参数中的一种。占位符只能代表数据值,不能代表表名、字段名或一段 SQL。
排序字段变了,怎么办?
搜索值可以绑定,ORDER BY 后面的字段却不能靠一个参数替代。适合的处理方式是建立固定映射:页面提交一个简短选项,服务端把它转换成预先写好的 SQL 片段。
$sort = 'newest'; // 实际请求先检查类型,再查这张映射表
$orderChoices = [
'newest' => 'published_at DESC, id DESC',
'oldest' => 'published_at ASC, id ASC',
];
$orderSql = $orderChoices[$sort] ?? $orderChoices['newest'];
$stmt = $pdo->prepare(
'SELECT id, title FROM articles
WHERE status = ? ORDER BY ' . $orderSql . ' LIMIT 20'
);
$stmt->execute(['published']);
这里虽然存在字符串拼接,但被拼接的是程序内部的固定选项。不要把映射改成“前端传什么字段就拼什么字段”,也别把前端下拉框当作安全边界,直接请求接口的人完全可以提交其他内容。
三个经常卡住的细节
一个占位符不能代表整组 IN 条件
传入 '1,2,3' 只是一个字符串值,不会自动变成三个独立参数。需要根据已验证的数组长度生成对应数量的问号,再逐个绑定;空数组单独处理。给批量查询设置数量上限,也能避免一个请求生成过长的语句。
LIKE 的通配符仍然有自己的含义
示例中的百分号实现包含搜索。用户自己输入百分号或下划线时,数据库也可能把它们当作匹配规则。是否允许这种搜索行为,要按产品要求决定。预处理解决参数与语句结构的分离,并不会自动把搜索语义改成“所有字符都按字面匹配”。
需要整数类型时,明确绑定
PDOStatement::execute 文档说明,通过参数数组传入的值按字符串参数处理。遇到要求整数的绑定位置,可以使用 bindValue() 并指定 PDO::PARAM_INT。先校验页码,再计算偏移量,别直接相信请求中的负数或超大数。
用了预处理,仍要保留业务条件
文章列表应该继续限制公开状态,后台查询应该继续验证管理员身份。一个查询即使完全没有 SQL 注入问题,也可能因为漏了权限条件而返回不该公开的数据。这两类检查要分别做。
改造旧代码时,可以先找出所有接收外部参数的查询,为每一处记录:哪些是数据值,哪些是固定的排序或字段选择,哪些是权限条件。再逐条替换,比较原来的返回结果。不要只检查正常关键词;空值、引号、中文、通配符和异常页码都应该有明确处理。
本文示例用于解释查询写法,不是可直接上线的完整接口。连接配置、输入校验、错误日志及页面输出处理,应沿用项目已有的统一实现。