PHP 异常处理全解析:try-catch、自定义异常与全局异常处理器

1. 引言

在 PHP 开发中,异常处理是保证程序健壮性的重要环节。无论是初学者还是资深开发者,掌握异常处理机制都能让代码在面对错误时更加从容。本文将系统讲解 PHP 异常处理的核心知识,包括 try-catch 的使用、Exception 与 Error 的区别、finally 的应用场景、自定义异常层次设计、异常日志记录以及全局异常处理器,帮助你构建一套完整的异常处理体系。

2. 异常处理基础:try-catch

2.1 基本语法

try-catch 是 PHP 异常处理的核心语法结构。其基本形式如下:

php 复制代码
try {
    // 可能抛出异常的代码
    $result = divide(10, 0);
    echo $result;
} catch (Exception $e) {
    // 捕获异常并处理
    echo "捕获到异常:" . $e->getMessage();
}

当 try 块中的代码抛出异常时,程序会立即跳转到对应的 catch 块执行。如果没有异常抛出,catch 块不会执行。

2.2 多个 catch 块

PHP 允许使用多个 catch 块来捕获不同类型的异常:

php 复制代码
try {
    // 业务代码
} catch (InvalidArgumentException $e) {
    // 处理参数异常
    echo "参数错误:" . $e->getMessage();
} catch (RuntimeException $e) {
    // 处理运行时异常
    echo "运行时错误:" . $e->getMessage();
} catch (Exception $e) {
    // 兜底捕获所有异常
    echo "其他异常:" . $e->getMessage();
}

多个 catch 块按照从上到下的顺序匹配,因此更具体的异常类型应放在前面。

2.3 不捕获异常的情况

如果异常没有被捕获,PHP 会将其向上传递,直到找到匹配的 catch 块。如果最终没有捕获,脚本会终止并显示致命错误。

php 复制代码
function test() {
    throw new Exception("未捕获的异常");
}

test(); // 脚本终止,显示未捕获异常

3. Exception 与 Error 的区别

3.1 历史背景

在 PHP 7 之前,错误(Error)和异常(Exception)是两种不同的机制。错误通过 trigger_error() 触发,由错误处理器处理;异常通过 throw 抛出,由 try-catch 捕获。PHP 7 引入了 Throwable 接口,统一了错误和异常的处理方式。

3.2 Throwable 接口

Throwable 是所有可抛出对象的基接口,Exception 和 Error 都实现了该接口:

php 复制代码
interface Throwable
{
    public function getMessage();
    public function getCode();
    public function getFile();
    public function getLine();
    public function getTrace();
    public function getTraceAsString();
}

3.3 Exception 与 Error 的核心区别

特性 Exception Error
触发方式 通过 throw 主动抛出 由 PHP 引擎自动触发
常见场景 业务逻辑错误、参数校验失败 类型错误、内存不足、调用不存在的方法
是否可恢复 通常可捕获并恢复 多数为致命错误,难以恢复
捕获方式 try-catch 捕获 可通过 catch (Error) 捕获
php 复制代码
// Exception 示例:业务逻辑主动抛出
try {
    $age = -1;
    if ($age < 0) {
        throw new InvalidArgumentException("年龄不能为负数");
    }
} catch (Exception $e) {
    echo "业务异常:" . $e->getMessage();
}

// Error 示例:PHP 引擎自动触发
try {
    $obj = null;
    $obj->method(); // 触发 Error: Call to a member function on null
} catch (Error $e) {
    echo "引擎错误:" . $e->getMessage();
}

3.4 统一捕获 Throwable

在实际开发中,可以使用 Throwable 统一捕获异常和错误:

php 复制代码
try {
    // 可能抛出异常或错误的代码
} catch (Throwable $t) {
    echo "捕获到:" . get_class($t) . " - " . $t->getMessage();
}

4. finally 的使用场景

4.1 基本语法

finally 块无论是否发生异常都会执行,通常用于资源清理:

php 复制代码
try {
    $file = fopen("data.txt", "r");
    // 读取文件内容
} catch (Exception $e) {
    echo "读取失败:" . $e->getMessage();
} finally {
    // 无论成功还是失败,都关闭文件
    if ($file) {
        fclose($file);
    }
}

4.2 典型使用场景

场景一:数据库连接释放

php 复制代码
try {
    $pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
    $pdo->query("SELECT * FROM users");
} catch (PDOException $e) {
    echo "数据库错误:" . $e->getMessage();
} finally {
    $pdo = null; // 释放连接
}

场景二:锁释放

php 复制代码
try {
    $lock = acquireLock();
    // 业务处理
} finally {
    releaseLock($lock); // 确保锁被释放
}

4.3 finally 与 return 的交互

finally 块中的 return 会覆盖 try 或 catch 中的 return:

php 复制代码
function test() {
    try {
        return "try 返回值";
    } finally {
        return "finally 返回值"; // 最终返回这个值
    }
}

echo test(); // 输出:finally 返回值

5. 自定义异常层次设计

5.1 基础自定义异常

自定义异常类需要继承 Exception 或其子类:

php 复制代码
class UserException extends Exception
{
    // 可以添加自定义属性或方法
    protected $userMessage;

    public function __construct($message, $code = 0, $userMessage = "")
    {
        parent::__construct($message, $code);
        $this->userMessage = $userMessage;
    }

    public function getUserMessage()
    {
        return $this->userMessage;
    }
}

5.2 分层异常体系

设计合理的异常层次结构,便于分类处理:

php 复制代码
// 基础异常
class AppException extends Exception {}

// 业务异常层
class BusinessException extends AppException {}
class ValidationException extends BusinessException {}
class AuthorizationException extends BusinessException {}

// 基础设施异常层
class InfrastructureException extends AppException {}
class DatabaseException extends InfrastructureException {}
class NetworkException extends InfrastructureException {}

5.3 使用分层异常

php 复制代码
try {
    // 业务逻辑
    if (!$user->isValid()) {
        throw new ValidationException("用户数据校验失败");
    }
    if (!$user->hasPermission('admin')) {
        throw new AuthorizationException("没有管理员权限");
    }
} catch (ValidationException $e) {
    // 处理校验异常
    echo "校验失败:" . $e->getMessage();
} catch (AuthorizationException $e) {
    // 处理权限异常
    echo "权限不足:" . $e->getMessage();
} catch (BusinessException $e) {
    // 处理其他业务异常
    echo "业务错误:" . $e->getMessage();
} catch (AppException $e) {
    // 处理基础设施异常
    echo "系统错误:" . $e->getMessage();
}

6. 异常日志记录与上下文

6.1 记录异常信息

在生产环境中,异常日志是排查问题的关键。使用 error_log 或日志库记录异常:

php 复制代码
try {
    // 业务代码
} catch (Exception $e) {
    $logMessage = sprintf(
        "[%s] 异常信息: %s\n文件: %s\n行号: %d\n堆栈: %s",
        date('Y-m-d H:i:s'),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    );
    error_log($logMessage, 3, '/var/log/php_errors.log');
}

6.2 添加上下文信息

记录异常时,应包含请求上下文,便于定位问题:

php 复制代码
class ExceptionLogger
{
    public static function log(Exception $e, array $context = [])
    {
        $logData = [
            'timestamp' => date('Y-m-d H:i:s'),
            'message' => $e->getMessage(),
            'code' => $e->getCode(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
            'trace' => $e->getTraceAsString(),
            'context' => $context,
            'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
            'request_method' => $_SERVER['REQUEST_METHOD'] ?? '',
            'user_id' => $_SESSION['user_id'] ?? null,
        ];

        $logJson = json_encode($logData, JSON_UNESCAPED_UNICODE);
        error_log($logJson . "\n", 3, '/var/log/app_exceptions.log');
    }
}

// 使用示例
try {
    // 业务代码
} catch (Exception $e) {
    ExceptionLogger::log($e, [
        'action' => 'user_register',
        'params' => ['email' => $email],
    ]);
}

6.3 使用 Monolog 记录日志

在大型项目中,推荐使用 Monolog 等日志库:

php 复制代码
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger('app');
$logger->pushHandler(new StreamHandler('/var/log/app.log', Logger::ERROR));

try {
    // 业务代码
} catch (Exception $e) {
    $logger->error('业务异常', [
        'exception' => $e,
        'context' => ['user_id' => $userId],
    ]);
}

7. 全局异常处理器

7.1 set_exception_handler 函数

PHP 提供了 set_exception_handler() 函数来设置全局异常处理器,捕获所有未被 try-catch 捕获的异常:

php 复制代码
function globalExceptionHandler($exception)
{
    $message = sprintf(
        "未捕获异常: %s\n文件: %s\n行号: %d\n堆栈:\n%s",
        $exception->getMessage(),
        $exception->getFile(),
        $exception->getLine(),
        $exception->getTraceAsString()
    );

    error_log($message, 3, '/var/log/global_exceptions.log');

    // 返回友好的错误页面
    http_response_code(500);
    echo "系统繁忙,请稍后重试。";
}

set_exception_handler('globalExceptionHandler');

// 测试
throw new Exception("这是一个未捕获的异常");

7.2 区分异常类型处理

全局处理器可以根据异常类型返回不同的响应:

php 复制代码
function globalExceptionHandler($exception)
{
    // 记录日志
    error_log($exception->getMessage(), 3, '/var/log/global_exceptions.log');

    if ($exception instanceof ValidationException) {
        http_response_code(400);
        echo json_encode(['error' => '参数错误', 'message' => $exception->getMessage()]);
    } elseif ($exception instanceof AuthorizationException) {
        http_response_code(403);
        echo json_encode(['error' => '权限不足']);
    } else {
        http_response_code(500);
        echo json_encode(['error' => '服务器内部错误']);
    }
}

set_exception_handler('globalExceptionHandler');

7.3 结合 set_error_handler

对于 PHP 7 之前的错误,可以结合 set_error_handler() 统一处理:

php 复制代码
// 将错误转换为异常
function errorToException($severity, $message, $file, $line)
{
    throw new ErrorException($message, 0, $severity, $file, $line);
}

set_error_handler('errorToException');

// 现在所有错误都会变成 ErrorException,可以被 try-catch 捕获
try {
    $undefinedVar = $notDefined; // 触发 Notice
} catch (ErrorException $e) {
    echo "捕获到错误:" . $e->getMessage();
}

7.4 框架中的全局异常处理

在 Laravel 等框架中,全局异常处理通常通过异常处理类实现:

php 复制代码
// Laravel 中的 App\Exceptions\Handler
class Handler extends ExceptionHandler
{
    public function register()
    {
        $this->reportable(function (Throwable $e) {
            // 记录异常日志
        });

        $this->renderable(function (ValidationException $e, $request) {
            if ($request->expectsJson()) {
                return response()->json(['error' => $e->getMessage()], 422);
            }
        });
    }
}

8. 最佳实践总结

8.1 异常处理原则

  • 只在需要处理的地方捕获异常,不要过度捕获。
  • 异常信息要具体,便于定位问题。
  • 记录完整的堆栈信息,方便排查。
  • 向用户展示友好信息,不要暴露内部细节。
  • 合理设计异常层次,便于分类处理。

8.2 常见误区

  • 捕获所有异常却不处理,导致问题被隐藏。
  • 在循环中捕获异常,影响性能。
  • 使用异常控制正常流程,降低代码可读性。
  • 忽略 finally 块,导致资源泄漏。

8.3 完整示例

php 复制代码
<?php

// 自定义异常层次
class AppException extends Exception {}
class ValidationException extends AppException {}
class DatabaseException extends AppException {}

// 全局异常处理器
set_exception_handler(function ($exception) {
    error_log($exception->getTraceAsString(), 3, '/var/log/app.log');
    http_response_code(500);
    echo "系统繁忙,请稍后重试";
});

// 业务代码
function getUser($id)
{
    if (!is_numeric($id)) {
        throw new ValidationException("用户 ID 必须是数字");
    }

    $pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
    $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$id]);
    $user = $stmt->fetch();

    if (!$user) {
        throw new AppException("用户不存在");
    }

    return $user;
}

try {
    $user = getUser($_GET['id'] ?? 0);
    echo "用户名:" . $user['name'];
} catch (ValidationException $e) {
    echo "参数错误:" . $e->getMessage();
} catch (DatabaseException $e) {
    echo "数据库错误,请稍后重试";
} catch (AppException $e) {
    echo "业务错误:" . $e->getMessage();
} finally {
    // 清理资源
    $pdo = null;
}

9. 总结

PHP 异常处理是构建健壮应用的基础能力。通过合理使用 try-catch、理解 Exception 与 Error 的区别、善用 finally 进行资源清理、设计清晰的异常层次、记录完整的日志上下文,并配置全局异常处理器,你可以构建一套完善的异常处理体系。希望本文能帮助你在实际项目中更好地处理异常,写出更可靠的 PHP 代码。

相关推荐
外收内放1 小时前
Python基础语法练习题(65)
开发语言·python
浅尝辄止;1 小时前
设计模式在HIS/LIS医疗系统中的完整实践指南(C#版)
开发语言·设计模式·c#
Wang's Blog2 小时前
Java 项目部署之 Docker工具快速入门: 容器命令练习:创建、改文件、重启到删除的完整闭环
java·开发语言·docker
JaguarJack2 小时前
免费可商用 PHP 管理后台 CatchAdmin V5.4.1 发布,新增 MCP 支持
后端·php·laravel·服务端
quantdash_cc2 小时前
实时行情出现短暂异常时应该怎么处理?从数据校验到故障定位
开发语言·python·数据分析·量化交易·股票数据·quantdash
BingoGo2 小时前
免费可商用 PHP 管理后台 CatchAdmin V5.4.1 发布,新增 MCP 支持
后端·php
谢亮_vipxieliang2 小时前
多智能体系统的“TCP/IP时刻”:MCP与A2A协议如何定义Agent时代
网络·tcp/ip·ai·php
zjxtxdy2 小时前
R语言数据统计入门
开发语言·r语言
cleble2 小时前
怎么用用 PowerShell 下载文件
开发语言·php