异常类层次结构
概述
PHP 的异常体系基于 Throwable 接口构建,分为 Error 和 Exception 两大分支。Error 表示 PHP 引擎层面的错误(通常不可恢复),Exception 表示应用层面的异常(通常可恢复)。
理解异常层次结构有助于正确选择异常类型、编写精确的 catch 语句,以及在合适的位置放置自定义异常。
版本说明
- PHP 7.0 引入 Throwable、Error 及其子类
- PHP 8.0 引入 ValueError、UnhandledMatchError
- PHP 8.1 引入 FiberError
基础概念
完整层次结构树
Throwable (接口)
├── Error
│ ├── ArithmeticError
│ │ ├── DivisionByZeroError
│ │ └── AssertionError (PHP 8.0+)
│ ├── AssertionError
│ ├── CompileError
│ │ └── ParseError
│ ├── TypeError
│ ├── ValueError
│ ├── ArgumentCountError
│ ├── UnhandledMatchError
│ ├── FiberError
│ └── ...
│
└── Exception
├── ErrorException
├── LogicException
│ ├── BadFunctionCallException
│ │ └── BadMethodCallException
│ ├── DomainException
│ ├── InvalidArgumentException
│ ├── LengthException
│ ├── RangeException
│ └── OverflowException
│ └── UnderflowException
└── RuntimeException
├── OutOfBoundsException
│ ├── OutOfRangeException
│ └── InfiniteLoopException
├── OverflowException
├── UnexpectedValueException
├── RuntimeException 子类...
└── ...Error 分支 vs Exception 分支
| 特性 | Error | Exception |
|---|---|---|
| 用途 | PHP 内部错误 | 应用层异常 |
| 可恢复性 | 通常不可恢复 | 通常可恢复 |
| 用户自定义 | 不推荐(继承 Error) | 推荐(继承 Exception) |
| try/catch | 可以 | 可以 |
| 示例 | TypeError, ParseError | RuntimeException, InvalidArgumentException |
语法与代码
Error 分支
php
<?php
declare(strict_types=1);
// TypeError — 类型不匹配
function expectString(string $value): string
{
return $value;
}
try {
expectString(42); // TypeError
} catch (TypeError $e) {
echo "TypeError: " . $e->getMessage();
}
// ValueError — 值不正确(PHP 8.0+)
try {
$result = intdiv(PHP_INT_MIN, -1); // 算术溢出
} catch (ValueError|ArithmeticError $e) {
echo get_class($e) . ": " . $e->getMessage();
}
// ArgumentCountError — 参数数量错误
function twoParams(int $a, int $b): void {}
try {
twoParams(1); // ArgumentCountError
} catch (ArgumentCountError $e) {
echo "参数数量错误: " . $e->getMessage();
}
// ParseError — 语法错误
try {
eval('invalid code ===');
} catch (ParseError $e) {
echo "语法错误: " . $e->getMessage();
}Exception 分支 — LogicException 系列
php
<?php
declare(strict_types=1);
// LogicException — 编程逻辑错误(应该在编码阶段发现)
// DomainException — 值不在有效域内
function setAge(int $age): void
{
if ($age < 0 || $age > 200) {
throw new DomainException("年龄 {$age} 不在有效范围内");
}
}
// InvalidArgumentException — 参数无效
function setName(string $name): void
{
if (strlen(trim($name)) === 0) {
throw new InvalidArgumentException('名称不能为空');
}
}
// BadMethodCallException — 调用了不存在的方法
class Proxy
{
public function __call(string $method, array $args): mixed
{
throw new BadMethodCallException("方法 {$method} 不存在");
}
}
// LengthException — 长度超出范围
function addToBuffer(string $item, array $buffer, int $maxSize): void
{
if (count($buffer) >= $maxSize) {
throw new LengthException("缓冲区已满 (最大: {$maxSize})");
}
$buffer[] = $item;
}Exception 分支 — RuntimeException 系列
php
<?php
declare(strict_types=1);
// RuntimeException — 运行时异常(不可预测的错误)
// OutOfBoundsException — 索引越界
function getFromList(array $list, int $index): mixed
{
if (!isset($list[$index])) {
throw new OutOfBoundsException("索引 {$index} 越界");
}
return $list[$index];
}
// UnexpectedValueException — 值不符合预期
function parseConfigValue(mixed $value): string
{
if (!is_string($value)) {
throw new UnexpectedValueException('配置值必须是字符串');
}
return $value;
}
// OverflowException — 数值溢出
function safeAdd(int $a, int $b): int
{
$result = $a + $b;
if ($result > PHP_INT_MAX) {
throw new OverflowException('加法结果溢出');
}
return $result;
}详细说明
自定义异常的位置
php
<?php
declare(strict_types=1);
// 自定义异常应继承 Exception(不是 Error)
// 选择继承哪个基类取决于异常的性质
// 1. 业务逻辑异常 → 继承 Exception 或 LogicException
class InvalidOrderException extends LogicException
{
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
// 2. 运行时异常 → 继承 RuntimeException
class DatabaseConnectionException extends RuntimeException
{
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
// 3. 领域特定异常 → 继承具体的 SPL 异常
class EntityNotFoundException extends OutOfBoundsException
{
private string $entityType;
private string $entityId;
public function __construct(string $entityType, string $entityId)
{
$this->entityType = $entityType;
$this->entityId = $entityId;
parent::__construct("{$entityType}({$entityId}) 未找到");
}
public function entityType(): string { return $this->entityType; }
public function entityId(): string { return $this->entityId; }
}异常类的通用属性
php
<?php
declare(strict_types=1);
// 所有 Throwable 都有这些方法
class AppException extends Exception
{
public function __construct(
string $message = '',
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
$e = new AppException('测试异常', 1001);
// Throwable 接口的方法
$e->getMessage(); // 异常消息
$e->getCode(); // 异常码
$e->getFile(); // 抛出位置文件
$e->getLine(); // 抛出位置行号
$e->getTrace(); // 堆栈跟踪数组
$e->getTraceAsString(); // 堆栈跟踪字符串
$e->getPrevious(); // 前一个异常
$e->__toString(); // 格式化字符串(Stringable)异常捕获的最佳顺序
php
<?php
declare(strict_types=1);
// 正确的 catch 顺序
function handleErrors(): void
{
try {
riskyOperation();
} catch (InvalidArgumentException $e) {
// 最具体的异常(子类)
} catch (RuntimeException $e) {
// 较通用的异常(父类)
} catch (Throwable $e) {
// 最通用的兜底
}
}
// 层次结构决定了 catch 的匹配:
// InvalidArgumentException extends LogicException extends Exception implements Throwable
// 如果抛出 InvalidArgumentException,会被第一个 catch 捕获
// 如果抛出 RuntimeException,会被第二个 catch 捕获
// 如果抛出 TypeError(Error 分支),会被第三个 catch 捕获实战示例
领域异常体系
php
<?php
declare(strict_types=1);
// 基础异常
abstract class AppBaseException extends Exception {}
// 业务异常
class BusinessException extends AppBaseException {}
class ValidationException extends BusinessException
{
/** @var array<string, string> */
private array $errors;
/**
* @param array<string, string> $errors
*/
public function __construct(array $errors, ?Throwable $previous = null)
{
$this->errors = $errors;
parent::__construct('数据验证失败', 422, $previous);
}
/**
* @return array<string, string>
*/
public function errors(): array
{
return $this->errors;
}
}
class EntityNotFoundException extends BusinessException
{
public function __construct(string $entity, string $id, ?Throwable $previous = null)
{
parent::__construct("实体 {$entity}(ID: {$id}) 未找到", 404, $previous);
}
}
class DuplicateEntityException extends BusinessException
{
public function __construct(string $entity, string $key, ?Throwable $previous = null)
{
parent::__construct("实体 {$entity}({$key}) 已存在", 409, $previous);
}
}
// 基础设施异常
class InfrastructureException extends AppBaseException {}
class DatabaseException extends InfrastructureException
{
public function __construct(string $message, ?Throwable $previous = null)
{
parent::__construct($message, 500, $previous);
}
}
class CacheException extends InfrastructureException
{
public function __construct(string $message, ?Throwable $previous = null)
{
parent::__construct($message, 500, $previous);
}
}注意事项
不要捕获 Error 除非必要
php
<?php
declare(strict_types=1);
// 反面示例:捕获所有 Throwable 包括 Error
try {
riskyOperation();
} catch (Throwable $e) {
// 可能捕获了 OutOfMemoryError、TypeError 等不应忽略的错误
return null;
}
// 正面示例:只捕获特定异常
try {
riskyOperation();
} catch (BusinessException $e) {
// 处理业务异常
return fallbackValue($e);
}
// Error 类型不捕获,让它向上传播到全局处理器异常码的约定
php
<?php
declare(strict_types=1);
// 常用的异常码约定
class AppExceptionCodes
{
// 业务异常码
public const VALIDATION_FAILED = 422;
public const NOT_FOUND = 404;
public const CONFLICT = 409;
public const FORBIDDEN = 403;
public const UNAUTHORIZED = 401;
// 系统异常码
public const INTERNAL_ERROR = 500;
public const SERVICE_UNAVAILABLE = 503;
public const DB_ERROR = 500;
}
// 使用
throw new EntityNotFoundException('User', '123');
// code = 404(在构造函数中设定)最佳实践
- 自定义异常继承 Exception:不要继承 Error(除非创建自定义内部错误)
- 异常体系与业务领域对齐:创建有意义的异常子类
- 使用异常码:便于 API 返回和日志分析
- 保持异常消息信息丰富:包含足够上下文但不含敏感信息
- 合理的层次深度:2~3 层继承即可
php
<?php
declare(strict_types=1);
// 推荐的异常层次
// AppException (基类)
// ├── DomainException (领域异常)
// │ ├── UserNotFoundException
// │ └── InvalidStateException
// ├── ServiceException (服务异常)
// │ ├── PaymentFailedException
// │ └── ExternalApiException
// └── InfrastructureException (基础设施)
// ├── DatabaseException
// └── CacheException