强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PHP 完全指南 / 第 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 捕获
ErrorPHP 引擎级错误,如类型错误
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