本文目录导读:

在 PHP 项目中,错误(Error)与异常(Exception)的处理机制经历了多个版本的演变,形成了一个清晰的层级结构,理解这个层级对于编写健壮的代码和处理运行时问题至关重要。
以下是 PHP 中错误与异常的完整层级体系,以及在实际项目中的应用建议。
核心层级结构 (PHP 7+)
PHP 的异常和错误都继承自 Throwable 接口,这是所有可抛出的根接口。
graph TD
Throwable --> Exception
Throwable --> Error
subgraph Exception
Exception --> RuntimeException
Exception --> LogicException
RuntimeException --> PDOException
RuntimeException --> ....
LogicException --> InvalidArgumentException
LogicException --> ....
end
subgraph Error (PHP 7+)
Error --> TypeError
Error --> ParseError
Error --> ArithmeticError
Error --> AssertionError
Error --> CompileError
ArithmeticError --> DivisionByZeroError
end
Throwable 接口
- 定义:所有可抛出对象的基接口。
- 方法:
getMessage(),getCode(),getFile(),getLine(),getTrace(),getTraceAsString(),getPrevious()。 - 捕获:可以使用
catch (Throwable $e)来捕获所有错误和异常。
Exception 分支 (传统异常)
- 定义:程序运行过程中,逻辑上可预测、可处理的问题。
- 使用场景:数据库连接失败、文件不存在、用户输入验证不通过、API 调用返回错误等。
- 子类:
RuntimeException(运行时异常,如PDOException,OutOfBoundsException),LogicException(逻辑异常,如InvalidArgumentException,LengthException)。
Error 分支 (PHP 7+ 新引入)
- 定义:程序运行过程中,不可恢复的、系统级别的问题。
- 使用场景:内存耗尽、类型不匹配、语法解析错误等。
- 子类:
TypeError:函数参数或返回值类型不匹配。ParseError:include/require的文件中存在语法错误。ArithmeticError:数学运算错误(如DivisionByZeroError)。AssertionError:assert()断言失败(PHP 7.3+ 从ArithmeticError独立出来)。CompileError:编译时错误(如eval()中的错误)。
PHP 5.x 时代的遗留问题:传统错误
在 PHP 7 之前的版本中,没有 Error 类,大部分严重问题(如:E_WARNING, E_NOTICE, E_ERROR)是不能被 try-catch 捕获的,它们会直接触发 die() 或打印错误信息并继续执行。
PHP 5.x 的错误处理流程:
// 传统错误 (如:除以零,文件不存在警告)
set_error_handler(function($severity, $message, $file, $line) {
// 自定义错误处理逻辑
// 但不能捕获 E_ERROR, E_PARSE
throw new ErrorException($message, 0, $severity, $file, $line); // 转成异常
});
E_ERROR,E_PARSE,E_CORE_ERROR等严重错误无法被set_error_handler捕获,脚本会直接停止。- 几乎所有传统错误(如
E_WARNING,E_NOTICE)都可以通过set_error_handler转换为ErrorException异常。
PHP 7/8 的变革:Error 作为 Throwable
PHP 7 将大部分致命的、不可恢复的错误(如 E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE)提升为了 Error 类,使其成为 Throwable 的一部分。
关键变化:
- 致命错误可捕获:大多数传统上导致脚本终止的严重错误,现在变成了
Error对象,可以被try-catch捕获。 - 类型错误:
TypeError替代了之前的E_RECOVERABLE_ERROR。 - 丢弃了部分 E_STRICT 通知:部分
E_STRICT通知被移入E_NOTICE或直接删除。
PHP 7/8 的错误处理流程:
try {
// 可能会产生错误或异常的代码
undeclaredFunction(); // 产生 Error
$result = 1 / 0; // 产生 DivisionByZeroError (继承自 Error)
} catch (\Error $e) {
echo "捕获到 Error: " . $e->getMessage();
} catch (\Exception $e) {
echo "捕获到 Exception: " . $e->getMessage();
} catch (\Throwable $e) {
echo "捕获到任何可抛出的错误/异常: " . $e->getMessage();
}
重要警告: 虽然严重的致命错误可以被 catch 捕获,但这并不意味着它们是可以安全恢复的。ParseError 表明代码文件本身存在语法错误,捕获后很难继续正常运行;OutOfMemoryError 表明内存耗尽,捕获后可能无法进行任何有意义的内存操作。
项目中如何优雅地分层处理
在实际的 PHP 项目中(特别是 Laravel, Symfony, ThinkPHP 等框架),通常采用以下分层策略:
1 顶层全局异常处理器
在框架的入口处(如 index.php 或 public/index.php),设置一个全局的异常/错误处理器。
// 1. 设置错误处理器 (将传统错误转为异常)
set_error_handler(function ($severity, $message, $file, $line) {
// 如果错误等级在 error_reporting() 中,则抛出异常
if (error_reporting() & $severity) {
throw new \ErrorException($message, 0, $severity, $file, $line);
}
return false; // 继续 PHP 的标准错误处理
});
// 2. 设置异常/错误的捕获器 (顶级 try-catch 或 register_shutdown_function)
set_exception_handler(function (\Throwable $e) {
// 记录日志
Log::error($e->getMessage(), ['exception' => $e]);
// 返回统一的 JSON/页面错误信息给用户
if (php_sapi_name() === 'cli') {
echo "Fatal Error: " . $e->getMessage() . "\n";
} else {
http_response_code(500);
echo json_encode([
'error' => true,
'message' => '服务器内部错误,请稍后重试。'
]);
}
});
// 3. 捕获致命错误 (可选但很有效)
register_shutdown_function(function () {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
// 处理致命错误
Log::error('Fatal Error: ' . $error['message']);
http_response_code(500);
echo json_encode(['error' => true, 'message' => '服务器遇到致命错误。']);
}
});
2 业务逻辑中的分层
在业务的代码中(如 Controller, Service, Model),根据异常的严重性进行分层捕获:
- 顶层(Controller/路由回调):捕获所有
Throwable,返回统一的响应(HTTP 500 或友好提示)。 - 业务层(Service):
- 对于可预测的、可恢复的业务异常(如用户未登录、商品库存不足),主动抛出业务异常类(如
BusinessException extends \RuntimeException)。 - 对于系统异常(数据库连接失败、文件未找到、API 响应错误),通常不主动捕获,让其上升到顶层异常处理器。
- 对于可预测的、可恢复的业务异常(如用户未登录、商品库存不足),主动抛出业务异常类(如
- 基础设施层(Model, API Client):
- 捕获底层抛出的系统异常(如
PDOException,GuzzleException),记录日志,然后抛出一个统一的自定义运行时异常(如SystemException extends \RuntimeException),以隐藏底层细节。
- 捕获底层抛出的系统异常(如
推荐的业务异常定义:
// 1. 业务异常 (用户可感知,应该以 4xx 形式返回)
class BusinessException extends \RuntimeException {
protected $httpCode = 400; // HTTP 状态码
protected $errorCode = 'BUSINESS_ERROR';
// 构造函数设置用户友好的 message
}
// 2. 系统异常 (用户不可感知,应该以 500 形式返回,记录详细日志)
class SystemException extends \RuntimeException {
protected $httpCode = 500;
protected $errorCode = 'SYSTEM_ERROR';
// 构造函数中建议不暴露详细堆栈给用户
}
示例:Service 层抛出异常
class OrderService {
public function createOrder($userId, $productId, $quantity) {
try {
$user = $this->userRepo->find($userId);
if (!$user) {
throw new BusinessException('用户不存在');
}
$product = $this->productRepo->find($productId);
if (!$product || $product->stock < $quantity) {
throw new BusinessException('商品库存不足');
}
// ... 创建订单
} catch (\PDOException $e) {
// 记录数据库错误日志 (包含堆栈)
Log::error('Order creation failed due to database error: ' . $e->getMessage(), ['exception' => $e]);
// 抛出系统异常,隐藏底层细节
throw new SystemException('订单创建失败,请稍后重试');
}
}
}
Controller 处理:
public function createOrder(Request $request) {
try {
$this->orderService->createOrder(...);
return response()->json(['success' => true]);
} catch (BusinessException $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], $e->getHttpCode()); // 400 Bad Request
} catch (SystemException $e) {
return response()->json([
'success' => false,
'message' => '服务器内部错误'
], 500);
} catch (\Throwable $e) {
// 兜底:记录日志,返回未知错误
Log::emergency('Unhandled exception: ' . $e->getMessage(), ['exception' => $e]);
return response()->json([
'success' => false,
'message' => '未知错误'
], 500);
}
}
关键区别总结
| 特征 | Exception |
Error (PHP 7+) |
Legacy Error (E_WARNING, E_NOTICE) |
|---|---|---|---|
| 继承 | 实现 Throwable |
实现 Throwable |
不实现 Throwable |
| 可捕获 | 可以 | 可以 (PHP 7+) | 默认不可,但可通过 set_error_handler 转为 ErrorException |
| 严重性 | 可恢复/业务逻辑 | 不可恢复/系统级别 | 警告/通知级别 (通常不致命) |
| 典型场景 | 用户未登录、参数错误 | 内存溢出、类型错误、语法错误 | 变量未定义、文件未找到(非致命) |
| 是否应该 catch | 是 (应该捕获并处理) | 谨慎使用 (捕获后记录日志,但仍需终止脚本或保持状态) | 通常不应该 catch (代码层面应检查避免产生) |
最佳实践总结
- 始终使用
Throwable作为顶层捕获类型,确保不会漏掉错误。 - 区分业务和系统异常:使用自定义异常类来区分用户可感知的错误和系统内部错误。
- 不要用 Error 做流程控制:
TypeError和ParseError表示逻辑错误,应在开发阶段就修复,而不是靠运行时捕获。 - 利用
set_error_handler转传统错误为异常:统一错误处理入口。 - 记录日志:所有
catch分支,特别是捕获Error或系统异常时,务必记录详细的堆栈日志,以便排查。 - 全局异常处理器:在框架入口处设置最终兜底,防止未捕获的异常直接暴露给用户。