PDO 是 PHP 内置数据库抽象层,需启用 pdo_mysql 扩展、显式设置 charset=utf8mb4、禁用模拟预处理、使用 prepare/execute 防注入,并显式管理事务与连接生命周期。

PDO 不是“数据库操作类”,它是 PHP 内置的数据库访问抽象层,必须配合具体驱动(如 pdo_mysql)才能连接 MySQL;不启用对应扩展、不设 PDO::ATTR_EMULATE_PREPARES => false、不显式声明 charset=utf8mb4,就谈不上安全可用。
确认 pdo_mysql 扩展已启用且生效
报 Class 'PDO' not found 或 could not find driver,不是代码写错了,而是环境没配好。PHP 7.0+ 虽自带 PDO 核心,但 pdo_mysql 是独立扩展,需手动启用。
- 运行
php -m | grep pdo,应同时看到pdo和pdo_mysql - 若缺失
pdo_mysql,编辑php.ini,取消注释或添加extension=pdo_mysql - 改完必须重启 Web 服务器(如 Apache/Nginx)或 PHP-FPM,
phpinfo()页面里搜 “PDO” 和 “mysql” 两个关键词都得出现 - 注意:CLI(命令行)和 Web SAPI 的
php.ini可能不同,要分别检查
DSN 中 charset=utf8mb4 必须显式写进字符串
只在连接后执行 $pdo->exec("SET NAMES utf8mb4") 是靠不住的——尤其在连接复用(如持久连接)、多语句执行、或预处理中,字符集可能在第一条语句前就已错乱,导致 emoji 插入失败、中文乱码、甚至宽字节注入绕过。
- 正确写法:
$dsn = "mysql:host=localhost;dbname=testdb;charset=utf8mb4" - 错误写法:
$dsn = "mysql:host=localhost;dbname=testdb"+ 单独SET NAMES - 不要加
port=3306,除非你真改了 MySQL 端口;硬写反而容易因客户端库版本差异导致连接失败 -
charset参数只对 MySQL 驱动有效,PostgreSQL/SQLite 不识别,换库时 DSN 要重写
prepare + execute 是唯一靠谱的参数传递方式
任何带用户输入的 SQL(哪怕只是 $_GET['id']),拼接字符串或用 addslashes() 都不防注入。占位符不是语法糖,是服务端预处理的开关。
立即学习“PHP免费学习笔记(深入)”;
- 支持两种占位符:
?(位置参数)和:name(命名参数),不能混用 - 表名、字段名、
ORDER BY子句、LIMIT偏移量——这些**不能**用占位符,必须走白名单校验或硬编码 -
PDO::ATTR_EMULATE_PREPARES => false必须设为false,否则 MySQL 服务端不真正预处理,INT 类型字段可能被当字符串传入,触发隐式类型转换漏洞 - 示例安全写法:
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE status = ? AND created_at > ?"); $stmt->execute(['active', '2024-01-01']);
事务与连接生命周期常被忽略的关键点
事务不是自动开启的,autocommit 默认是开的;连接也不是用完就断,对象销毁不等于连接关闭,尤其在长生命周期脚本或 CLI 场景下容易出问题。
- 开启事务必须显式调用
$pdo->beginTransaction(),之后所有语句才进入事务上下文 - 提交用
$pdo->commit(),回滚用$pdo->rollback();异常未捕获时不会自动回滚 - 想用持久连接?必须在构造时通过
$options传PDO::ATTR_PERSISTENT => true,事后用setAttribute()无效 - 持久连接不清理状态:临时表、锁、未提交事务会残留,下次复用可能出错;高并发下慎用
- 显式释放连接:设
$pdo = null,并确保没有PDOStatement对象还持有引用(也设为null)



















