PHP异常处理层级怎么设计

wen PHP项目 2

本文目录导读:

PHP异常处理层级怎么设计

  1. 基础架构:三层核心异常
  2. 具体类结构设计
  3. 业务层级细分(最常用)
  4. 基础设施层级(可选)
  5. 全局异常处理(以Laravel为例)
  6. 优雅的“熔断”机制:避免多层嵌套Try-Catch
  7. 设计原则总结
  8. 一些额外建议

在PHP中设计异常处理层级,主要是为了将不同的错误类型进行逻辑分组,从而能在不同的层级(如控制器、服务层、全局中间件)进行差异化处理(比如返回不同的HTTP状态码、不同的错误消息格式)。

以下是一套经过实践检验的、结构清晰的异常处理层级设计方案,适用于现代PHP框架(如Laravel、Symfony或ThinkPHP)。


基础架构:三层核心异常

建议将异常分为三个核心层级,每个层级都有明确的职责:

// 1. 顶层:系统/框架级异常 (不应被业务代码捕获,应由框架处理)
//    如:PDOException, TypeError, ParseError
//    --> 交给全局异常处理器记录日志并返回500
// 2. 中层:业务逻辑异常 (最核心)
//    如:用户不存在、订单已支付、库存不足
//    --> 根据子类类型返回不同HTTP状态码
// 3. 底层:基础设施异常 (第三方服务、数据库、缓存)
//    如:Redis连接失败、API调用超时、文件写入失败
//    --> 通常需要重试或降级,返回503

具体类结构设计

创建一个继承自 \ExceptionBaseException,然后在此之下建立子层级。

<?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层:如果遇到问题,只管 throwthrow 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记录日志并返回通用错误 必须

一些额外建议

  1. 不要使用Exception作为业务控制流:即不要用 try { ... } catch (\Exception $e) { return 'something'; } 来代替 if-else 逻辑判断。
  2. 错误码保持一致性:建议在 config/errors.php 中集中管理 errorCode 的意义,1000=通用业务错误,1001=资源未找到,1002=未授权。
  3. 区分“可恢复”与“不可恢复”异常:基础设施异常(如数据库断连)通常是不可恢复的,应该直接上报运维;而业务异常(如用户输入错误)是预期的,直接返回给前端即可。
  4. 利用IDE提示:将所有的业务异常声明在 @throws 注解中,@throws ResourceNotFoundException,便于团队成员理解方法风险。

抱歉,评论功能暂时关闭!