Throwable 接口
概述
Throwable 是 PHP 7.0+ 引入的顶层异常接口,是所有可抛出对象(Error 和 Exception)的共同父接口。它是 PHP 异常处理体系的核心,try/catch 语句只能捕获实现了 Throwable 接口的对象。
PHP 版本说明
Throwable自 PHP 7.0 起引入- 它替代了 PHP 5.x 中只能
catch (\Exception $e)的限制 - PHP 7.0+ 的
try/catch只能捕获Throwable实例 - 用户自定义类不能直接
implements Throwable
基础概念
Throwable 的设计目的
在 PHP 5.x 中,try/catch 只能捕获 Exception,无法捕获 Fatal Error。PHP 7.0 引入了 Throwable 接口来统一错误和异常的处理:
Throwable (接口) — 不可被用户类直接实现
├── Error — PHP 引擎内部错误
│ ├── TypeError
│ ├── ValueError (PHP 8.0+)
│ ├── ArgumentCountError
│ ├── ArithmeticError
│ │ └── DivisionByZeroError
│ ├── CompileError (PHP 7.3+)
│ │ └── ParseError
│ ├── UnhandledMatchError (PHP 8.0+)
│ └── FiberError (PHP 8.1+)
└── Exception — 用户级异常
├── ErrorException
├── LogicException
│ ├── InvalidArgumentException
│ ├── OutOfBoundsException
│ ├── RuntimeException
│ ├── DomainException
│ └── LengthException
├── RuntimeException
│ ├── OutOfRangeException
│ ├── OverflowException
│ ├── RangeException
│ ├── UnderflowException
│ └── UnexpectedValueException
└── ... (更多子类)为什么用户类不能实现 Throwable
PHP 引擎内部通过特殊机制处理 Throwable,如果允许用户类直接实现,可能破坏错误处理的基本保证。因此 PHP 强制规定:
Error和Exception是Throwable的仅有的两个内置实现- 用户类只能
extends Exception或extends Error(但不推荐后者) - 直接
implements Throwable会导致编译错误
<?php
// 这行代码会导致编译错误:
// class MyError implements Throwable {}
// Fatal error: Class MyError cannot implement interface Throwable,
// it is implicitly implemented by Error and Exception语法与代码
Throwable 接口定义
Throwable 接口定义了以下方法:
<?php
declare(strict_types=1);
// Throwable 接口的方法签名(内部定义)
interface Throwable
{
public function getMessage(): string;
public function getCode(): int;
public function getFile(): string;
public function getLine(): int;
public function getTrace(): array;
public function getTraceAsString(): string;
public function getPrevious(): ?Throwable;
public function __toString(): string;
}
// 无论捕获的是 Error 还是 Exception,都可以调用这些方法
function handleThrowable(\Throwable $e): void
{
echo "消息: {$e->getMessage()}" . PHP_EOL;
echo "代码: {$e->getCode()}" . PHP_EOL;
echo "文件: {$e->getFile()}:{$e->getLine()}" . PHP_EOL;
echo "前一个异常: " . ($e->getPrevious() ? $e->getPrevious()->getMessage() : '无') . PHP_EOL;
}catch(Throwable $e) 捕获所有
<?php
declare(strict_types=1);
function riskyOperation(): mixed
{
// 可能抛出 TypeError(Error 子类)
$value = (int) 'not_a_number';
// 可能抛出 Exception
throw new \RuntimeException('操作失败');
}
try {
riskyOperation();
} catch (\Throwable $e) {
// 捕获所有 Error 和 Exception
echo '捕获到 Throwable: ' . get_class($e) . PHP_EOL;
echo '消息: ' . $e->getMessage() . PHP_EOL;
}Throwable 类型声明
<?php
declare(strict_types=1);
// 函数参数使用 Throwable 类型声明
function logException(\Throwable $e, string $context = ''): void
{
$logEntry = sprintf(
"[%s] %s in %s:%d\n %s",
date('Y-m-d H:i:s'),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
);
if ($context !== '') {
$logEntry = "[{$context}] " . $logEntry;
}
error_log($logEntry);
}
// 可以传入 Error 或 Exception
try {
$data = json_decode('{"invalid"', true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable $e) {
logException($e, 'JSON_PARSE');
}Throwable 与 finally 的配合
<?php
declare(strict_types=1);
function transactionalOperation(): string
{
$connected = false;
try {
// 模拟连接数据库
$connected = true;
// 执行可能抛出 Error 或 Exception 的操作
$result = intdiv(10, 0); // DivisionByZeroError
return '操作成功';
} catch (\Throwable $e) {
echo "捕获异常: {$e->getMessage()}" . PHP_EOL;
return '操作失败';
} finally {
// 无论是否抛出异常都会执行
if ($connected) {
echo '清理:关闭连接' . PHP_EOL;
}
echo 'finally 块执行完毕' . PHP_EOL;
}
}
echo transactionalOperation();
// 输出:
// 捕获异常: Division by zero
// 清理:关闭连接
// finally 块执行完毕
// 操作失败Throwable 在异常链中的使用
<?php
declare(strict_types=1);
class Application
{
public static function run(): void
{
try {
self::boot();
self::handleRequest();
} catch (\Throwable $e) {
self::handleException($e);
}
}
private static function boot(): void
{
// 模拟启动时加载配置
$config = json_decode('bad json', true, 512, JSON_THROW_ON_ERROR);
}
private static function handleRequest(): void
{
echo '处理请求...';
}
private static function handleException(\Throwable $e): void
{
// 遍历完整异常链
$chain = [];
$current = $e;
while ($current !== null) {
$chain[] = sprintf(
'[%s] %s in %s:%d',
get_class($current),
$current->getMessage(),
basename($current->getFile()),
$current->getLine()
);
$current = $current->getPrevious();
}
foreach ($chain as $i => $entry) {
echo ($i + 1) . '. ' . $entry . PHP_EOL;
}
}
}
Application::run();详细说明
Throwable 接口方法详解
| 方法 | 返回类型 | 说明 | 示例 |
|---|---|---|---|
getMessage() | string | 异常消息 | "Division by zero" |
getCode() | int | 异常代码 | 1001 |
getFile() | string | 异常发生的文件路径 | "/app/src/Service.php" |
getLine() | int | 异常发生的行号 | 42 |
getTrace() | array | 堆栈跟踪数组(结构化) | [...] |
getTraceAsString() | string | 格式化的堆栈跟踪字符串 | "#0 ..." |
getPrevious() | ?Throwable | 异常链中的前一个异常 | null 或 Throwable |
__toString() | string | 异常的字符串表示 | Exception: ... |
getTrace() 返回的结构
<?php
declare(strict_types=1);
function inner(): void
{
throw new \RuntimeException('内部错误');
}
function middle(): void
{
inner();
}
function outer(): void
{
middle();
}
try {
outer();
} catch (\Throwable $e) {
$trace = $e->getTrace();
// $trace 是一个数组,每个元素代表一个调用栈帧:
// [
// [
// 'file' => '/path/to/file.php',
// 'line' => 42,
// 'function' => 'inner',
// 'class' => null, // 如果是静态方法则为类名
// 'type' => null, // '::' 或 '->'
// 'args' => [], // 函数参数
// ],
// ...
// ]
foreach ($trace as $i => $frame) {
$func = $frame['class'] ?? '' . ($frame['type'] ?? '') . $frame['function'];
$file = $frame['file'] ?? 'internal';
$line = $frame['line'] ?? 0;
echo "#{$i} {$func} in {$file}:{$line}" . PHP_EOL;
}
}PHP 5.x 与 PHP 7.0+ 的异常处理差异
| 特性 | PHP 5.x | PHP 7.0+ |
|---|---|---|
| Fatal Error 处理 | 不可捕获,脚本终止 | 转为 Error,可通过 catch 捕获 |
try/catch 捕获范围 | 仅 Exception | 所有 Throwable(Error + Exception) |
| 统一捕获 | 无 | catch (\Throwable $e) |
| 类型声明 | 不支持 Throwable 参数类型 | 支持 Throwable 类型声明 |
set_exception_handler | 只接收 Exception | 接收 Throwable |
迁移注意
如果代码从 PHP 5.x 升级到 7.0+,某些原本导致 Fatal Error 的代码现在会抛出 Error。如果不捕获这些 Error,它们的行为与之前相同(未被捕获的 Throwable 会导致 Fatal Error)。
实战示例
全局异常处理器模板
<?php
declare(strict_types=1);
class GlobalExceptionHandler
{
private const LOG_FILE = '/var/log/app/error.log';
public static function register(): void
{
// 错误转 Throwable
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
if (!(error_reporting() & $severity)) {
return false;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
// 全局 Throwable 处理
set_exception_handler(function (\Throwable $e): void {
self::log($e);
self::respond($e);
});
// 致命错误兜底
register_shutdown_function(function (): void {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR], true)) {
self::logFatal($error);
}
});
}
private static function log(\Throwable $e): void
{
$entry = sprintf(
"[%s] [%s] %s in %s:%d\n%s",
date('Y-m-d H:i:s'),
self::classify($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
);
file_put_contents(self::LOG_FILE, $entry . "\n", FILE_APPEND | LOCK_EX);
}
private static function classify(\Throwable $e): string
{
return match (true) {
$e instanceof \Error => 'ERROR',
$e instanceof \LogicException => 'LOGIC',
$e instanceof \RuntimeException => 'RUNTIME',
default => 'EXCEPTION',
};
}
private static function respond(\Throwable $e): void
{
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "Error: {$e->getMessage()}\n");
exit(1);
}
http_response_code(500);
echo json_encode(['error' => '服务器内部错误', 'code' => $e->getCode()]);
}
private static function logFatal(array $error): void
{
$entry = sprintf(
"[FATAL] %s in %s:%d",
$error['message'],
$error['file'],
$error['line']
);
file_put_contents(self::LOG_FILE, $entry . "\n", FILE_APPEND | LOCK_EX);
}
}
GlobalExceptionHandler::register();注意事项
catch (\Throwable $e)会捕获所有:包括Error和Exception,用于最外层的兜底捕获。不要在业务逻辑内部滥用——应捕获具体的异常类型。Throwable 不能被用户类实现:如果需要自定义异常,必须
extends Exception或其子类,不能implements Throwable。getTrace() 可能包含敏感数据:堆栈跟踪中的
args字段会包含函数参数的实际值,可能包含密码、密钥等敏感信息。__toString()可能抛出异常:如果在__toString()方法内部产生异常,会导致 Fatal Error。Throwable::__toString()本身不会抛出异常。未捕获的 Throwable 仍然是 Fatal Error:如果
Throwable被抛出但未被捕获,PHP 仍然会产生 Fatal Error 并终止脚本。
最佳实践
推荐做法
- 外层用 Throwable,内层用具体类型:框架入口
catch (\Throwable $e),业务逻辑catch (\InvalidArgumentException $e) - 类型声明使用 Throwable:日志函数、异常处理器参数使用
\Throwable类型声明 - 区分 Error 和 Exception:
Error通常需要修复代码,Exception通常需要处理业务逻辑 - 生产环境过滤堆栈参数:记录日志前,清理
getTrace()中的敏感参数 - 使用 set_exception_handler 处理未捕获异常:注册全局处理器,防止堆栈信息暴露给用户