ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP8.2怎么实现多个catch捕获不同异常

PHP8.2怎么实现多个catch捕获不同异常 前言try/catch用起来简单出事的时候却容易让人懵明明写了 catch异常还是冒到最外层或者反过来一个catch (\Exception $e)把本该暴露的TypeError吞掉了程序带着坏数据继续往下跑最后在一个完全不相关的地方报错。还有一类是 catch 块顺序写反某个分支永远不执行排查时看代码明明写了却毫无反应。先纠正标题里的版号。多个catch块捕获不同异常不是 PHP 8.2 的特性它从 PHP 5.0 引入异常机制时就有了用竖线把多个异常类型写在一个 catch 里联合捕获multi-catch是 PHP 7.1 引入的。PHP 8.2 在这件事上没有新增语法。所以本文按事实讲语法自 5.0 / 7.1 起可用在任何 8.x包括 8.2、8.4上都同样能跑示例中的新语法会单独标注所需版本。需要分清两个容易混淆的概念概念写法引入版本多个 catch 块catch (A $e) { } catch (B $e) { }PHP 5.0联合捕获multi-catchcatch (A\B $e) { }Throwable接口catch (\Throwable $e)PHP 7.0finally块try { } catch { } finally { }PHP 5.5后文按异常体系 → 捕获顺序 → 联合捕获 → 异常链与清理 → 实战的顺序展开最后一节列的是真会踩的坑。一、先搞清楚 PHP 的异常体系PHP 的错误与异常有两条根\Exception传统意义上的异常。\ErrorPHP 7.0 引入表示 PHP 自身的错误比如TypeError、ValueErrorPHP 8.0、DivisionByZeroError。两者的共同父接口就是\ThrowablePHP 7.0。关键事实\Error不继承\Exception。所以catch (\Exception $e)捕获不到TypeError。这是老代码升级到 8.x 后最常见的catch 没生效的原因。Throwable (interface) ├── Exception │ ├── RuntimeException │ ├── InvalidArgumentException │ └── ... └── Error ├── TypeError ├── ValueError (PHP 8.0) ├── DivisionByZeroError └── ...写 catch 时先决定一件事你是想处理某几类业务异常其余照常抛出还是兜底一切保证接口一定返回 JSON。前者 catch 具体的类后者 catch\Throwable。二、多个 catch 的匹配规则与顺序PHP 从上到下依次匹配第一个匹配上的 catch 执行其余全部跳过。这里的匹配按继承关系走子类异常也能被父类的 catch 接住所以顺序必须从具体到宽泛。?php declare(strict_types1); class ApiException extends RuntimeException {} class NotFoundException extends ApiException {} class ValidationException extends ApiException {} try { throw new NotFoundException(user 42 not found); } catch (NotFoundException $e) { echo 404 分支\n; } catch (ApiException $e) { echo 通用 API 分支\n; } catch (Throwable $e) { echo 兜底分支\n; }输出是404 分支。如果把catch (ApiException $e)提到最前面NotFoundException就永远进不了第一个分支——这是静态代码检查能过、但行为完全错误的典型写法。catch 后面可以跟变量名、类型、或者什么都不写PHP 8.0 起允许省略变量} catch (NotFoundException) { // PHP 8.0不需要异常对象时可以省略变量名 echo 404\n; }三、联合捕获一个块处理多种异常当几种异常的处理逻辑完全一样用竖线合并?php declare(strict_types1); try { doSomething(); } catch (InvalidArgumentException | DomainException $e) { // $e 的静态类型是二者的最近公共祖先通常是 RuntimeException 或 Exception echo get_class($e), : , $e-getMessage(), \n; }两个必须记住的约束联合里只能写类型不能写变量catch (A | $e)是语法错误。$e的静态类型变成联合的公共父类型所以只能调用公共父类上有的方法。InvalidArgumentException::getMessage()没问题但如果某个类型独有的方法就调不到了只能instanceof判断或拆成两个 catch。还有一条经验不要把存在继承关系的两个类型写进同一个联合捕获比如catch (ApiException | NotFoundException $e)。ApiException已经覆盖了它的子类多写一个只是让人误以为两者会被区别对待实际处理逻辑完全一样。要区别对待就拆成两个 catch要合并就只写父类。四、finally、异常链与自定义异常finally无论是否抛异常都会执行常用于释放资源。要小心它对返回值的影响——见坑点第 5 条。异常链exception chaining用第三个参数传递上游异常排查时能一路回溯到根因?php declare(strict_types1); class RepositoryException extends RuntimeException {} function findUser(PDO $pdo, int $id): array { try { $stmt $pdo-prepare(SELECT id, name FROM users WHERE id ?); $stmt-execute([$id]); $row $stmt-fetch(PDO::FETCH_ASSOC); } catch (PDOException $e) { // 把底层异常挂在链上而不是只拼消息字符串 throw new RepositoryException(查询用户 {$id} 失败, 0, $e); } if ($row false) { throw new NotFoundException(用户 {$id} 不存在); } return $row; } try { findUser($pdo, 42); } catch (NotFoundException $e) { http_response_code(404); echo not found\n; } catch (RepositoryException $e) { http_response_code(500); error_log($e-getMessage() . \n . $e-getTraceAsString()); // 顺着链条打印根因 for ($prev $e-getPrevious(); $prev ! null; $prev $prev-getPrevious()) { error_log( caused by: . get_class($prev) . : . $prev-getMessage()); } echo server error\n; }getPrevious()返回链上的上一个异常这条链是排查生产问题的关键。实战完整可运行示例下面这个脚本把多 catch 顺序、联合捕获、\Error与\Exception的分野、异常链和finally放在一起存成.php直接跑即可。它不依赖数据库。?php declare(strict_types1); class ApiException extends RuntimeException {} class NotFoundException extends ApiException {} class ValidationException extends ApiException {} class RepositoryException extends ApiException {} /** * 模拟业务处理返回处理结果字符串 */ function handle(string $mode): string { switch ($mode) { case notfound: throw new NotFoundException(资源不存在); case invalid: throw new ValidationException(参数校验失败); case repo: try { throw new PDOException(SQLSTATE[HY000] 连接超时); } catch (PDOException $e) { throw new RepositoryException(仓储层不可用, 0, $e); } case type: // TypeError 属于 \Error不是 \Exception return str_repeat([], 3); case divzero: return (string) (1 % 0); default: return ok; } } foreach ([ok, notfound, invalid, repo, type, divzero] as $mode) { echo str_pad($mode, 10), ; try { echo handle($mode), \n; } catch (NotFoundException $e) { echo 404: , $e-getMessage(), \n; } catch (RepositoryException | ValidationException $e) { // 联合捕获两类走同一段处理 echo business[ . get_class($e) . ]: , $e-getMessage(), \n; if ($e-getPrevious() ! null) { echo caused by , get_class($e-getPrevious()), : , $e-getPrevious()-getMessage(), \n; } } catch (ApiException $e) { echo fallback api: , $e-getMessage(), \n; } catch (DivisionByZeroError $e) { echo php error: 除零 , get_class($e), \n; } catch (Throwable $e) { // 兜住 \ErrorTypeError 等避免整个脚本挂掉 echo unexpected: , get_class($e), : , $e-getMessage(), \n; } finally { echo [finally]\n; } }输出ok ok [finally] notfound 404: 资源不存在 [finally] invalid business[ValidationException]: 参数校验失败 [finally] repo business[RepositoryException]: 仓储层不可用 caused by PDOException: SQLSTATE[HY000] 连接超时 [finally] type unexpected: TypeError: str_repeat(): Argument #1 ($string) must be of type string, array given [finally] divzero php error: 除零 DivisionByZeroError [finally]注意type那一行如果最外层只写catch (ApiException $e)而没有catch (Throwable $e)这个TypeError会直接终止脚本返回白页或者 500 且没有日志——这正是catch 写了却不生效的真实来源。常见坑点❌ 只 catch\ExceptionTypeError、ValueError、DivisionByZeroError都属于\Error不继承\Exception会直接冒到顶层。✅兜底用catch (\Throwable $e)只想捕业务异常时也别把Throwable完全排除在外。❌ 父类 catch 写在子类前面catch (ApiException $e)放在catch (NotFoundException $e)之前后者永远不执行且代码能正常编译审查时极难发现。✅固定从具体到宽泛的顺序把Throwable永远放最后。❌ 在联合捕获里调某个类型的独有方法catch (A | B $e)后写$e-getDetail()而getDetail()只有A有运行时报Call to undefined method。✅联合类型只写共有逻辑需要区分时用instanceof分支或者干脆拆成两个 catch。❌ 空 catch 吞异常catch (Throwable $e) {}接口层面永远不报错实际上失败原因全部丢失出问题时连一行日志都没有。✅至少error_log()记一条或者重新抛出一个带getPrevious()的领域异常。❌ 忽略finally里的returnfinally中的return会覆盖try和catch里的return静默改变函数结果。✅finally只做清理关连接、放锁、删临时文件不要写return或break。❌ 用getMessage()拼字符串丢失栈信息把$e-getMessage()塞进新异常的消息里原始getTrace、getFile、getLine全没了。✅用第三个参数传原异常throw new MyException(..., 0, $e);用getPrevious()取根因。❌ 把异常信息直接输出给用户catch (\Throwable $e) { echo $e-getMessage(); }页面把文件路径、SQL 片段、内部结构全暴露出去而日志里一条记录都没有安全与运维两头都输。✅对外只返回统一的错误码或提示语把get_class($e)、getMessage()、getTraceAsString()写进error_log。❌ 在 catch 里throw新异常却不catch它catch 块内部抛出的异常不会再被同一个 try 的其他 catch 块接住而是继续向外冒。✅记住 catch 块自身的异常走外层 try 的 catch要么在外层再包一层要么就地处理干净。总结需求写法最低版本分别处理不同异常多个catch块具体在前PHP 5.0多种异常走同一段逻辑catch (A\B $e)兜底一切并记录日志catch (\Throwable $e)PHP 7.0不需要异常对象catch (FooException) { }PHP 8.0保留根因throw new X(..., 0, $e)getPrevious()PHP 5.3一定执行的清理finally { }不要写returnPHP 5.5标题说的 PHP 8.2 并不是这些语法的引入版本——多 catch 是 5.0联合捕获是 7.18.2 在这里没有新东西。真正要记住的只有两条catch 顺序必须从具体到宽泛兜底必须用\Throwable而不是\Exception。把这两条落到代码规范里绝大多数catch 不生效的诡异现象就不会再出现。
返回列表