很多人写 PHP 异常处理就是 try-catch 一把梭,catch 里面 echo 个错误信息就完事了。
今天聊聊如何设计一个合理的异常体系。
PHP 内置异常层级
Throwable (interface)├── Error│ ├── ArithmeticError│ ├── AssertionError│ ├── TypeError│ └── ValueError└── Exception ├── LogicException │ ├── InvalidArgumentException │ ├── OutOfRangeException │ └── ... └── RuntimeException ├── OutOfBoundsException ├── UnexpectedValueException └── ...
设计自定义异常体系
基础异常类
namespaceApp\Exceptions;useException;useThrowable;abstractclassBaseExceptionextendsException{protectedarray $context = [];protected string $errorCode = '';publicfunction__construct( string $message = '', string $errorCode = '', array $context = [], int $code = 0, ?Throwable $previous = null ){parent::__construct($message, $code, $previous);$this->errorCode = $errorCode;$this->context = $context; }publicfunctiongetErrorCode(): string{return$this->errorCode; }publicfunctiongetContext(): array{return$this->context; }publicfunctiontoArray(): array{return ['error_code' => $this->errorCode,'message' => $this->getMessage(),'context' => $this->context, ]; }}
业务异常
// 业务逻辑异常classBusinessExceptionextendsBaseException{publicfunction__construct( string $message, string $errorCode = 'BUSINESS_ERROR', array $context = [] ){parent::__construct($message, $errorCode, $context, 400); }}// 资源不存在classNotFoundExceptionextendsBaseException{publicfunction__construct(string $resource, mixed $id = null){ $message = $id ? "{$resource} #{$id} 不存在" : "{$resource} 不存在";parent::__construct($message, 'NOT_FOUND', ['resource' => $resource, 'id' => $id], 404); }}// 验证异常classValidationExceptionextendsBaseException{protectedarray $errors = [];publicfunction__construct(array $errors, string $message = '数据验证失败'){$this->errors = $errors;parent::__construct($message, 'VALIDATION_ERROR', ['errors' => $errors], 422); }publicfunctiongetErrors(): array{return$this->errors; }}// 认证异常classAuthenticationExceptionextendsBaseException{publicfunction__construct(string $message = '请先登录'){parent::__construct($message, 'UNAUTHENTICATED', [], 401); }}// 授权异常classAuthorizationExceptionextendsBaseException{publicfunction__construct(string $message = '没有权限执行此操作'){parent::__construct($message, 'UNAUTHORIZED', [], 403); }}
使用异常
classUserService{publicfunctionfind(int $id): User{ $user = $this->repository->find($id);if ($user === null) {thrownew NotFoundException('用户', $id); }return $user; }publicfunctioncreate(array $data): User{ $errors = $this->validate($data);if (!empty($errors)) {thrownew ValidationException($errors); }if ($this->repository->existsByEmail($data['email'])) {thrownew BusinessException('邮箱已被注册','EMAIL_EXISTS', ['email' => $data['email']] ); }return$this->repository->create($data); }publicfunctiondelete(int $id, User $operator): void{ $user = $this->find($id);if (!$operator->canDelete($user)) {thrownew AuthorizationException('您没有权限删除此用户'); }$this->repository->delete($id); }}
全局异常处理器
classExceptionHandler{publicfunctionhandle(Throwable $e): void{$this->log($e);if ($e instanceof ValidationException) {$this->respondValidationError($e); } elseif ($e instanceof NotFoundException) {$this->respondNotFound($e); } elseif ($e instanceof AuthenticationException) {$this->respondUnauthenticated($e); } elseif ($e instanceof BusinessException) {$this->respondBusinessError($e); } else {$this->respondInternalError($e); } }privatefunctionrespondValidationError(ValidationException $e): void{$this->jsonResponse(['code' => $e->getErrorCode(),'message' => $e->getMessage(),'errors' => $e->getErrors(), ], 422); }privatefunctionrespondInternalError(Throwable $e): void{ $message = getenv('APP_DEBUG') ? $e->getMessage() : '服务器内部错误';$this->jsonResponse(['code' => 'INTERNAL_ERROR','message' => $message, ], 500); }privatefunctionjsonResponse(array $data, int $statusCode): void{ http_response_code($statusCode); header('Content-Type: application/json; charset=utf-8');echo json_encode($data, JSON_UNESCAPED_UNICODE);exit; }}// 注册全局异常处理set_exception_handler([new ExceptionHandler(), 'handle']);
异常处理最佳实践
❌ 不要吞掉异常
// 错误:吞掉异常try {$this->doSomething();} catch (Exception $e) {// 什么都不做}// 正确:至少记录日志try {$this->doSomething();} catch (Exception $e) {$this->logger->error($e->getMessage());throw $e;}
❌ 不要用异常控制流程
// 错误:用异常控制流程try { $user = $this->repository->find($id);} catch (NotFoundException $e) { $user = $this->createDefaultUser();}// 正确:用返回值$user = $this->repository->find($id);if ($user === null) { $user = $this->createDefaultUser();}
✅ 捕获具体异常
// 错误:捕获所有异常try {$this->process();} catch (Exception $e) {// 处理}// 正确:捕获具体异常try {$this->process();} catch (ValidationException $e) {// 处理验证错误} catch (NotFoundException $e) {// 处理资源不存在}
✅ 使用 finally 清理资源
$file = fopen('data.txt', 'r');try {$this->processFile($file);} catch (Exception $e) {$this->logger->error($e->getMessage());throw $e;} finally { fclose($file); // 无论是否异常都会执行}
总结
异常体系设计要点:
下一篇我们聊聊生产环境的错误日志最佳实践。