第 12 章 — 异常处理
第 12 章 — 异常处理:try/catch、自定义异常与 SPL 异常
12.1 异常基础
<?php
declare(strict_types=1);
// 抛出异常
function divide(float $a, float $b): float
{
if ($b === 0.0) {
throw new InvalidArgumentException('Division by zero');
}
return $a / $b;
}
// 捕获异常
try {
$result = divide(10, 0);
echo "Result: $result";
} catch (InvalidArgumentException $e) {
echo "参数错误: " . $e->getMessage();
} finally {
echo "\n计算完成"; // 无论是否异常都会执行
}
12.2 异常层次结构
Throwable
├── Exception
│ ├── LogicException
│ │ ├── BadFunctionCallException
│ │ ├── DomainException
│ │ ├── InvalidArgumentException
│ │ ├── LengthException
│ │ ├──OutOfRangeException
│ │ └── ...
│ ├── RuntimeException
│ │ ├── OutOfBoundsException
│ │ ├── OverflowException
│ │ ├── RangeException
│ │ ├── UnderflowException
│ │ ├── UnexpectedValueException
│ │ └── PDOException
│ └── 自定义异常
├── Error
│ ├── TypeError
│ ├── ArithmeticError
│ │ └── DivisionByZeroError
│ ├── ArgumentCountError
│ ├── AssertionError
│ ├── CompileError
│ │ └── ParseError
│ └── ValueError
└── FiberError
| 类型 | 用途 |
|---|
Exception | 应用级异常,应该被 try/catch 捕获 |
Error | PHP 引擎级错误,如类型错误 |
Throwable | 两者的共同接口 |
12.3 多重 catch 与 finally
<?php
function processOrder(array $data): void
{
try {
if (!isset($data['id'])) {
throw new InvalidArgumentException('Missing order ID');
}
$order = loadOrder($data['id']);
if ($order === null) {
throw new OutOfBoundsException("Order #{$data['id']} not found");
}
if ($order['status'] === 'cancelled') {
throw new DomainException('Cannot process cancelled order');
}
processPayment($order);
} catch (InvalidArgumentException $e) {
// 参数错误 — 记录日志,返回 400
logError('BAD_REQUEST', $e);
http_response_code(400);
} catch (OutOfBoundsException $e) {
// 未找到 — 返回 404
logError('NOT_FOUND', $e);
http_response_code(404);
} catch (DomainException $e) {
// 业务规则违反 — 返回 422
logError('BUSINESS_ERROR', $e);
http_response_code(422);
} catch (Throwable $e) {
// 捕获所有异常和错误
logError('INTERNAL_ERROR', $e);
http_response_code(500);
} finally {
// 无论如何都执行 — 清理资源
releaseLock();
}
}
PHP 8.0+ 管道运算符 catch
<?php
try {
riskyOperation();
} catch (InvalidArgumentException | OutOfBoundsException | DomainException $e) {
// 捕获多种异常类型
echo "业务错误: {$e->getMessage()}";
}
12.4 自定义异常
12.4.1 基础自定义异常
<?php
declare(strict_types=1);
namespace App\Exceptions;
class AppException extends \RuntimeException
{
private string $errorCode;
private array $context;
public function __construct(
string $errorCode,
string $message = '',
int $code = 0,
?\Throwable $previous = null,
array $context = [],
) {
parent::__construct($message, $code, $previous);
$this->errorCode = $errorCode;
$this->context = $context;
}
public function getErrorCode(): string
{
return $this->errorCode;
}
public function getContext(): array
{
return $this->context;
}
public function toArray(): array
{
return [
'error_code' => $this->errorCode,
'message' => $this->getMessage(),
'context' => $this->context,
];
}
}
// 具体异常类
class ValidationException extends AppException
{
private array $errors;
public function __construct(array $errors, string $message = 'Validation failed')
{
parent::__construct('VALIDATION_ERROR', $message, 422);
$this->errors = $errors;
}
public function getErrors(): array
{
return $this->errors;
}
}
class NotFoundException extends AppException
{
public function __construct(string $resource, mixed $id)
{
parent::__construct(
'NOT_FOUND',
"{$resource} with ID '{$id}' not found",
404,
context: ['resource' => $resource, 'id' => $id],
);
}
}
class UnauthorizedException extends AppException
{
public function __construct(string $message = 'Unauthorized')
{
parent::__construct('UNAUTHORIZED', $message, 401);
}
}
class ForbiddenException extends AppException
{
public function __construct(string $message = 'Forbidden')
{
parent::__construct('FORBIDDEN', $message, 403);
}
}
12.4.2 异常接口
<?php
// 定义异常标记接口
interface HttpException
{
public function getStatusCode(): int;
public function getHeaders(): array;
}
// 让自定义异常实现接口
class ServiceUnavailableException extends \RuntimeException implements HttpException
{
public function __construct(
private readonly int $retryAfter = 60,
) {
parent::__construct('Service temporarily unavailable', 503);
}
public function getStatusCode(): int
{
return 503;
}
public function getHeaders(): array
{
return ['Retry-After' => (string)$this->retryAfter];
}
}
12.5 SPL 异常
| 异常类 | 使用场景 |
|---|
InvalidArgumentException | 参数类型或值错误 |
LogicException | 程序逻辑错误 |
RuntimeException | 运行时才可检测的错误 |
OutOfBoundsException | 无效索引 |
OverflowException | 容器已满 |
UnderflowException | 空容器取值 |
LengthException | 长度无效 |
BadMethodCallException | 调用不存在的方法 |
UnexpectedValueException | 值不符合预期 |
<?php
// 使用 SPL 异常
class Collection
{
private array $items = [];
private int $maxSize;
public function __construct(int $maxSize = 100)
{
$this->maxSize = $maxSize;
}
public function add(mixed $item): void
{
if (count($this->items) >= $this->maxSize) {
throw new OverflowException('Collection is full');
}
$this->items[] = $item;
}
public function remove(): mixed
{
if (empty($this->items)) {
throw new UnderflowException('Collection is empty');
}
return array_pop($this->items);
}
public function get(int $index): mixed
{
if (!isset($this->items[$index])) {
throw new OutOfBoundsException("Index {$index} does not exist");
}
return $this->items[$index];
}
}
12.6 全局异常处理器
<?php
declare(strict_types=1);
// 设置全局异常处理器
set_exception_handler(function (\Throwable $e): void {
$log = [
'time' => date('Y-m-d H:i:s'),
'type' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
];
// 记录到日志文件
error_log(json_encode($log, JSON_UNESCAPED_UNICODE) . "\n", 3, '/var/log/php/error.log');
// 生产环境隐藏详细信息
if (getenv('APP_ENV') === 'production') {
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error']);
} else {
http_response_code(500);
echo json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
});
// 设置错误转异常处理器
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): never {
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});
12.7 错误处理 vs 异常处理
| 特性 | 错误处理 | 异常处理 |
|---|
| 触发方式 | trigger_error() | throw new |
| 捕获方式 | set_error_handler() | try/catch |
| 可恢复 | 部分情况 | 通常可恢复 |
| 传播 | 不传播 | 沿调用栈传播 |
| 推荐场景 | 旧代码兼容 | 现代 PHP 开发 |
PHP 8.0+ 的改进
<?php
// 以前:很多函数返回 false
$result = fopen('/nonexistent', 'r'); // 返回 false
// 现在:TypeError、ValueError 可以被捕获
try {
$val = new \ValueError('Invalid value');
} catch (ValueError $e) {
echo $e->getMessage();
}
// match 表达式也可以抛异常
$status = match ($input) {
'valid' => true,
default => throw new ValueError("Invalid: $input"),
};
12.8 业务场景:API 异常处理中间件
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Exceptions\AppException;
use App\Exceptions\ValidationException;
use App\Exceptions\HttpException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Slim\Psr7\Response;
class ExceptionHandlerMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler,
): ResponseInterface {
try {
return $handler->handle($request);
} catch (ValidationException $e) {
return $this->jsonResponse(422, [
'error' => $e->getErrorCode(),
'message' => $e->getMessage(),
'errors' => $e->getErrors(),
]);
} catch (HttpException $e) {
return $this->jsonResponse($e->getStatusCode(), [
'error' => 'HTTP_ERROR',
'message' => $e->getMessage(),
], $e->getHeaders());
} catch (AppException $e) {
return $this->jsonResponse(400, $e->toArray());
} catch (\Throwable $e) {
// 记录完整错误
error_log(sprintf(
"[%s] %s: %s in %s:%d\n%s",
date('Y-m-d H:i:s'),
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString(),
));
$response = ['error' => 'INTERNAL_ERROR', 'message' => 'An unexpected error occurred'];
if (getenv('APP_DEBUG') === 'true') {
$response['debug'] = [
'type' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
];
}
return $this->jsonResponse(500, $response);
}
}
private function jsonResponse(int $status, array $data, array $headers = []): ResponseInterface
{
$response = new Response($status);
$response->getBody()->write(json_encode($data, JSON_UNESCAPED_UNICODE));
$response = $response->withHeader('Content-Type', 'application/json');
foreach ($headers as $name => $value) {
$response = $response->withHeader($name, $value);
}
return $response;
}
}
12.9 扩展阅读
上一章:第 11 章 — OOP 进阶
下一章:第 13 章 — Attributes