构建一套完整的错误策略:链式的业务异常、受控的 finally,以及作为最后一道防线的全局处理器。
在 Kodokon 中打开本课自 PHP 7 起,所有被抛出的东西都实现了 Throwable,它分为两个分支:Error(引擎和类型层面的失败,比如 TypeError)以及 Exception(应用层面的失败)。你只在程序的边界处捕获 Error。在应用这一侧,SPL 提供了一套现成的层级:LogicException 表示开发者的 bug,需要修复;RuntimeException 表示运行时的意外情况,需要处理。专业的习惯是:从这些基类派生出你的业务异常,每一种可识别的情形对应一个。
<?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();
}只有当你知道该拿这个错误怎么办时,try/catch 才值得用 - 否则,就让它向上冒泡。三个工具能让控制更精细:多重捕获 catch (JsonException | ValueError $e) 把相同的处理归并到一起;当你把一个技术异常转译成业务异常时,previous 参数会链接起最初的原因;finally 在任何情况下都会运行 - 成功、异常,甚至是提前 return - 是释放资源的理想之选。
<?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();
}在链条的末端,两个全局处理器为整套机制收尾。set_exception_handler 捕获所有未被处理的东西:结构化的日志、一个通用的 500 响应 - 而且绝不向用户泄露消息或堆栈跟踪。set_error_handler 把遗留的错误(warning、notice)转换成 ErrorException,把两个世界统一起来:一切都变成异常,一切都走同一条处理路径。
<?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';Error 和 Exception 有什么共同点?Error 继承自 ExceptionThrowable 接口finally 块什么时候运行?previous 参数是用来做什么的?