本文目录导读:

在PHP中设计异常处理层级,主要是为了将不同的错误类型进行逻辑分组,从而能在不同的层级(如控制器、服务层、全局中间件)进行差异化处理(比如返回不同的HTTP状态码、不同的错误消息格式)。
以下是一套经过实践检验的、结构清晰的异常处理层级设计方案,适用于现代PHP框架(如Laravel、Symfony或ThinkPHP)。
基础架构:三层核心异常
建议将异常分为三个核心层级,每个层级都有明确的职责:
// 1. 顶层:系统/框架级异常 (不应被业务代码捕获,应由框架处理) // 如:PDOException, TypeError, ParseError // --> 交给全局异常处理器记录日志并返回500 // 2. 中层:业务逻辑异常 (最核心) // 如:用户不存在、订单已支付、库存不足 // --> 根据子类类型返回不同HTTP状态码 // 3. 底层:基础设施异常 (第三方服务、数据库、缓存) // 如:Redis连接失败、API调用超时、文件写入失败 // --> 通常需要重试或降级,返回503
具体类结构设计
创建一个继承自 \Exception 的 BaseException,然后在此之下建立子层级。
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
/**
* 应用基础异常(所有自定义异常的父类)
*/
class BaseException extends Exception
{
// 期望的HTTP状态码
protected int $statusCode = 500;
// 错误码(用于前端或API识别具体错误,非HTTP状态码)
protected int $errorCode = 0;
// 错误数据(可选,用于携带验证失败等详细信息)
protected ?array $data = null;
public function __construct(
string $message = "",
int $errorCode = 0,
?array $data = null,
int $statusCode = 500,
?Throwable $previous = null
) {
parent::__construct($message, $errorCode, $previous);
$this->errorCode = $errorCode;
$this->data = $data;
$this->statusCode = $statusCode;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function getData(): ?array
{
return $this->data;
}
}
业务层级细分(最常用)
<?php
namespace App\Exceptions\Business;
use App\Exceptions\BaseException;
// ---- 业务通用 ----
class BusinessException extends BaseException
{
public function __construct(string $message = "业务处理失败", int $errorCode = 1000, ?array $data = null, ?Throwable $previous = null)
{
parent::__construct($message, $errorCode, $data, 400, $previous);
}
}
// ---- 资源相关 ----
class ResourceNotFoundException extends BaseException
{
public function __construct(string $message = "资源未找到", int $errorCode = 1001, ?array $data = null, ?Throwable $previous = null)
{
parent::__construct($message, $errorCode, $data, 404, $previous);
}
}
// ---- 权限相关 ----
class UnauthorizedException extends BaseException
{
public function __construct(string $message = "未授权", int $errorCode = 1002, ?array $data = null, ?Throwable $previous = null)
{
parent::__construct($message, $errorCode, $data, 401, $previous);
}
}
class ForbiddenException extends BaseException
{
public function __construct(string $message = "无权限访问", int $errorCode = 1003, ?array $data = null, ?Throwable $previous = null)
{
parent::__construct($message, $errorCode, $data, 403, $previous);
}
}
// ---- 验证错误 ----
class ValidationException extends BaseException
{
public function __construct(string $message = "数据验证失败", int $errorCode = 1004, ?array $data = null, ?Throwable $previous = null)
{
parent::__construct($message, $errorCode, $data, 422, $previous);
}
}
// ---- 并发/状态冲突 ----
class ConflictException extends BaseException
{
public function __construct(string $message = "资源冲突", int $errorCode = 1005, ?array $data = null, ?Throwable $previous = null)
{
parent::__construct($message, $errorCode, $data, 409, $previous);
}
}
// ---- 限流/频率限制 ----
class TooManyRequestsException extends BaseException
{
public function __construct(string $message = "请求过于频繁", int $errorCode = 1006, ?array $data = null, ?Throwable $previous = null)
{
parent::__construct($message, $errorCode, $data, 429, $previous);
}
}
基础设施层级(可选)
<?php
namespace App\Exceptions\Infrastructure;
use App\Exceptions\BaseException;
// 数据库连接故障
class DatabaseConnectionException extends BaseException
{
public function __construct(string $message = "数据库服务不可用")
{
parent::__construct($message, 2001, null, 503);
}
}
// 第三方API调用失败
class ExternalServiceException extends BaseException
{
public function __construct(string $message = "外部服务调用失败")
{
parent::__construct($message, 2002, null, 502);
}
}
// 缓存服务异常
class CacheException extends BaseException
{
public function __construct(string $message = "缓存服务异常")
{
parent::__construct($message, 2003, null, 503);
}
}
全局异常处理(以Laravel为例)
这是整个体系发挥作用的关键环节,在 App\Exceptions\Handler 中注册渲染逻辑:
<?php
namespace App\Exceptions;
use App\Exceptions\Business\{ValidationException, ResourceNotFoundException};
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
protected $dontReport = [
// 不需要记录到日志的异常(如验证错误、404)
ValidationException::class,
ResourceNotFoundException::class,
// ...
];
public function register()
{
$this->reportable(function (Throwable $e) {
// 可以在这里加入Sentry或日志记录逻辑
// 但注意不要重复记录 $dontReport 里的异常
});
$this->renderable(function (\App\Exceptions\BaseException $e, $request) {
// 处理所有继承了 BaseException 的自定义异常
if ($request->expectsJson() || $request->is('api/*')) {
return response()->json([
'code' => $e->getCode() ?: $e->getStatusCode(), // 自定义业务码
'message' => $e->getMessage(),
'data' => $e->getData(), // 比如验证错误的详细字段
], $e->getStatusCode());
}
// 如果是Web请求,可以跳转到错误页面
return response()->view("errors.{$e->getStatusCode()}", [
'message' => $e->getMessage()
], $e->getStatusCode());
});
// 兜底:处理所有未捕获的PHP异常(比如500错误)
$this->renderable(function (Throwable $e, $request) {
if ($request->expectsJson() || $request->is('api/*')) {
return response()->json([
'code' => 500,
'message' => '服务器内部错误',
'data' => null,
], 500);
}
return response()->view('errors.500', [], 500);
});
}
}
优雅的“熔断”机制:避免多层嵌套Try-Catch
设计完层级后,不要在业务代码里写很多 try { ... } catch (SpecificException $e) { ... }。
利用 异常冒泡 设计:
- Service层:如果遇到问题,只管
throw(throw new ResourceNotFoundException())。 - Controller层:绝大多数情况下不需要catch,异常自动冒泡到全局处理器。
- 事务层:仅在需要回滚数据库事务的地方 catch,然后重新抛出(
throw $e)。
// Service层的推荐写法 —— 不捕获
public function getUser(int $id): User
{
$user = User::find($id);
if (!$user) {
throw new ResourceNotFoundException("用户ID:{$id} 不存在");
}
return $user;
}
// Controller层 —— 也不需要写try-catch
public function show(int $id)
{
$user = $this->userService->getUser($id);
return response()->json($user);
}
// 异常直接冒泡到全局Handler,返回 404 JSON
设计原则总结
| 层级 | 代表异常 | HTTP状态码 | 处理方式 | 日志记录 |
|---|---|---|---|---|
| 业务层 | ValidationException, ResourceNotFoundException |
4xx | 全局Handler直接返回错误信息 | 可选(低优先级) |
| 业务层 | ConflictException, BusinessException |
409/400 | 全局Handler返回错误信息 | 可选 |
| 基础设施层 | DatabaseConnectionException |
503 | 全局Handler返回错误并记录日志 | 必须 |
| 系统层(原生) | PDOException, Error |
500 | 全局Handler记录日志并返回通用错误 | 必须 |
一些额外建议
- 不要使用Exception作为业务控制流:即不要用
try { ... } catch (\Exception $e) { return 'something'; }来代替if-else逻辑判断。 - 错误码保持一致性:建议在
config/errors.php中集中管理errorCode的意义,1000=通用业务错误,1001=资源未找到,1002=未授权。 - 区分“可恢复”与“不可恢复”异常:基础设施异常(如数据库断连)通常是不可恢复的,应该直接上报运维;而业务异常(如用户输入错误)是预期的,直接返回给前端即可。
- 利用IDE提示:将所有的业务异常声明在
@throws注解中,@throws ResourceNotFoundException,便于团队成员理解方法风险。