自定义错误处理器
概述
PHP 允许通过 set_error_handler() 注册自定义错误处理器,通过 set_exception_handler() 注册自定义异常处理器。自定义处理器可以在错误/异常发生时执行特定逻辑,如记录日志、发送告警、转换错误类型等。
在现代 PHP 应用中,自定义错误/异常处理器是框架和应用的基础设施组件,用于统一错误处理策略、实现优雅降级。
版本说明
set_error_handler()从 PHP 4.0.1 起可用- PHP 7.0 中
set_error_handler()的回调签名增加了返回类型bool - PHP 8.0+ 中错误处理器推荐将错误转为
ErrorException
基础概念
set_error_handler
php
<?php
declare(strict_types=1);
// set_error_handler 注册自定义错误处理器
// function set_error_handler(
// callable $callback,
// int $error_levels = E_ALL
// ): ?callable
// 回调函数签名:
// function handler(
// int $errno, // 错误级别
// string $errstr, // 错误消息
// string $errfile, // 错误文件
// int $errline, // 错误行号
// array $errcontext // 错误上下文(PHP 8.0 弃用,8.2 移除)
// ): bool
// 返回值:
// true — 已处理,不调用 PHP 内置处理器
// false — 未处理,继续传递给内置处理器set_exception_handler
php
<?php
declare(strict_types=1);
// set_exception_handler 注册未捕获异常的处理器
// function set_exception_handler(
// callable $callback
// ): ?callable
// 回调函数签名:
// function handler(Throwable $exception): void
// 注意:
// - 异常处理器中抛出的异常会导致 Fatal Error
// - 异常处理器执行后脚本终止
// - 如果在异常处理器中调用了 exit/die,后续代码不执行语法与代码
基本自定义错误处理器
php
<?php
declare(strict_types=1);
// 注册错误处理器
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
$levels = [
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_PARSE => 'PARSE',
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',
];
$levelName = $levels[$severity] ?? "LEVEL_{$severity}";
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] [{$levelName}] {$message} in {$file}:{$line}";
error_log($logMessage);
// 返回 true 表示错误已处理
return true;
});
// 触发一个 Notice/Warning(PHP 8+)
echo $undefinedVar;
// error_log 记录:[2025-01-01 12:00:00] [WARNING] Undefined variable $undefinedVar in /path/file.php:45
// 触发用户自定义错误
trigger_error('自定义错误消息', E_USER_WARNING);
// error_log 记录:[2025-01-01 12:00:01] [USER_WARNING] 自定义错误消息 in /path/file.php:50将错误转为异常
php
<?php
declare(strict_types=1);
// 常见模式:将传统错误转为 ErrorException
set_error_handler(function (
int $severity,
string $message,
string $file,
int $line
): bool {
// 检查错误是否在当前 error_reporting 级别内
if (!(error_reporting() & $severity)) {
return false;
}
// 将错误转为 ErrorException 并抛出
throw new ErrorException($message, 0, $severity, $file, $line);
});
// 现在可以用 try/catch 捕获传统错误
try {
$result = 1 / 0; // Warning/DivisionByZeroError
$file = fopen('/nonexistent', 'r');
} catch (ErrorException $e) {
echo "捕获到 ErrorException: " . $e->getMessage();
echo "严重性: " . $e->getSeverity();
} catch (DivisionByZeroError $e) {
echo "除零错误: " . $e->getMessage();
}自定义异常处理器
php
<?php
declare(strict_types=1);
set_exception_handler(function (Throwable $exception): void {
$isCli = PHP_SAPI === 'cli';
if ($isCli) {
fwrite(STDERR, sprintf(
"\n[ERROR] %s: %s\n in %s:%d\n",
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
));
exit(1);
}
// Web 环境
http_response_code(500);
$isDebug = getenv('APP_DEBUG') === 'true';
if ($isDebug) {
echo "<pre>" . htmlspecialchars((string) $exception) . "</pre>";
} else {
echo json_encode([
'error' => [
'message' => '服务器内部错误',
'code' => 500,
]
]);
}
// 记录日志
error_log(sprintf(
"[%s] [EXCEPTION] %s: %s in %s:%d\n%s",
date('Y-m-d H:i:s'),
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine(),
$exception->getTraceAsString()
));
});
// 触发未捕获异常
throw new RuntimeException('测试异常处理器');详细说明
返回 true/false 的控制
php
<?php
declare(strict_types=1);
// 返回 true:错误已处理,PHP 内置处理器不再执行
// 返回 false:错误未处理,PHP 继续使用默认处理器
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
// 只处理 Warning 级别
if ($severity === E_WARNING) {
error_log("[应用自定义处理] Warning: {$message}");
return true; // 已处理,不再输出到页面
}
// 其他级别交给 PHP 默认处理
return false;
});
// E_WARNING 被自定义处理器处理
echo $undefinedVar; // 由自定义处理器记录
// E_USER_ERROR 可能交给默认处理器
// trigger_error('fatal', E_USER_ERROR); // 默认处理器处理错误级别过滤
php
<?php
declare(strict_types=1);
// set_error_handler 第二个参数指定处理的错误级别
// 只处理 Warning 和 Notice
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
error_log("[Handler] {$message}");
return true;
}, E_WARNING | E_NOTICE);
// E_USER_ERROR 不会被上面的处理器捕获
// 因为第二个参数限制了处理范围
// 也可以在处理器内部过滤
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
if ($severity === E_WARNING) {
// 只处理 Warning
error_log("[WARNING] {$message}");
return true;
}
return false; // 其他交给默认处理器
});恢复之前的处理器
php
<?php
declare(strict_types=1);
// set_error_handler 返回之前的处理器
function myHandler(int $severity, string $message, string $file, int $line): bool
{
error_log("[My] {$message}");
return true;
}
$previousHandler = set_error_handler('myHandler');
// 恢复之前的处理器
restore_error_handler();
// set_exception_handler 返回之前的处理器
function myExceptionHandler(Throwable $e): void
{
error_log("[Exception] " . $e->getMessage());
}
$previousExceptionHandler = set_exception_handler('myExceptionHandler');
// 恢复
restore_exception_handler();PHP 8 中错误上下文的移除
php
<?php
declare(strict_types=1);
// PHP 7.x:错误处理器接收第 5 个参数 $errcontext
// PHP 8.0:$errcontext 被弃用
// PHP 8.2:$errcontext 参数被完全移除
// PHP 7.x 写法(不再推荐)
// set_error_handler(function (int $severity, string $message, string $file, int $line, array $context): bool {
// // $context 包含触发错误时的所有变量
// return true;
// });
// PHP 8.1+ 写法(推荐)
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
error_log("[{$severity}] {$message} in {$file}:{$line}");
return true;
});PHP 8.2 移除 $errcontext
$errcontext 参数在 PHP 8.0 中被弃用,在 PHP 8.2 中完全移除。如果处理器函数签名包含该参数,PHP 8.2 会产生兼容性警告。
实战示例
完整的应用错误处理器
php
<?php
declare(strict_types=1);
class AppErrorHandler
{
private string $env;
private string $logPath;
public function __construct(string $env = 'production', string $logPath = '/var/log/app_errors.log')
{
$this->env = $env;
$this->logPath = $logPath;
}
public function register(): void
{
// 配置基础错误报告
error_reporting(E_ALL);
ini_set('display_errors', $this->env === 'development' ? '1' : '0');
ini_set('log_errors', '1');
// 注册错误处理器
set_error_handler($this->handleError(...));
// 注册异常处理器
set_exception_handler($this->handleException(...));
// 注册关机函数(处理 Fatal Error)
register_shutdown_function($this->handleShutdown(...));
}
public function handleError(
int $severity,
string $message,
string $file,
int $line
): bool {
if (!(error_reporting() & $severity)) {
return false;
}
$this->log('error', $message, $file, $line, $severity);
// Fatal 级别的错误转为异常
if (in_array($severity, [E_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR], true)) {
throw new ErrorException($message, 0, $severity, $file, $line);
}
return true;
}
public function handleException(Throwable $exception): void
{
$this->log(
'exception',
$exception->getMessage(),
$exception->getFile(),
$exception->getLine(),
0,
get_class($exception),
$exception->getTraceAsString()
);
$this->renderErrorResponse($exception);
}
public function handleShutdown(): void
{
$error = error_get_last();
if ($error === null) {
return;
}
$fatalLevels = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
if (!in_array($error['type'], $fatalLevels, true)) {
return;
}
$this->log(
'fatal',
$error['message'],
$error['file'],
$error['line'],
$error['type']
);
}
private function log(
string $type,
string $message,
string $file,
int $line,
int $severity = 0,
?string $class = null,
?string $trace = null
): void {
$entry = json_encode([
'timestamp' => date('Y-m-d\TH:i:sP'),
'type' => $type,
'severity' => $severity,
'message' => $message,
'file' => $file,
'line' => $line,
'class' => $class,
'trace' => $trace,
'env' => $this->env,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
error_log($entry . "\n", 3, $this->logPath);
}
private function renderErrorResponse(Throwable $exception): void
{
http_response_code(500);
if ($this->env === 'development') {
echo "<h1>Error</h1>";
echo "<p>" . htmlspecialchars(get_class($exception)) . "</p>";
echo "<p>" . htmlspecialchars($exception->getMessage()) . "</p>";
echo "<pre>" . htmlspecialchars($exception->getTraceAsString()) . "</pre>";
} else {
echo json_encode(['error' => 'Internal Server Error']);
}
}
}
// 使用
$handler = new AppErrorHandler('production');
$handler->register();注意事项
不能处理的错误
php
<?php
declare(strict_types=1);
// set_error_handler 不能处理以下错误:
// 1. E_ERROR — 致命错误
// 2. E_PARSE — 编译时解析错误
// 3. E_CORE_ERROR — PHP 核心初始化错误
// 4. E_CORE_WARNING — PHP 核心初始化警告
// 5. E_COMPILE_ERROR — 编译时错误
// 6. E_COMPILE_WARNING — 编译时警告
// 这些错误需要通过 register_shutdown_function 捕获
register_shutdown_function(function (): void {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
error_log("[FATAL] {$error['message']} in {$error['file']}:{$error['line']}");
}
});异常处理器中不能抛出异常
php
<?php
declare(strict_types=1);
// 异常处理器中再抛出异常会导致 Fatal Error
set_exception_handler(function (Throwable $e): void {
// 危险!如果这里再 throw,会导致 Fatal Error
// throw new RuntimeException('处理器内部错误');
// 安全做法:使用 try/catch 包裹
try {
$this->sendAlert($e);
} catch (Throwable $handlerError) {
error_log("异常处理器内部错误: " . $handlerError->getMessage());
}
});最佳实践
- 在应用入口注册处理器:确保所有错误都被统一处理
- 错误转为 ErrorException:实现 try/catch 统一处理传统错误和异常
- 异常处理器中不要抛异常:捕获所有可能的内部异常
- 区分环境响应:开发环境显示详情,生产环境返回通用错误
- 使用 register_shutdown_function:捕获自定义处理器无法处理的 Fatal Error
php
<?php
declare(strict_types=1);
// 入口文件模板
set_error_handler(function (int $s, string $m, string $f, int $l): bool {
if (!(error_reporting() & $s)) return false;
throw new ErrorException($m, 0, $s, $f, $l);
});
set_exception_handler(function (Throwable $e): void {
error_log($e->__toString());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error']);
});
register_shutdown_function(function (): void {
$e = error_get_last();
if ($e && in_array($e['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
error_log("[FATAL] {$e['message']} in {$e['file']}:{$e['line']}");
}
});