Exception 与 ErrorException
概述
Exception 是 PHP 异常体系中最基础的异常类,所有用户自定义异常都应继承自它。ErrorException 则是一个特殊异常类,用于将传统的 PHP 错误转换为异常对象,使错误能通过 try/catch 机制统一处理。
PHP 版本说明
Exception自 PHP 5.1 起可用,PHP 7.0 后成为Throwable接口的实现ErrorException自 PHP 5.1 起可用,继承自Exception
基础概念
Exception 类的定位
在 PHP 7.0+ 的异常体系中,Throwable 是顶层接口,它有两个直接实现类:Error 和 Exception。
Throwable (接口)
├── Error ← PHP 内部错误(不可被用户类直接捕获继承 Throwable)
└── Exception ← 用户级异常(可被用户类继承)
└── ErrorException ← 将 PHP 错误转为异常Exception 是用户异常的根基类。当你的业务逻辑出现可预期的错误时,应该抛出 Exception 或其子类。
ErrorException 的作用
PHP 有两套错误处理机制:
- 错误(Error):通过
error_reporting()和set_error_handler()处理 - 异常(Exception):通过
try/catch处理
ErrorException 是连接两套机制的桥梁——通过 set_error_handler() 将错误包装为 ErrorException,从而统一使用 try/catch 处理。
语法与代码
Exception 类属性与方法
<?php
declare(strict_types=1);
try {
throw new \Exception(
message: '数据库连接失败',
code: 1001,
previous: null
);
} catch (\Exception $e) {
// 四个核心属性
echo $e->getMessage(); // "数据库连接失败"
echo $e->getCode(); // 1001
echo $e->getFile(); // 文件完整路径
echo $e->getLine(); // 抛出行号
// 堆栈跟踪
echo $e->getTraceAsString();
// 获取前一个异常(异常链)
echo $e->getPrevious(); // null
}异常链:previous 参数
<?php
declare(strict_types=1);
function readConfig(string $path): array
{
if (!file_exists($path)) {
throw new \RuntimeException("配置文件不存在: {$path}");
}
$content = file_get_contents($path);
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException(
message: '配置文件 JSON 格式错误',
code: 0,
previous: new \Exception(json_last_error_msg())
);
}
return $data;
}
try {
$config = readConfig('/app/config.json');
} catch (\RuntimeException $e) {
// 捕获外层异常
echo '主异常: ' . $e->getMessage() . PHP_EOL;
// 追溯到内部异常
$previous = $e->getPrevious();
if ($previous !== null) {
echo '原因: ' . $previous->getMessage() . PHP_EOL;
}
}ErrorException 转换错误为异常
<?php
declare(strict_types=1);
// 自定义错误转换处理器
set_error_handler(
callback: function (int $severity, string $message, string $file, int $line): bool {
// 只转换用户关心的错误级别
$map = [
E_WARNING => 'WARNING',
E_NOTICE => 'NOTICE',
E_USER_ERROR => 'USER_ERROR',
E_USER_WARNING => 'USER_WARNING',
E_USER_NOTICE => 'USER_NOTICE',
E_STRICT => 'STRICT',
E_DEPRECATED => 'DEPRECATED',
];
if (!isset($map[$severity])) {
return false; // 不处理的级别交给内置处理器
}
throw new \ErrorException(
message: $message,
code: 0,
severity: $severity,
filename: $file,
line: $line
);
}
);
// 现在警告也能被 try/catch 捕获了
try {
$array = [];
echo $array['nonexistent']; // 触发 E_WARNING
} catch (\ErrorException $e) {
echo "捕获到错误异常: {$e->getMessage()}";
echo "严重级别: {$e->getSeverity()}";
}ErrorException 的 severity 参数
<?php
declare(strict_types=1);
// ErrorException 继承自 Exception,额外增加 severity 属性
$errorException = new \ErrorException(
message: 'Undefined array key "name"',
code: 0,
severity: E_WARNING,
filename: __FILE__,
line: __LINE__
);
// 父类 Exception 的方法全部可用
echo $errorException->getMessage(); // 异常消息
echo $errorException->getCode(); // 异常码
echo $errorException->getFile(); // 文件名
echo $errorException->getLine(); // 行号
echo $errorException->getTraceAsString();
// ErrorException 特有方法
echo $errorException->getSeverity(); // E_WARNING = 2详细说明
Exception 核心属性一览
| 属性 | 类型 | 说明 | 默认值 |
|---|---|---|---|
$message | string | 异常描述信息 | "" |
$code | int | 异常代码,用于分类 | 0 |
$file | string | 抛出异常的文件路径 | 自动填充 |
$line | int | 抛出异常的行号 | 自动填充 |
$previous | ?Throwable | 前一个异常(异常链) | null |
Exception 核心方法一览
| 方法 | 返回类型 | 说明 |
|---|---|---|
getMessage() | string | 获取异常消息 |
getCode() | int | 获取异常码 |
getFile() | string | 获取抛出文件 |
getLine() | int | 获取抛出行号 |
getTrace() | array | 获取堆栈跟踪数组 |
getTraceAsString() | string | 获取格式化的堆栈跟踪字符串 |
getPrevious() | ?Throwable | 获取前一个异常 |
__toString() | string | 将异常格式化为字符串 |
ErrorException 构造函数签名
public __construct(
string $message = "",
int $code = 0,
int $severity = E_ERROR,
?string $filename = null,
?int $line = null,
?Throwable $previous = null
)ErrorException 比 Exception 多了一个 severity 参数,用于保存原始 PHP 错误级别。
__toString() 输出格式
Exception 的 __toString() 方法返回格式化的异常信息:
Exception: 消息内容 in /path/to/file.php:42
Stack trace:
#0 /path/to/file.php(20): functionName()
#1 {main}
thrown in /path/to/file.php on line 42注意
__toString() 在 __toString() 魔术方法内部抛出时会导致致命错误。因此不要在 __toString() 中使用异常处理。
实战示例
统一错误与异常处理器
<?php
declare(strict_types=1);
class ErrorHandler
{
public static function register(): void
{
// 将错误转为 ErrorException
set_error_handler(function (int $severity, string $msg, string $file, int $line): bool {
if (!(error_reporting() & $severity)) {
return false;
}
throw new \ErrorException($msg, 0, $severity, $file, $line);
});
// 捕获所有未处理的异常
set_exception_handler(function (\Throwable $e): void {
error_log("[{$e->getCode()}] {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}");
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "Error: {$e->getMessage()}\n");
} else {
http_response_code(500);
echo json_encode(['error' => '服务器内部错误']);
}
});
// 捕获致命错误
register_shutdown_function(function (): void {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR], true)) {
error_log("[FATAL] {$error['message']} in {$error['file']}:{$error['line']}");
}
});
}
}
ErrorHandler::register();
// 现在所有错误和异常都能被统一处理注意事项
不要捕获 Exception 作为兜底:PHP 7.0+ 应捕获
Throwable而非Exception,因为Exception无法捕获Error类型的错误(如TypeError、ParseError)。错误处理器返回值:
set_error_handler回调返回true表示错误已处理,PHP 内部错误处理器不会继续执行;返回false则交由内置处理器处理。ErrorException 不会自动生效:必须手动通过
set_error_handler()配合使用。PHP 不会自动将错误转为ErrorException。severity 级别的过滤:在自定义错误处理器中,应使用
error_reporting() & $severity检查当前报告级别是否包含该错误。异常链不要无限嵌套:
previous参数用于包装原始异常,但不要在循环中不断包装形成循环引用。
最佳实践
推荐做法
- 异常消息使用英文:异常消息通常记录到日志,英文更利于日志分析和搜索
- 异常码使用常量定义:使用常量而非魔术数字,便于管理和引用
- 异常链保留原始异常:使用
previous参数包装底层异常,方便调试 - 生产环境隐藏堆栈信息:不要将
getTraceAsString()输出给终端用户 - ErrorException 只转换必要级别:不要转换
E_NOTICE等低级别错误为异常,避免过度捕获
<?php
declare(strict_types=1);
// 最佳实践:定义异常码常量
final class AppErrorCode
{
public const DB_CONNECTION_FAILED = 1001;
public const DB_QUERY_FAILED = 1002;
public const INVALID_INPUT = 2001;
public const UNAUTHORIZED = 3001;
public const NOT_FOUND = 3002;
}
// 最佳实践:使用 previous 保留原始异常
try {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'wrong');
} catch (PDOException $pdoException) {
throw new \RuntimeException(
message: '服务暂时不可用,请稍后重试',
code: AppErrorCode::DB_CONNECTION_FAILED,
previous: $pdoException
);
}进阶用法
调试与测试技巧
<?php
declare(strict_types=1);
// 单元测试辅助函数
function createTestResource(): mixed
{
return match (true) {
default => new stdClass(),
};
}
// 调试输出函数
function debugOutput(mixed , string = ''): void
{
= ? ": " : '';
.= print_r(, true);
fwrite(STDERR, . "\n");
}
// 性能基准测试
function benchmark(callable , int = 1000): float
{
= hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$fn();
}
return (hrtime(true) - $start) / 1e9;
}日志记录实践
<?php
declare(strict_types=1);
/**
* 简易日志记录器
*/
class SimpleLogger
{
private string $logFile;
private string $level = 'INFO';
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
private function log(string $level, string $message, array $context): void
{
$timestamp = date('Y-m-d H:i:s');
$contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}配置与环境检测
<?php
declare(strict_types=1);
// 环境检测工具
class EnvironmentChecker
{
public static function checkRequirements(array $requirements): array
{
$results = [];
foreach ($requirements as $name => $check) {
$results[$name] = is_callable($check) ? $check() : false;
}
return $results;
}
public static function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'sapi' => PHP_SAPI,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'loaded_extensions' => get_loaded_extensions(),
];
}
}常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 网络问题/配置错误 | 检查配置,增加超时时间 |
| 权限不足 | 文件/目录权限 | 使用 chmod/chown 修正 |
| 性能下降 | 索引缺失/数据量大 | 添加索引,优化查询 |
| 数据不一致 | 并发冲突/事务残留 | 使用锁机制和事务 |
| 内存溢出 | 大数据集/未释放资源 | 增大内存限制,分批处理 |
故障排除步骤
- 检查错误日志和异常信息
- 确认配置和环境是否正确
- 使用调试工具逐步排查
- 参考官方文档查找已知问题
版本兼容性说明
| 功能 | 最低版本 | 说明 |
|---|---|---|
| 基础功能 | PHP 8.1 | 本文档基准版本 |
| 只读属性 | PHP 8.1 | public readonly 修饰符 |
| 枚举类型 | PHP 8.1 | enum 类型和 match 表达式 |
| Fiber | PHP 8.1 | 协程/轻量级并发 |
| 命名参数 | PHP 8.0 | foo(arg_name: value) |
| 联合类型 | PHP 8.0 | `int |
| Null 安全运算符 | PHP 8.0 | $obj?->method() |
| 析构器 promotion | PHP 8.0 | __construct(public $x) |
<?php
declare(strict_types=1);
// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
if (version_compare(PHP_VERSION, $minVersion, '<')) {
throw new RuntimeException(
sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
);
}
}
ensureVersion('8.1.0');