4. 自定义错误处理函数:set_error_handler() 的使用与参数详解
PHP 默认的错误处理方式,说白了就是直接把错误信息甩到页面上。这在开发阶段还行,但到了生产环境,那就是灾难——用户看到一堆莫名其妙的代码,安全隐患也暴露无遗。所以,我们需要自己接管错误处理。
我个人习惯,项目一启动,第一件事就是注册自定义错误处理器。今天我们就来聊聊 set_error_handler() 这个函数,以及它的回调函数该怎么写。
4.1 set_error_handler() 的基本用法
这个函数的作用很简单:告诉 PHP,“嘿,别自己处理错误了,让我来。” 它的签名是这样的:
set_error_handler(callable $handler, int $error_levels = E_ALL): ?callable
第一个参数是回调函数,第二个参数是你要捕获的错误级别。默认是 E_ALL,也就是所有错误都抓。返回值是之前注册的错误处理器,方便你恢复。
来看个最简单的例子:
function myErrorHandler($errno, $errstr, $errfile, $errline) {
echo "出错了:[$errno] $errstr 在文件 $errfile 的第 $errline 行";
return true; // 告诉 PHP 我们已经处理了
}
set_error_handler('myErrorHandler');
// 故意触发一个错误
echo $undefinedVar;
运行这段代码,你不会看到 PHP 默认的错误输出,而是我们自定义的提示信息。嗯,这里要注意:return true 表示错误已被处理,PHP 不会再执行内部错误处理。如果返回 false,PHP 会继续按默认方式处理。
4.2 错误处理函数的参数详解
回调函数最多可以接收 5 个参数。我刚开始用的时候,经常只写前两个,后来发现后面几个参数在调试时特别有用。我们一个一个来看:
| 参数 | 类型 | 说明 |
|---|---|---|
$errno |
int | 错误级别,比如 E_WARNING、E_NOTICE 等 |
$errstr |
string | 错误信息,人类可读的描述 |
$errfile |
string | 发生错误的文件名 |
$errline |
int | 错误所在的行号 |
$errcontext |
array | 错误发生时所有变量的符号表(PHP 7.2 后废弃) |
$errcontext 参数在 PHP 7.2 中已被废弃,8.0 中彻底移除。别再用它了,我当年维护老项目时踩过这个坑。
写一个更完整的例子:
function myErrorHandler($errno, $errstr, $errfile, $errline) {
// 根据错误级别做不同处理
switch ($errno) {
case E_WARNING:
$level = '警告';
break;
case E_NOTICE:
$level = '通知';
break;
case E_USER_ERROR:
$level = '用户错误';
break;
default:
$level = '其他错误';
}
$message = "[$level] $errstr (文件: $errfile, 行: $errline)";
// 记录到日志文件
error_log($message, 3, '/var/log/php_errors.log');
// 如果是致命错误,可以终止脚本
if ($errno == E_USER_ERROR) {
die("发生严重错误,请联系管理员");
}
return true;
}
set_error_handler('myErrorHandler');
4.3 错误级别的过滤
你想想看,有时候我们只想捕获特定类型的错误。比如开发阶段想看所有错误,但生产环境只想记录警告和错误,忽略通知。这时候第二个参数就派上用场了:
// 只捕获 E_WARNING 和 E_ERROR
set_error_handler('myErrorHandler', E_WARNING | E_ERROR);
// 捕获所有错误,除了 E_NOTICE
set_error_handler('myErrorHandler', E_ALL & ~E_NOTICE);
我曾经在一个老项目中,发现日志文件被 E_NOTICE 级别的错误刷爆了。后来加上过滤,日志量直接降了 90%。
4.4 恢复之前的错误处理器
有时候你只想临时接管错误处理,比如在执行一段不信任的代码时。完事之后,最好恢复原来的处理器:
// 保存原来的处理器
$oldHandler = set_error_handler('myTempHandler');
// 执行一些可能出错的代码
// ...
// 恢复
set_error_handler($oldHandler);
或者用 restore_error_handler() 直接恢复:
set_error_handler('myTempHandler');
// ... 执行代码
restore_error_handler();
set_error_handler([$this, 'handleError'])。我习惯把错误处理封装成一个独立的类,方便复用。
4.5 哪些错误是 set_error_handler 抓不到的?
这里有个大坑,我必须提醒你:set_error_handler() 不能捕获所有类型的错误。它只能处理运行时错误,比如 E_WARNING、E_NOTICE、E_USER_ERROR 等。但以下情况它无能为力:
- 致命错误(E_ERROR、E_PARSE 等)——脚本直接崩了,根本来不及调用你的处理器
- 编译时错误——比如语法错误,PHP 还没执行到你的代码就挂了
- 核心错误——比如内存耗尽
那怎么办?别急,后面我们会讲到 register_shutdown_function() 和异常处理,它们可以配合起来,形成一个完整的错误处理体系。
4.6 实战:一个完整的错误处理器模板
最后,分享一个我在项目中常用的模板。它结合了日志记录、错误级别过滤和友好的用户提示:
class ErrorHandler {
private $logFile;
private $debugMode;
public function __construct($logFile, $debugMode = false) {
$this->logFile = $logFile;
$this->debugMode = $debugMode;
}
public function register() {
set_error_handler([$this, 'handle']);
}
public function handle($errno, $errstr, $errfile, $errline) {
// 检查是否被 @ 操作符抑制
if (!(error_reporting() & $errno)) {
return false;
}
$level = $this->getLevelName($errno);
$log = sprintf(
"[%s] %s in %s on line %d",
$level,
$errstr,
$errfile,
$errline
);
// 写入日志
error_log(date('Y-m-d H:i:s') . ' ' . $log . PHP_EOL, 3, $this->logFile);
// 开发环境直接输出
if ($this->debugMode) {
echo "<pre>$log</pre>";
}
return true;
}
private function getLevelName($errno) {
$levels = [
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',
E_USER_DEPRECATED => 'User Deprecated',
];
return $levels[$errno] ?? 'Unknown';
}
}
// 使用
$handler = new ErrorHandler('/tmp/php_errors.log', true);
$handler->register();
这个模板我用了好几年,基本没出过问题。你根据自己的项目需求调整一下就行。
好了,自定义错误处理函数就聊到这里。记住一点:错误处理不是可有可无的装饰,而是生产环境的生命线。下一节我们聊聊如何用 register_shutdown_function() 来捕获那些漏网之鱼。