错误基础概要
概述
PHP 的错误处理机制经历了重大演进。PHP 5.x 中错误和异常是两套完全独立的系统;PHP 7.0 引入 Throwable 接口统一了两者的处理方式;PHP 8.0+ 中错误默认都转换为异常抛出。
理解 PHP 错误机制的演进对于编写健壮的应用程序至关重要。现代 PHP 推荐使用异常(Exception)作为主要的错误处理手段,错误(Error)仅在不可恢复的程序级问题中使用。
版本说明
- PHP 5.x:错误和异常独立,错误不能被 try/catch 捕获
- PHP 7.0:引入 Throwable,Error 实现了 Throwable,可被 try/catch 捕获
- PHP 8.0+:大多数错误转为 Error 异常,默认错误报告更严格
基础概念
错误 vs 异常
php
<?php
declare(strict_types=1);
// 错误(Error)— PHP 引擎层面的故障
// 通常表示程序存在严重问题,不应被忽略
// 例如:语法错误、类型错误、内存不足
echo $undefinedVar; // PHP 8+: Warning: Undefined variable
// 异常(Exception)— 应用层面的可预期异常
// 表示程序遇到了可处理的异常情况
// 例如:参数无效、文件不存在、网络超时
throw new InvalidArgumentException('参数不能为空');| 特性 | 错误 (Error) | 异常 (Exception) |
|---|---|---|
| 来源 | PHP 引擎 / 内部函数 | 应用代码 / 库代码 |
| 可恢复性 | 通常不可恢复 | 通常可恢复 |
| 处理方式 | try/catch 或 set_error_handler | try/catch |
| 基类 | Error | Exception |
| 严重性 | 高(程序级问题) | 中(业务级问题) |
| PHP 7+ | 实现 Throwable,可被 try/catch | 实现 Throwable |
PHP 错误机制演进
php
<?php
declare(strict_types=1);
// PHP 5.x — 两套独立体系
// 错误:trigger_error(), E_WARNING, E_ERROR
// 异常:throw new Exception()
// 错误不能被 try/catch 捕获
// PHP 7.0 — 统一为 Throwable
// Error 和 Exception 都实现 Throwable
// 所有错误和异常都可以用 try/catch 捕获
try {
$result = 1 / 0; // DivisionByZeroError
} catch (Throwable $e) {
echo $e->getMessage(); // Division by zero
}
// PHP 8.0+ — 错误默认更严格
// 许多之前只是 Warning/Notice 的现在抛出 Error
// 例如:访问未定义属性、参数类型不匹配Throwable:统一的错误/异常接口
php
<?php
declare(strict_types=1);
// PHP 7+ 的错误/异常层次
// Throwable
// ├── Error
// │ ├── TypeError
// │ ├── ValueError
// │ ├── ArgumentCountError
// │ ├── ArithmeticError
// │ ├── CompileError
// │ └── ...
// └── Exception
// ├── RuntimeException
// ├── LogicException
// └── ...语法与代码
错误触发示例
php
<?php
declare(strict_types=1);
// 1. 类型错误 (TypeError)
function greet(string $name): string
{
return "Hello, {$name}";
}
try {
greet(123); // PHP 8+ strict mode: TypeError
} catch (TypeError $e) {
echo "TypeError: " . $e->getMessage();
}
// 2. 参数数量错误 (ArgumentCountError)
function add(int $a, int $b): int
{
return $a + $b;
}
try {
add(1); // ArgumentCountError
} catch (ArgumentCountError $e) {
echo "参数数量错误: " . $e->getMessage();
}
// 3. 值错误 (ValueError) — PHP 8.0+
try {
$num = intval('abc', 37); // 无效的进制
} catch (ValueError $e) {
echo "值错误: " . $e->getMessage();
}
// 4. 算术错误 (ArithmeticError)
try {
$result = intdiv(PHP_INT_MIN, -1); // 整数溢出
} catch (ArithmeticError $e) {
echo "算术错误: " . $e->getMessage();
}错误报告级别
php
<?php
declare(strict_types=1);
// 查看当前错误报告级别
echo error_reporting();
// -1 表示所有错误
// E_ALL 表示所有错误和警告
// 设置错误报告级别
error_reporting(E_ALL);
error_reporting(E_ALL & ~E_NOTICE); // 排除 Notice
// PHP 8.0+ 推荐设置
error_reporting(E_ALL | E_STRICT);详细说明
PHP 5 到 PHP 8 的错误处理变化
php
<?php
declare(strict_types=1);
// PHP 5.x:错误是传统机制
// 调用未定义函数 → Fatal Error(脚本终止)
// 访问未定义变量 → Notice(继续执行)
// 参数类型不匹配 → Catchable Fatal Error
// PHP 7.x:统一为 Throwable
// 调用未定义函数 → Error (PHP 7+)
// 参数类型不匹配 → TypeError
// 除以零 → DivisionByZeroError (PHP 7+)
// PHP 8.x:更严格的默认行为
// 访问未定义属性 → Warning (PHP 8.0)
// 访问未定义变量 → Warning (PHP 8.0, 之前是 Notice)
// 未定义常量 → Error (PHP 8.0, 之前是 Notice)
// 函数返回值类型不匹配 → TypeError
// PHP 8.1:
// 内部函数参数类型不匹配(之前是 Warning)→ TypeError
// 返回值类型不匹配(内部函数)→ TypeError
// PHP 8.2:
// 动态属性弃用(使用 #[AllowDynamicProperties] 保留)
// 更多严格类型检查错误转换为异常
php
<?php
declare(strict_types=1);
// 在 PHP 7.0+ 中,使用 ErrorException 将传统错误转为异常
// 这是兼容旧代码和统一异常处理的桥梁
class ErrorHandler
{
public function register(): void
{
// 将 PHP 错误转为 ErrorException
set_error_handler(
function (int $severity, string $message, string $file, int $line): bool {
// 只处理 error_reporting 包含的错误级别
if (!(error_reporting() & $severity)) {
return false;
}
throw new ErrorException($message, 0, $severity, $file, $line);
}
);
}
}
// 使用
$handler = new ErrorHandler();
$handler->register();
try {
// 触发 Warning
$file = fopen('/nonexistent/file.txt', 'r');
} catch (ErrorException $e) {
echo "捕获到错误(转为异常): " . $e->getMessage();
}错误抑制运算符 @
php
<?php
declare(strict_types=1);
// @ 运算符抑制错误输出
// 但不推荐使用,因为它:
// 1. 影响性能
// 2. 隐藏了可能有用的调试信息
// 3. 不能抑制所有错误(Fatal Error、ParseException)
$result = @file_get_contents('nonexistent.txt');
// 错误被抑制,$result 为 false
// 替代方案:使用错误控制或异常
$result = file_get_contents('nonexistent.txt');
if ($result === false) {
// 优雅地处理错误
echo "文件不存在";
}
// PHP 8.0+:@ 运算符不影响错误处理器
// 如果自定义了 set_error_handler,@ 不会阻止错误处理器执行
// 但 error_reporting() 在 @ 作用域内会返回 0不推荐使用 @ 运算符
PHP 官方不推荐使用 @ 运算符抑制错误。应使用 try/catch 或条件检查来处理预期的错误场景。
实战示例
生产环境错误处理框架
php
<?php
declare(strict_types=1);
class ProductionErrorHandler
{
private bool $debugMode;
public function __construct(bool $debugMode = false)
{
$this->debugMode = $debugMode;
}
public function register(): void
{
// 注册错误处理器
set_error_handler($this->handleError(...));
// 注册异常处理器
set_exception_handler($this->handleException(...));
// 注册致命错误处理器
register_shutdown_function($this->handleShutdown(...));
}
public function handleError(
int $severity,
string $message,
string $file,
int $line
): bool {
$errorTypes = [
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parse Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
E_STRICT => 'Strict',
E_RECOVERABLE_ERROR => 'Recoverable Error',
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'User Deprecated',
];
$type = $errorTypes[$severity] ?? "Unknown({$severity})";
$this->logError($type, $message, $file, $line);
// 返回 true 表示已处理,不调用 PHP 内置错误处理器
return true;
}
public function handleException(Throwable $e): void
{
$this->logError(
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine()
);
if (!$this->debugMode) {
echo '服务器发生内部错误';
} else {
echo "<pre>{$e}</pre>";
}
}
public function handleShutdown(): void
{
$error = error_get_last();
if ($error === null) {
return;
}
if (
$error['type'] === E_ERROR
|| $error['type'] === E_PARSE
|| $error['type'] === E_CORE_ERROR
|| $error['type'] === E_COMPILE_ERROR
) {
$this->logError(
'Fatal ' . $error['type'],
$error['message'],
$error['file'],
$error['line']
);
}
}
private function logError(
string $type,
string $message,
string $file,
int $line
): void {
$entry = sprintf(
"[%s] [%s] %s in %s:%d",
date('Y-m-d H:i:s'),
$type,
$message,
$file,
$line
);
error_log($entry);
}
}
// 使用
$handler = new ProductionErrorHandler(debugMode: false);
$handler->register();注意事项
错误处理的性能影响
php
<?php
declare(strict_types=1);
// 错误处理会带来性能开销
// 1. @ 运算符会临时将 error_reporting 设为 0,然后再恢复
// 2. set_error_handler 每次错误都会调用
// 3. try/catch 块本身开销极小(没有异常时)
// 错误处理性能建议:
// - 使用条件检查代替 @ 运算符
// - 在生产环境中减少 error_reporting 级别
// - 避免在热路径中频繁触发错误
// 好:条件检查
if (file_exists($path)) {
$content = file_get_contents($path);
}
// 差:使用 @ 运算符
$content = @file_get_contents($path);不能被 try/catch 捕获的错误
php
<?php
declare(strict_types=1);
// 以下错误不能被 try/catch 捕获:
// 1. 语法错误(ParseError 除外的一些编译时错误)
// 2. 某些内部错误(如内存限制超出)
// ParseError 可以被捕获(PHP 7+)
try {
eval('invalid php code ===');
} catch (ParseError $e) {
echo "语法错误: " . $e->getMessage();
}最佳实践
- 统一使用 Throwable 处理:在全局处理器中使用
catch(Throwable)捕获所有 - 错误级别按环境区分:开发环境显示所有错误,生产环境只记录
- 避免使用 @ 抑制错误:使用条件检查或 try/catch 替代
- 注册全局处理器:set_error_handler + set_exception_handler + register_shutdown_function
- 使用 ErrorException 桥接旧代码:将传统错误转为异常以统一处理
php
<?php
declare(strict_types=1);
// 入口文件统一错误处理
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
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(sprintf(
"[%s] %s: %s in %s:%d",
date('Y-m-d H:i:s'),
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine()
));
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error']);
});