多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

PHP的PDO错误与错误处理

PHP的PDO错误与错误处理 前言PDO 提供了三种错误处理策略用PDO::ATTR_ERRMODE属性切换。听起来是个小配置项实际上它是本章最容易踩坑的地方原因在于默认值变过。在 PHP 8.0.0 之前默认模式是PDO::ERRMODE_SILENT——出错了什么都不说只是把错误码放到一边等你自己去查。很多老代码正是靠这个沉默在跑。从 PHP 8.0.0 起手册明确规定默认模式改为PDO::ERRMODE_EXCEPTION。于是同一段代码从 PHP 7 升到 PHP 8 之后原本静默失败的查询会突然抛出异常把本来藏在水下的问题全掀到水面上。这既是一次升级事故的常见来源也是一次难得的纠错机会。第二个高频误解是把 PDO 异常当成普通异常处理。PDOException继承自RuntimeException但它的$code属性类型是int|string——PDO 会把 SQLSTATE 字符串例如42S02塞进code。所以if ($e-getCode() 1146)这种拿整数比对的写法永远不会成立。第三连接失败是特殊的。手册专门提示PDO::__construct()在连接失败时总是抛PDOException跟你设的PDO::ATTR_ERRMODE是什么无关。本文按 PHP 8.1 梳理三种错误模式的取舍、SQLSTATE 三元组的读法以及一套能直接用的统一错误处理写法。一、三种错误模式怎么选模式行为适用阶段PDO::ERRMODE_SILENT只设置错误码不抛不报PHP 8.0 之前的默认值现代代码不建议PDO::ERRMODE_WARNING设置错误码同时发出E_WARNING临时调试想让错误可见但不中断流程PDO::ERRMODE_EXCEPTION设置错误码并抛PDOExceptionPHP 8.0 起的默认值生产环境首选手册对ERRMODE_EXCEPTION的评价很实在它会在错误点把脚本炸掉从而非常快地指出问题位置而且因为脚本因异常终止时未提交的事务会被自动回滚一致性也有了兜底。此外它让你能集中写try/catch比在每一步后面判断返回值要少写大量嵌套。设置方式有两种推荐在构造函数里一次性给足?php // 适用于 PHP 8.0显式声明以免依赖版本默认值declare(strict_types1);$dsn mysql:host127.0.0.1;dbnameshop;charsetutf8mb4;try {$pdo new PDO($dsn, app, secret, [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES false,]);} catch (PDOException $e) {// 连接失败一定会走到这里与 ERRMODE 无关error_log(DB connect failed: . $e-getMessage());http_response_code(500);exit(服务暂时不可用);}即使 PHP 8 的默认值已经是异常模式显式写出来仍然值得坚持。一是向下兼容时行为确定二是读代码的人一眼能看出这套代码的错误处理契约。有一点要特别提醒ERRMODE_WARNING在 Web 环境里并不好用。它发出的是 PHP 警告会混进页面输出如果display_errors开着警告内容会直接渲染到 HTML 里把表名、SQL 片段甚至连接信息暴露给访客。手册在连接章节专门警告过未捕获异常产生的致命错误回溯可能泄露连接细节所以生产服务器的display_errors应当设为0。二、SQLSTATE 与 errorInfo 三元组PDO 用 SQL-92 定义的 SQLSTATE 码作为统一错误标识由各驱动把自家错误码翻译过去。两个读取方法分工明确PDO::errorCode(): ?string—— 只返回一个五字符 SQLSTATE没有错误时返回null。PDO::errorInfo(): array—— 返回三元组信息更全。errorInfo()的数组结构是固定的下标内容0SQLSTATE 错误码五字符1驱动特定的错误码2驱动特定的错误信息PDOStatement上也有同名的errorCode()和errorInfo()语义一致但表示的是该语句上最近一次操作的错误。判断到底该看哪个对象有一个简单规则手册原文的意思错误来自语句对象上的调用就查那个语句对象错误来自数据库对象上的调用就在数据库对象上查。?php // 适用于 PHP 8.0演示 SILENT 模式下的手工检查$pdo new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE PDO::ERRMODE_SILENT]);$stmt $pdo-prepare(SELECT id FROM users WHERE email ?);if ($stmt false) {// prepare 失败错误记在数据库对象上var_dump($pdo-errorCode(), $pdo-errorInfo());exit(1);}if (!$stmt-execute([$email])) {// execute 失败错误记在语句对象上var_dump($stmt-errorCode(), $stmt-errorInfo());exit(1);}$rows $stmt-fetchAll();注意SELECT这类语句在 SILENT 模式下即使execute()成功也可能带回警告级状态所以严格来说还要检查errorCode()是否以01开头。这也是 SILENT 模式不推荐的原因——检查点太多漏一个就等于没有错误处理。三、PDOException 的两个特殊之处PDOException继承自RuntimeException手册在介绍里还加了一句态度鲜明的提醒不要在你自己的代码里抛出PDOException。它是 PDO 内部用来上报错误的类型语义上属于来自 PDO 层的信号。它的类摘要里有两条值得记住的信息class PDOException extends RuntimeException{protected int|string $code;public ?array $errorInfo null;}第一条$code是int|string。PDO 把 SQLSTATE 以字符串形式塞进code所以?php // 适用于 PHP 8.0try {$pdo-query(SELECT wrongcolumn FROM wrongtable);} catch (PDOException $e) {echo $e-getCode(); // 例如 42S02是字符串不是整数echo $e-getMessage(); // SQLSTATE[42S02]: Base table or view not found: ...}第二条PDOException上有errorInfo属性PHP 8.x 手册的类摘要中列出内容与PDO::errorInfo()对应。用它比去正则解析getMessage()里的SQLSTATE[...]前缀可靠得多?php // 适用于 PHP 8.0try {$pdo-prepare(INSERT INTO users (email) VALUES (?))-execute([$email]);} catch (PDOException $e) {$info $e-errorInfo ?? null; // 老环境上该属性可能不存在$sqlState $info[0] ?? (string) $e-getCode();if ($sqlState 23000) { // 完整性约束冲突// 例如邮箱唯一索引冲突转成友好的业务提示echo 该邮箱已被注册\n;} else {throw $e; // 其余错误继续向上抛}}这里用??取值是刻意的它既能处理errorInfo为null的情况也能在没有这个属性的旧版本上安全降级不会因为访问未定义属性而报警告。顺带提一下PDO::ERR_NONE这个常量。手册说它是个便利常量用来跟errorCode()的返回值做比较——因为errorCode()在没有错误时返回null拿它跟ERR_NONE比并不总是等价更稳妥的写法是直接判null或用 00000判断成功。四、一套可复用的错误处理骨架把上面这些东西合起来可以得到一个适合业务代码的封装底层统一抛异常顶层统一翻译。?php // 适用于 PHP 8.0declare(strict_types1);final class Db{private static ?PDO $pdo null;public static function conn(): PDO{if (self::$pdo instanceof PDO) {return self::$pdo;}try {self::$pdo new PDO(mysql:host127.0.0.1;dbnameshop;charsetutf8mb4,app,secret,[PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES false,]);} catch (PDOException $e) {error_log([db] connect: . $e-getMessage());throw new RuntimeException(数据库连接失败, 0, $e);}return self::$pdo;}/** SQLSTATE 到用户可读提示的映射只覆盖需要友好提示的几个 */private const FRIENDLY [23000 数据冲突请检查后重试,42S02 数据表不存在请联系管理员,HY000 数据库内部错误,];public static function run(string $sql, array $params []): PDOStatement{try {$stmt self::conn()-prepare($sql);$stmt-execute($params);return $stmt;} catch (PDOException $e) {$info $e-errorInfo ?? null;$sqlState $info[0] ?? (string) $e-getCode();// 详细错误只进日志绝不返回给调用方error_log(sprintf([db] %s | sql%s | params%s, $sqlState, $sql, json_encode($params)));$msg self::FRIENDLY[$sqlState] ?? 数据处理失败;throw new RuntimeException($msg, 0, $e);}}}这个骨架体现了三条原则都是从手册的行为约束里推出来的。其一原始异常一律不直接对外暴露。PDOException::getMessage()里带着 SQLSTATE、驱动错误码和表名直接返回给前端等于把数据库结构告诉攻击者。日志里留全量响应里只给翻译过的提示。其二连接异常单独处理。因为PDO::__construct()的异常不受ERRMODE控制把它和查询异常混在一条路径上容易误判。其三json_encode($params)记录参数时仍要留个心眼。如果参数里含密码、令牌日志本身就变成了泄露点生产环境应当对敏感字段做掩码。常见坑点依赖版本默认值❌ 什么都不设指望出错时会抛异常 ✅ 显式写PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTIONPHP 8.0 前默认是 SILENT用整数比对getCode()❌if ($e-getCode() 1146) { ... }✅$sqlState ($e-errorInfo ?? [])[0] ?? (string) $e-getCode();SQLSTATE 是字符串正则解析异常消息拿错误码❌preg_match(/SQLSTATE\[(\w)\]/, $e-getMessage(), $m);✅ 优先用errorInfo属性或PDO::errorInfo()三元组忘了连接异常不受 ERRMODE 影响❌ 设了ERRMODE_SILENT就以为new PDO()不会抛 ✅ 构造函数失败永远抛PDOException必须单独try/catch把errorInfo()当成当前错误❌ 随便找个 PDO 对象调errorInfo()就当拿到最近一次错误 ✅ 错误记在发起那次调用的对象上语句错查语句对象连接错查数据库对象生产环境把原始异常消息返回给前端❌echo $e-getMessage();✅ 日志记录全量响应只返回翻译后的提示SILENT 模式下只看execute()的返回值❌$stmt-execute($p);后不检查任何东西 ✅ 要么换用异常模式要么每次检查errorCode()并按 00000判断成功display_errors在生产开启❌ 未捕获异常导致的致命错误把回溯打到页面上 ✅ 生产display_errors0错误写日志手册明确提示回溯可能泄露连接细节总结项目内容默认错误模式PHP 8.0.0 起为PDO::ERRMODE_EXCEPTION之前为PDO::ERRMODE_SILENT三种模式ERRMODE_SILENT/ERRMODE_WARNING/ERRMODE_EXCEPTION连接异常PDO::__construct()失败总是抛PDOException与 ERRMODE 无关errorCode()返回?string即五字符 SQLSTATE无错时为nullerrorInfo()返回数组0为 SQLSTATE1为驱动错误码2为驱动错误消息PDOException::$code类型 intPDOException::$errorInfoPHP 8.x 中提供与PDO::errorInfo()对应PDO 的错误处理没有复杂机制难点全在细节约定上默认值在 PHP 8 变了、连接异常不受错误模式管、异常码是字符串而不是整数。把这三点记住然后坚持底层抛异常、顶层翻译、日志留全量、响应给口径的分层写法出错时既能定位问题又不会把数据库结构泄给外面。
返回列表