Throwable 接口
概述
Throwable 是 PHP 异常和错误体系的顶层接口,是 Error 和 Exception 的共同父接口。从 PHP 7.0 开始,所有的异常和错误都实现了 Throwable 接口,使得 catch (Throwable $e) 能够捕获所有可抛出的对象。
Throwable 不能被用户自定义的类直接实现(PHP 内部保留),用户只能通过继承 Exception 或 Error 来创建自定义的异常/错误类型。
版本说明
Throwable 接口在 PHP 7.0 中引入。PHP 5.x 中的 Exception 和 Error 没有共同父接口。PHP 8.0+ 中 Throwable 的地位更加重要,因为所有错误默认都转为异常抛出。
基础概念
接口定义
php
<?php
declare(strict_types=1);
// Throwable 接口定义(简化)
// interface Throwable extends Stringable
// {
// 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;
// }Throwable 层次结构
Throwable
├── Error(PHP 内部错误)
│ ├── TypeError
│ ├── ValueError
│ ├── ArgumentCountError
│ ├── ArithmeticError
│ │ └── DivisionByZeroError
│ ├── CompileError
│ │ └── ParseError
│ ├── UnhandledMatchError
│ └── FiberError
│
└── Exception(用户异常)
├── ErrorException
├── LogicException
│ ├── InvalidArgumentException
│ ├── OutOfBoundsException
│ ├── OutOfRangeException
│ ├── RuntimeException
│ ├── OverflowException
│ ├── RangeException
│ ├── UnderflowException
│ ├── UnexpectedValueException
│ └── DomainException
└── RuntimeException
├── PDOException
├── RuntimeException 子类...
└── ...语法与代码
catch(Throwable) 捕获所有
php
<?php
declare(strict_types=1);
function riskyOperation(): string
{
$result = 1 / 0; // 抛出 DivisionByZeroError
return "操作成功";
}
try {
riskyOperation();
} catch (Throwable $e) {
echo "捕获到 " . get_class($e) . "\n";
echo "消息: {$e->getMessage()}\n";
echo "文件: {$e->getFile()}:{$e->getLine()}\n";
}
// 输出:
// 捕获到 DivisionByZeroError
// 消息: Division by zero
// 文件: /path/to/file.php:8不能被用户类直接实现
php
<?php
declare(strict_types=1);
// 编译错误!不能直接实现 Throwable
// class MyThrowable implements 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 {}
// }
// Fatal error: Class MyThrowable cannot implement interface Throwable
// 正确方式:继承 Exception 或 Error
class AppException extends Exception
{
// 可以添加自定义属性和方法
}
class AppError extends Error
{
// 继承 Error 很少使用,一般用 Exception
}Throwable 的方法
php
<?php
declare(strict_types=1);
function demonstrateThrowableMethods(): void
{
try {
$array = [1, 2, 3];
echo $array[10]; // 越界访问不会抛异常(PHP 8.1 之前)
// 但类型错误会抛异常
strlen([]); // TypeError
} catch (Throwable $e) {
echo "类名: " . get_class($e) . "\n";
echo "消息: " . $e->getMessage() . "\n";
echo "错误码: " . $e->getCode() . "\n";
echo "文件: " . $e->getFile() . "\n";
echo "行号: " . $e->getLine() . "\n";
echo "堆栈跟踪:\n";
echo $e->getTraceAsString();
// 之前的异常(异常链)
$previous = $e->getPrevious();
if ($previous !== null) {
echo "前一个异常: " . $previous->getMessage() . "\n";
}
// 字符串表示(Stringable)
echo $e->__toString();
}
}
demonstrateThrowableMethods();详细说明
PHP 7 前后的区别
php
<?php
declare(strict_types=1);
// PHP 5.x 中:错误和异常是两套完全不同的机制
// - Error 是错误,不能被 try/catch 捕获(需要 set_error_handler)
// - Exception 是异常,可以被 try/catch 捕获
// PHP 7.0+ 中:统一为 Throwable
// - Error 实现 Throwable,可以被 try/catch 捕获
// - Exception 实现 Throwable,可以被 try/catch 捕获
// PHP 7.0+ 兼容方案
function globalErrorHandler(int $errno, string $errstr, string $errfile, int $errline): bool
{
// 将 Error 转为 ErrorException 以便统一处理
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler('globalErrorHandler');Error 和 Exception 的区别
php
<?php
declare(strict_types=1);
// Error:PHP 内部错误,通常不应被捕获和忽略
// - 表示程序存在严重问题
// - 捕获后通常需要记录日志并终止程序
// - 例如:TypeError, ParseError, DivisionByZeroError
// Exception:业务异常,表示可预期的异常情况
// - 表示程序遇到了可处理的异常情况
// - 捕获后可以恢复程序执行
// - 例如:InvalidArgumentException, RuntimeException
function divide(int $a, int $b): float
{
if ($b === 0) {
// 业务异常:使用 Exception
throw new InvalidArgumentException('除数不能为零');
}
return $a / $b;
}
try {
echo divide(10, 0);
} catch (InvalidArgumentException $e) {
// 处理业务异常
echo "业务错误: {$e->getMessage()}\n";
} catch (Throwable $e) {
// 兜底捕获
echo "未知错误: {$e->getMessage()}\n";
throw $e; // 非 Exception 错误应继续抛出
}异常链(Previous Exception)
php
<?php
declare(strict_types=1);
class UserService
{
/**
* @throws RuntimeException
*/
public function createUser(string $email, string $name): int
{
try {
$this->validateEmail($email);
return $this->insertUser($email, $name);
} catch (InvalidArgumentException $e) {
// 将验证异常包装为业务异常,保留原始异常
throw new RuntimeException(
"创建用户失败: {$e->getMessage()}",
0,
$e // 传入 previous 异常
);
}
}
private function validateEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("邮箱格式无效: {$email}");
}
}
private function insertUser(string $email, string $name): int
{
// 模拟数据库插入
return 1;
}
}
try {
$service = new UserService();
$service->createUser('invalid-email', 'Test');
} catch (RuntimeException $e) {
echo "外层异常: {$e->getMessage()}\n";
// 遍历异常链
$current = $e;
$depth = 0;
while ($current !== null) {
$indent = str_repeat(' ', $depth);
echo "{$indent}→ " . get_class($current) . ": {$current->getMessage()}\n";
$current = $current->getPrevious();
$depth++;
}
}实战示例
统一异常处理器
php
<?php
declare(strict_types=1);
class ExceptionHandler
{
public function handle(Throwable $e): void
{
$isError = $e instanceof Error;
$level = $isError ? 'ERROR' : 'EXCEPTION';
$this->log($level, $e);
if ($isError) {
// Error 通常需要终止程序
$this->renderErrorPage($e);
exit(1);
}
// Exception 可以恢复
$this->renderExceptionPage($e);
}
private function log(string $level, Throwable $e): void
{
$message = sprintf(
"[%s] [%s] %s in %s:%d\nStack Trace:\n%s",
date('Y-m-d H:i:s'),
$level,
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
);
error_log($message);
}
private function renderErrorPage(Throwable $e): void
{
http_response_code(500);
echo "服务器内部错误";
}
private function renderExceptionPage(Throwable $e): void
{
http_response_code(400);
echo "请求错误: {$e->getMessage()}";
}
}
// 注册全局异常处理器
set_exception_handler(function (Throwable $e): void {
$handler = new ExceptionHandler();
$handler->handle($e);
});注意事项
不要捕获所有 Throwable 后忽略
php
<?php
declare(strict_types=1);
// 反面示例:捕获所有异常后忽略
function badPractice(): mixed
{
try {
return riskyCalculation();
} catch (Throwable $e) {
// 危险!捕获了所有异常和错误,包括 OutOfMemoryError
return null;
}
}
// 正面示例:区分 Error 和 Exception
function goodPractice(): mixed
{
try {
return riskyCalculation();
} catch (InvalidArgumentException $e) {
// 处理特定的业务异常
return null;
} catch (RuntimeException $e) {
// 处理运行时异常
throw new RuntimeException("操作失败", 0, $e);
}
// 不捕获 Error,让它向上传播
}getPrevious() 的使用
php
<?php
declare(strict_types=1);
// 注意:getPrevious() 返回的是构造函数中传入的第三个参数
// 如果使用 throw new Exception('msg', 0, $previous),则 getPrevious() 返回 $previous
// 如果只是 throw new Exception('msg'),则 getPrevious() 返回 null
try {
throw new Exception('第三层异常');
} catch (Exception $e3) {
try {
throw new RuntimeException('第二层异常', 0, $e3);
} catch (RuntimeException $e2) {
throw new RuntimeException('第一层异常', 0, $e2);
}
} catch (Throwable $e) {
// 遍历异常链
do {
echo get_class($e) . ": " . $e->getMessage() . "\n";
} while (($e = $e->getPrevious()) !== null);
}最佳实践
- 仅在顶层使用 catch(Throwable):在框架/应用的全局异常处理器中使用,业务代码应捕获具体异常类型
- 区分 Error 和 Exception:Error 表示程序级错误,Exception 表示业务级异常
- 保持异常链完整:使用
new Exception('msg', 0, $previous)保留原始异常信息 - Error 通常不应被忽略:捕获 Error 后应记录日志并终止或重新抛出
- 自定义异常继承 Exception:不要继承 Error,除非明确需要创建自定义错误类型
php
<?php
declare(strict_types=1);
// 全局异常处理器模板(推荐放在入口文件)
set_exception_handler(function (Throwable $throwable): void {
$context = [
'type' => get_class($throwable),
'message' => $throwable->getMessage(),
'code' => $throwable->getCode(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'trace' => $throwable->getTraceAsString(),
];
// 记录日志
error_log(json_encode($context, JSON_UNESCAPED_UNICODE));
// 区分环境返回不同响应
if (PHP_SAPI === 'cli') {
fwrite(STDERR, "错误: {$throwable->getMessage()}\n");
exit(1);
}
http_response_code(500);
echo json_encode(['error' => '服务器内部错误']);
});