自定义异常类
概述
PHP 允许开发者通过继承 Exception(或其子类)来创建自定义异常类。自定义异常可以携带特定的业务信息,使异常处理更加精确和语义化。
良好的自定义异常设计是应用程序错误处理策略的核心。通过定义有意义的异常类型,可以让调用方精确捕获特定异常,避免笼统的 try/catch。
版本说明
自定义异常类在所有 PHP 版本中可用。PHP 8.0+ 建议为自定义异常添加类型声明、使用 readonly 属性(PHP 8.1+)、以及构造器属性提升。
基础概念
为什么需要自定义异常
php
<?php
declare(strict_types=1);
// 使用通用异常的问题:
try {
// 不同业务场景都抛出 RuntimeException
throw new RuntimeException('用户不存在');
throw new RuntimeException('余额不足');
} catch (RuntimeException $e) {
// 无法区分是哪种业务异常
}
// 使用自定义异常的优势:
try {
throw new UserNotFoundException('u_123');
} catch (UserNotFoundException $e) {
// 精确处理用户不存在
} catch (InsufficientBalanceException $e) {
// 精确处理余额不足
}自定义异常的要素
- 类名:清晰表达异常含义(后缀为
Exception) - 继承关系:继承合适的基类(Exception、RuntimeException 等)
- 属性:携带业务上下文信息
- 构造函数:接收必要的上下文参数
- 异常码:用于 API 响应和日志分析
语法与代码
基本自定义异常
php
<?php
declare(strict_types=1);
class ValidationException extends RuntimeException
{
/** @var array<string, string> */
private array $errors;
/**
* @param array<string, string> $errors 字段 => 错误消息
*/
public function __construct(array $errors, ?Throwable $previous = null)
{
$this->errors = $errors;
parent::__construct('数据验证失败', 422, $previous);
}
/**
* @return array<string, string>
*/
public function errors(): array
{
return $this->errors;
}
public function firstError(): string
{
return reset($this->errors);
}
}
// 使用
try {
$errors = [
'email' => '邮箱格式不正确',
'age' => '年龄必须大于0',
];
throw new ValidationException($errors);
} catch (ValidationException $e) {
echo $e->getMessage(); // 数据验证失败
echo $e->getCode(); // 422
print_r($e->errors()); // ['email' => '...', 'age' => '...']
echo $e->firstError(); // 邮箱格式不正确
}带业务上下文的异常
php
<?php
declare(strict_types=1);
class EntityNotFoundException extends RuntimeException
{
private string $entityType;
private string $entityId;
public function __construct(
string $entityType,
string $entityId,
?Throwable $previous = null
) {
$this->entityType = $entityType;
$this->entityId = $entityId;
$message = "{$entityType}(ID: {$entityId}) 未找到";
parent::__construct($message, 404, $previous);
}
public function entityType(): string
{
return $this->entityType;
}
public function entityId(): string
{
return $this->entityId;
}
}
// 使用
try {
throw new EntityNotFoundException('Order', 'ORD-001');
} catch (EntityNotFoundException $e) {
echo $e->getMessage(); // Order(ID: ORD-001) 未找到
echo $e->entityType(); // Order
echo $e->entityId(); // ORD-001
echo $e->getCode(); // 404
}PHP 8.1+ 构造器属性提升
php
<?php
declare(strict_types=1);
// PHP 8.1+ 使用 readonly 和构造器属性提升
class ApiException extends RuntimeException
{
public function __construct(
private readonly string $publicMessage,
private readonly string $internalCode,
int $httpStatus = 500,
?Throwable $previous = null
) {
parent::__construct($this->publicMessage, $httpStatus, $previous);
}
public function publicMessage(): string
{
return $this->publicMessage;
}
public function internalCode(): string
{
return $this->internalCode;
}
}
// 使用
throw new ApiException(
publicMessage: '请求频率过高',
internalCode: 'RATE_LIMIT_EXCEEDED',
httpStatus: 429
);详细说明
异常消息格式
php
<?php
declare(strict_types=1);
// 异常消息应包含足够上下文
// 但不应包含敏感信息(密码、密钥等)
// 好的消息
throw new RuntimeException("文件 /tmp/data.csv 读取失败: Permission denied");
throw new EntityNotFoundException('User', '123');
throw new InvalidArgumentException("参数 \$amount 必须大于 0,收到 -1");
// 差的消息(缺乏上下文)
throw new RuntimeException('Error');
throw new RuntimeException('Failed');
throw new RuntimeException('操作失败');
// 好的消息 + 异常属性
class PaymentException extends RuntimeException
{
public function __construct(
private readonly string $orderId,
private readonly string $reason,
?Throwable $previous = null
) {
$message = "订单 {$orderId} 支付失败: {$reason}";
parent::__construct($message, 0, $previous);
}
public function orderId(): string { return $this->orderId; }
public function reason(): string { return $this->reason; }
}自定义异常码
php
<?php
declare(strict_types=1);
// 使用枚举定义异常码(PHP 8.1+)
enum AppErrorCode: int
{
case NotFound = 404;
case ValidationFailed = 422;
case Unauthorized = 401;
case Forbidden = 403;
case Conflict = 409;
case InternalError = 500;
}
class AppException extends RuntimeException
{
public function __construct(
private readonly AppErrorCode $errorCode,
string $detail = '',
?Throwable $previous = null
) {
$message = $detail !== '' ? $detail : $errorCode->name;
parent::__construct($message, $errorCode->value, $previous);
}
public function errorCode(): AppErrorCode
{
return $this->errorCode;
}
}
// 使用
throw new AppException(
AppErrorCode::NotFound,
"用户不存在"
);异常链(Previous Exception)
php
<?php
declare(strict_types=1);
class OrderService
{
public function createOrder(array $data): int
{
try {
$this->validateData($data);
return $this->insertOrder($data);
} catch (ValidationException $e) {
// 包装异常,保留原始异常
throw new OrderCreationException(
'订单创建失败: ' . $e->getMessage(),
previous: $e
);
}
}
private function validateData(array $data): void
{
// ...
}
private function insertOrder(array $data): int
{
return 1;
}
}
class OrderCreationException extends RuntimeException {}
class ValidationException extends RuntimeException {}实战示例
完整的业务异常体系
php
<?php
declare(strict_types=1);
// 基础异常
abstract class BaseException extends Exception
{
public function __construct(string $message, int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
// 用户相关异常
class UserException extends BaseException {}
class UserNotFoundException extends UserException
{
public function __construct(string $userId, ?Throwable $previous = null)
{
parent::__construct("用户 {$userId} 不存在", 404, $previous);
}
}
class UserAlreadyExistsException extends UserException
{
public function __construct(string $email, ?Throwable $previous = null)
{
parent::__construct("邮箱 {$email} 已被注册", 409, $previous);
}
}
class InvalidCredentialsException extends UserException
{
public function __construct(?Throwable $previous = null)
{
parent::__construct('用户名或密码错误', 401, $previous);
}
}
// 支付相关异常
class PaymentException extends BaseException {}
class PaymentFailedException extends PaymentException
{
public function __construct(
private readonly string $transactionId,
private readonly string $reason,
?Throwable $previous = null
) {
$message = "交易 {$transactionId} 失败: {$reason}";
parent::__construct($message, 400, $previous);
}
}
// 使用示例
class UserService
{
public function register(string $email, string $password): int
{
if ($this->emailExists($email)) {
throw new UserAlreadyExistsException($email);
}
return $this->createUser($email, $password);
}
public function login(string $email, string $password): array
{
$user = $this->findByEmail($email);
if ($user === null) {
throw new InvalidCredentialsException();
}
if (!$this->verifyPassword($password, $user['password'])) {
throw new InvalidCredentialsException();
}
return $user;
}
private function emailExists(string $email): bool { return false; }
private function createUser(string $email, string $password): int { return 1; }
private function findByEmail(string $email): ?array { return null; }
private function verifyPassword(string $raw, string $hash): bool { return true; }
}
// 调用方可以精确捕获
try {
$userService = new UserService();
$userService->register('existing@mail.com', 'password');
} catch (UserAlreadyExistsException $e) {
echo "邮箱已注册: " . $e->getMessage();
} catch (UserException $e) {
echo "用户相关错误: " . $e->getMessage();
} catch (Throwable $e) {
echo "系统错误: " . $e->getMessage();
}注意事项
不要创建过多的异常类
php
<?php
declare(strict_types=1);
// 反面示例:过度细分
class EmailTooShortException extends ValidationException {}
class EmailTooLongException extends ValidationException {}
class EmailInvalidFormatException extends ValidationException {}
class EmailDomainNotAllowedException extends ValidationException {}
// 正面示例:使用属性区分
class ValidationException extends RuntimeException
{
public function __construct(
private readonly string $field,
private readonly string $rule,
private readonly mixed $value,
?Throwable $previous = null
) {
$message = "字段 {$field} 验证失败: {$rule}";
parent::__construct($message, 422, $previous);
}
public function field(): string { return $this->field; }
public function rule(): string { return $this->rule; }
public function value(): mixed { return $this->value; }
}异常应该是不可变的
php
<?php
declare(strict_types=1);
// PHP 8.1+ 使用 readonly 确保异常不可变
class DomainException extends RuntimeException
{
public function __construct(
private readonly string $domain,
private readonly string $reason,
?Throwable $previous = null
) {
parent::__construct("{$domain} 错误: {$reason}", 0, $previous);
}
public function domain(): string { return $this->domain; }
public function reason(): string { return $this->reason; }
}最佳实践
- 异常名使用 PascalCase + Exception 后缀:如
UserNotFoundException - 继承合适的基类:
LogicException或RuntimeException - 包含业务上下文:通过属性携带业务相关信息
- 设置有意义的异常码:如 HTTP 状态码或自定义错误码
- 使用异常链:通过
previous参数保留原始异常
php
<?php
declare(strict_types=1);
// 推荐的自定义异常模板(PHP 8.1+)
class BusinessException extends RuntimeException
{
public function __construct(
string $message,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
class NotFoundException extends BusinessException
{
public function __construct(
string $entity,
string|int $id,
?Throwable $previous = null
) {
parent::__construct(
"{$entity}({$id}) 未找到",
404,
$previous
);
}
}