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 代码。