PHP 业务异常分类

wen PHP项目 3

本文目录导读:

PHP 业务异常分类

  1. 顶层分类(按错误性质)
  2. 业务异常细分(按商业场景)
  3. 核心落地代码(PHP 实现示例)
  4. 全局捕获与响应处理
  5. 研发团队必须遵守的规范
  6. 判定表

在 PHP 业务开发中,业务异常分类 的核心目标不是“捕获错误”,而是让错误处理变得可预测、可追踪、可恢复,一个好的异常分类体系,能让你的代码从“满是 if-else 的泥潭”进化为“优雅的契约式编程”。

以下是针对 PHP 后端的业务异常分类实战指南,分为顶层分类常见商业场景代码落地三个层面。


顶层分类(按错误性质)

这是最基础的分层,决定了异常是否应该被用户看到。

分类 核心特征 是否提示用户 是否记录日志 典型示例
系统异常 代码BUG、依赖宕机、不可控因素 ❌(显示友好提示) ✅(必须详细记录堆栈) 数据库连接失败、Redis超时、NPE、文件权限错误
业务异常 业务规则不允许、状态冲突、参数不满足 ✅(显示具体原因) ⚠️(一般记录,便于排查) 库存不足、余额不足、订单已取消、密码错误
参数校验异常 外部输入不合法(格式/范围) ✅(显示具体字段错误) ❌(通常不记录堆栈,防日志刷屏) 手机号格式错误、年龄超出范围、邮箱为空
安全/权限异常 未登录、无权限、越权操作 ✅(跳转登录/提示403) ✅(必须记录,防攻击) Token过期、无权访问该资源、CSRF校验失败

业务异常细分(按商业场景)

在顶层分类下,业务领域内通常根据业务状态机资源类型划分:

领域状态冲突异常

  • 场景:业务流程有先后顺序(如:已发货的订单不能再改地址)。
  • 类型OrderStateExceptionApprovalFlowException
  • 处理:通常需要捕获后提示“当前状态不允许此操作”。

资源耗尽/不足异常

  • 场景:库存扣减、金额扣减、积分消耗。
  • 类型InsufficientStockExceptionInsufficientBalanceExceptionQuotaLimitExceededException
  • 处理必须伴随事务回滚,且可能需要判断是否并发(乐观锁失败)。

依赖服务异常

  • 场景:调用第三方支付、短信、物流API失败。
  • 类型PaymentGatewayExceptionSmsServiceException
  • 处理必须设计重试机制,且对下游返回的错误进行分类(可重试 vs 不可重试)。

幂等性异常

  • 场景:客户端重复提交订单、重复点击支付按钮。
  • 类型DuplicateRequestExceptionIdempotencyConflictException
  • 处理:通常返回原来的成功结果,或提示“请勿重复操作”。

版本冲突异常

  • 场景:多人编辑同一数据(乐观锁)。
  • 类型OptimisticLockExceptionDataConflictException
  • 处理:提示用户“数据已更新,请刷新”。

核心落地代码(PHP 实现示例)

为了避免直接用 Exception 类,建议使用 异常接口 + 基础抽象类 设计。

步骤 1:定义接口(标记唯一性)

<?php
namespace App\Exceptions;
/**
 * 业务异常接口
 * 实现该接口的异常,会被全局处理器捕获并输出给用户(而非500错误)
 */
interface BusinessExceptionInterface
{
    public function getErrorCode(): string|int;
    public function getErrorMessage(): string;
    public function getExtraData(): array;
}

步骤 2:创建基础业务异常类

<?php
namespace App\Exceptions;
class BusinessException extends \RuntimeException implements BusinessExceptionInterface
{
    protected string|int $errorCode;
    protected array $extraData = [];
    public function __construct(
        string|int $errorCode,
        string $message,
        array $extraData = [],
        ?\Throwable $previous = null
    ) {
        $this->errorCode = $errorCode;
        $this->extraData = $extraData;
        parent::__construct($message, (int)$errorCode, $previous);
    }
    public function getErrorCode(): string|int { return $this->errorCode; }
    public function getErrorMessage(): string { return $this->getMessage(); }
    public function getExtraData(): array { return $this->extraData; }
}

步骤 3:根据场景定义子类(分类落地)

<?php
namespace App\Exceptions\Order;
use App\Exceptions\BusinessException;
class InsufficientStockException extends BusinessException
{
    // 构造函数中直接写死错误码和默认提示
    public function __construct(int $remaningStock = 0)
    {
        parent::__construct(
            10001,   // 错误码
            '库存不足', 
            ['remaning_stock' => $remaningStock]
        );
    }
}
class OrderStateConflictException extends BusinessException
{
    public function __construct(string $currentState, string $action)
    {
        parent::__construct(
            10002,
            "订单当前状态 [{$currentState}] 不允许执行 [{$action}] 操作",
            ['current_state' => $currentState]
        );
    }
}

全局捕获与响应处理

在框架层面(如 Laravel 的 Handler.php 或原生 PHP 的 set_exception_handler),需要根据接口类型做分流:

<?php
// 全局异常处理器核心逻辑 (Laravel Example)
public function render($request, \Throwable $e)
{
    // 1. 业务异常 & 参数校验异常 -> 返回 200/422 状态码,但带业务错误码
    if ($e instanceof \App\Exceptions\BusinessExceptionInterface) {
        return response()->json([
            'code'    => $e->getErrorCode(),
            'message' => $e->getErrorMessage(),
            'data'    => $e->getExtraData(),
            'status'  => 'error',
        ], 200); // HTTP状态码建议用200,避免前端网关误判
    }
    // 2. 参数校验异常 -> 返回 422
    if ($e instanceof \Illuminate\Validation\ValidationException) {
        return response()->json([
            'code'    => 422,
            'message' => '请求参数不合法',
            'errors'  => $e->errors(),
        ], 422);
    }
    // 3. 系统异常 -> 返回 500 (隐藏细节)
    if ($e instanceof \Throwable) {
        \Log::error($e->getMessage(), ['trace' => $e->getTraceAsString()]);
        return response()->json([
            'code'    => 500,
            'message' => '服务器开小差了,请稍后重试',
        ], 500);
    }
}

研发团队必须遵守的规范

没有规范,分类就形同虚设:

错误码分段制度

  • 1xxx:用户输入/权限错误(1000~1999)
  • 2xxx:订单/交易域(2000~2999)
  • 3xxx:库存/商品域(3000~3999)
  • 4xxx:支付/财务域(4000~4999)
  • 5xxx:外部系统依赖错误(5000~5999)

禁止跨层捕获

  • 避免在 service 层用 try...catch (\Exception $e) 去吞掉一切,只捕获你知道如何恢复的异常。

日志分级

  • BusinessException:记录 error_codemessage不记录堆栈(除非 Debug 模式)。
  • SystemException:必须记录完整堆栈,并关联 request_id 方便追踪。

异步队列不抛业务异常

  • 在队列中抛出 BusinessException 会导致死循环重试。队列中应该捕获业务异常并记录,然后丢弃该消息

判定表

条件 处理方式
用户能理解原因? 能 → 业务异常;不能 → 系统异常
用户能通过改输入解决? 能 → 参数异常;不能 → 业务状态冲突
重试会成功吗? 会 → 依赖服务异常(需重试);不会 → 业务逻辑异常(需人工干预)
是用户重复操作吗? 是 → 幂等异常(直接返回成功或提示)

构建这样的分类体系后,前端拿到 code=2001 就知道是订单状态冲突,拿到 code=3001 就知道是库存不足,拿 code=500 就知道是后端不可用,混乱度将大幅降低。

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