Kodokon kodokon.com

异常:层级、finally、全局处理器

构建一套完整的错误策略:链式的业务异常、受控的 finally,以及作为最后一道防线的全局处理器。

9 分钟 · 3 题

在 Kodokon 中打开本课

自 PHP 7 起,所有被抛出的东西都实现了 Throwable,它分为两个分支:Error(引擎和类型层面的失败,比如 TypeError)以及 Exception(应用层面的失败)。你只在程序的边界处捕获 Error。在应用这一侧,SPL 提供了一套现成的层级:LogicException 表示开发者的 bug,需要修复;RuntimeException 表示运行时的意外情况,需要处理。专业的习惯是:从这些基类派生出你的业务异常,每一种可识别的情形对应一个。

PHP
<?php

declare(strict_types=1);

abstract class AppException extends RuntimeException
{
}

final class UserNotFound extends AppException
{
    public static function withEmail(
        string $email
    ): self {
        return new self("No user for $email");
    }
}

try {
    throw UserNotFound::withEmail('lea@example.com');
} catch (UserNotFound $e) {
    echo $e->getMessage();
}
业务层级:你可以根据所在的层级,捕获 UserNotFound、AppException 或 RuntimeException。

只有当你知道该拿这个错误怎么办时,try/catch 才值得用 - 否则,就让它向上冒泡。三个工具能让控制更精细:多重捕获 catch (JsonException | ValueError $e) 把相同的处理归并到一起;当你把一个技术异常转译成业务异常时,previous 参数会链接起最初的原因;finally 在任何情况下都会运行 - 成功、异常,甚至是提前 return - 是释放资源的理想之选。

PHP
<?php

declare(strict_types=1);

final class PayloadInvalid extends RuntimeException
{
}

function decode(string $json): mixed
{
    try {
        return json_decode(
            $json,
            true,
            flags: JSON_THROW_ON_ERROR,
        );
    } catch (JsonException $e) {
        throw new PayloadInvalid(
            'Invalid JSON payload',
            previous: $e,
        );
    } finally {
        echo "decode() finished\n";
    }
}

try {
    decode('{broken');
} catch (PayloadInvalid $e) {
    echo $e->getPrevious()?->getMessage();
}
异常转译:技术层面的原因通过 getPrevious() 仍然可以访问。

在链条的末端,两个全局处理器为整套机制收尾。set_exception_handler 捕获所有未被处理的东西:结构化的日志、一个通用的 500 响应 - 而且绝不向用户泄露消息或堆栈跟踪。set_error_handler 把遗留的错误(warning、notice)转换成 ErrorException,把两个世界统一起来:一切都变成异常,一切都走同一条处理路径。

PHP
<?php

declare(strict_types=1);

set_error_handler(function (
    int $severity,
    string $message,
    string $file,
    int $line,
): bool {
    throw new ErrorException(
        $message,
        0,
        $severity,
        $file,
        $line,
    );
});

set_exception_handler(function (Throwable $e): void {
    error_log($e->getMessage());
    http_response_code(500);
    echo 'A technical error occurred.';
});

trigger_error('Legacy warning', E_USER_WARNING);
echo 'Never reached';
warning 变成了一个未被捕获的异常:全局处理器做出响应,然后脚本停止。

知识检测

确认你已牢记本课的重点内容。

  1. 在现代 PHP 中,ErrorException 有什么共同点?
    • Error 继承自 Exception
    • 两者都实现了 Throwable 接口
    • 两者都是 PHP 引擎专用的
  2. finally 块什么时候运行?
    • 只有在没有抛出异常时
    • 只有在 catch 之后
    • 任何情况下都运行:成功、异常(无论是否被捕获)、提前 return
  3. 异常的 previous 参数是用来做什么的?
    • 在你把异常转译成更高层的异常时,保留原始异常
    • 阻止异常沿调用栈向上冒泡
    • 定义下一次失败时要抛出的异常