PDO 预处理语句
概述
预处理语句(Prepared Statements)是 PDO 的核心安全特性,通过参数绑定防止 SQL 注入。prepare() 创建预处理语句,bindParam()/bindValue() 绑定参数,execute() 执行语句。PDO 支持命名占位符(:name)和问号占位符(?)两种参数风格。
适用场景
- 所有 SQL 查询(安全必需)
- 重复执行的 SQL(性能优化)
- 用户输入的参数化查询
- 批量数据操作
基础概念
占位符类型
| 类型 | 语法 | 示例 |
|---|---|---|
| 命名占位符 | :name | WHERE id = :id |
| 问号占位符 | ? | WHERE id = ? |
bindParam vs bindValue
| 函数 | 引用绑定 | 类型 | 执行时求值 |
|---|---|---|---|
bindParam() | 是(引用) | 可指定 PDO::PARAM_* | 是 |
bindValue() | 否(值) | 可指定 PDO::PARAM_* | 否 |
性能优势
预处理语句在多次执行相同 SQL 时(不同参数)具有性能优势,因为 SQL 只需编译一次。
语法与代码示例
命名占位符
php
<?php
declare(strict_types=1);
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
// prepare + bindValue + execute
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND status = :status');
$stmt->bindValue(':id', 1, PDO::PARAM_INT);
$stmt->bindValue(':status', 'active', PDO::PARAM_STR);
$stmt->execute();
$user = $stmt->fetch();
// execute 直接传参数数组(推荐简洁写法)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND status = :status');
$stmt->execute(['id' => 1, 'status' => 'active']);
$user = $stmt->fetch();问号占位符
php
<?php
// 问号占位符(按索引绑定)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ? AND status = ?');
$stmt->bindValue(1, 1, PDO::PARAM_INT);
$stmt->bindValue(2, 'active', PDO::PARAM_STR);
$stmt->execute();
// execute 数组(索引从 1 开始)
$stmt->execute([1, 'active']);
$user = $stmt->fetch();bindParam 引用绑定
php
<?php
// bindParam 绑定变量引用
// 在 execute() 时才读取变量值
$stmt = $pdo->prepare('INSERT INTO logs (message, level) VALUES (:msg, :level)');
$message = 'Test message';
$level = 'INFO';
$stmt->bindParam(':msg', $message, PDO::PARAM_STR);
$stmt->bindParam(':level', $level, PDO::PARAM_STR);
// 修改变量值后执行
$message = 'Modified message';
$level = 'WARNING';
$stmt->execute(); // 使用修改后的值批量执行
php
<?php
// 批量插入
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$users = [
['name' => 'Alice', 'email' => 'alice@example.com', 'age' => 28],
['name' => 'Bob', 'email' => 'bob@example.com', 'age' => 32],
['name' => 'Charlie', 'email' => 'charlie@example.com', 'age' => 25],
];
$pdo->beginTransaction();
try {
foreach ($users as $user) {
$stmt->execute([
'name' => $user['name'],
'email' => $user['email'],
'age' => $user['age'],
]);
}
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
throw $e;
}IN 查询处理
php
<?php
// PDO 不直接支持 IN 占位符,需要动态构建
$ids = [1, 2, 3, 4, 5];
// 动态生成占位符
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ({$placeholders})");
$stmt->execute(array_values($ids));
$users = $stmt->fetchAll();
// 命名占位符版本
$placeholders = implode(',', array_map(fn($i) => ':id' . $i, array_keys($ids)));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ({$placeholders})");
$params = array_combine(
array_map(fn($i) => ':id' . $i, array_keys($ids)),
$ids
);
$stmt->execute($params);实战示例
通用查询构建器
php
<?php
declare(strict_types=1);
class QueryBuilder
{
private PDO $pdo;
private string $table;
private array $where = [];
private array $params = [];
private ?int $limit = null;
private ?int $offset = null;
private array $orderBy = [];
public function __construct(PDO $pdo, string $table)
{
$this->pdo = $pdo;
$this->table = $table;
}
public function where(string $column, mixed $value, string $operator = '='): self
{
$param = ':w_' . count($this->where);
$this->where[] = "{$column} {$operator} {$param}";
$this->params[$param] = $value;
return $this;
}
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
public function offset(int $offset): self
{
$this->offset = $offset;
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): self
{
$this->orderBy[] = "{$column} {$direction}";
return $this;
}
public function get(): array
{
$sql = "SELECT * FROM {$this->table}";
if (!empty($this->where)) {
$sql .= ' WHERE ' . implode(' AND ', $this->where);
}
if (!empty($this->orderBy)) {
$sql .= ' ORDER BY ' . implode(', ', $this->orderBy);
}
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
if ($this->offset !== null) {
$sql .= " OFFSET {$this->offset}";
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->params);
return $stmt->fetchAll();
}
public function first(): ?array
{
$this->limit(1);
$results = $this->get();
return $results[0] ?? null;
}
}
// 使用
$users = (new QueryBuilder($pdo, 'users'))
->where('status', 'active')
->where('age', 18, '>')
->orderBy('created_at', 'DESC')
->limit(10)
->get();注意事项
占位符只能用于值
php
<?php
// 占位符不能用于表名、列名、SQL 关键字
// 不好
$stmt = $pdo->prepare('SELECT * FROM :table WHERE :column = :value');
// 好:表名和列名直接拼接到 SQL(需要白名单验证)
$allowedTables = ['users', 'orders', 'products'];
$table = $_GET['table'];
if (!in_array($table, $allowedTables, true)) {
throw new InvalidArgumentException('Invalid table');
}
$stmt = $pdo->prepare("SELECT * FROM {$table} WHERE id = :id");LIKE 查询中的占位符
php
<?php
// LIKE 通配符需要在值中包含
$search = '%john%';
$stmt = $pdo->prepare("SELECT * FROM users WHERE name LIKE :search");
$stmt->execute(['search' => $search]);最佳实践
1. 优先使用 execute() 传参
php
<?php
// 推荐:简洁
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $userId]);
// 不必要:繁琐
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $userId, PDO::PARAM_INT);
$stmt->execute();2. 指定参数类型
php
<?php
// 对于整型、布尔型参数指定类型
$stmt = $pdo->prepare('SELECT * FROM users WHERE age > :age AND active = :active');
$stmt->bindValue(':age', 18, PDO::PARAM_INT);
$stmt->bindValue(':active', true, PDO::PARAM_BOOL);
$stmt->execute();进阶用法
调试与测试技巧
php
<?php
declare(strict_types=1);
// 单元测试辅助函数
function createTestResource(): mixed
{
return match (true) {
default => new stdClass(),
};
}
// 调试输出函数
function debugOutput(mixed , string = ''): void
{
= ? ": " : '';
.= print_r(, true);
fwrite(STDERR, . "\n");
}
// 性能基准测试
function benchmark(callable , int = 1000): float
{
= hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$fn();
}
return (hrtime(true) - $start) / 1e9;
}日志记录实践
php
<?php
declare(strict_types=1);
/**
* 简易日志记录器
*/
class SimpleLogger
{
private string $logFile;
private string $level = 'INFO';
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
private function log(string $level, string $message, array $context): void
{
$timestamp = date('Y-m-d H:i:s');
$contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}配置与环境检测
php
<?php
declare(strict_types=1);
// 环境检测工具
class EnvironmentChecker
{
public static function checkRequirements(array $requirements): array
{
$results = [];
foreach ($requirements as $name => $check) {
$results[$name] = is_callable($check) ? $check() : false;
}
return $results;
}
public static function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'sapi' => PHP_SAPI,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'loaded_extensions' => get_loaded_extensions(),
];
}
}常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 网络问题/配置错误 | 检查配置,增加超时时间 |
| 权限不足 | 文件/目录权限 | 使用 chmod/chown 修正 |
| 性能下降 | 索引缺失/数据量大 | 添加索引,优化查询 |
| 数据不一致 | 并发冲突/事务残留 | 使用锁机制和事务 |
| 内存溢出 | 大数据集/未释放资源 | 增大内存限制,分批处理 |
故障排除步骤
- 检查错误日志和异常信息
- 确认配置和环境是否正确
- 使用调试工具逐步排查
- 参考官方文档查找已知问题
版本兼容性说明
| 功能 | 最低版本 | 说明 |
|---|---|---|
| 基础功能 | PHP 8.1 | 本文档基准版本 |
| 只读属性 | PHP 8.1 | public readonly 修饰符 |
| 枚举类型 | PHP 8.1 | enum 类型和 match 表达式 |
| Fiber | PHP 8.1 | 协程/轻量级并发 |
| 命名参数 | PHP 8.0 | foo(arg_name: value) |
| 联合类型 | PHP 8.0 | `int |
| Null 安全运算符 | PHP 8.0 | $obj?->method() |
| 析构器 promotion | PHP 8.0 | __construct(public $x) |
php
<?php
declare(strict_types=1);
// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
if (version_compare(PHP_VERSION, $minVersion, '<')) {
throw new RuntimeException(
sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
);
}
}
ensureVersion('8.1.0');