PHP Error
Error levels, display vs log, custom handlers, and dealing with fatal errors.
Error levels
| Level | Meaning | Can catch |
|---|---|---|
| E_ERROR | 致命运行时错误,脚本终止 | 否(需 shutdown 函数) |
| E_WARNING | 警告,继续执行 | 是 |
| E_NOTICE | 提示,可能是 bug | 是 |
| E_DEPRECATED | 使用了过时特性 | 是 |
| E_PARSE | 语法错误,无法运行 | 否 |
| E_ALL | 所有级别 | — |
Development vs production
<?php
// 开发环境
ini_set("display_errors", "1");
ini_set("error_reporting", (string)E_ALL);
// 生产环境
// ini_set("display_errors", "0");
// ini_set("log_errors", "1");
// ini_set("error_log", "/var/log/php/error.log");
echo ini_get("display_errors") === "1" ? "错误会显示在页面上" : "错误只写日志", "\n";
echo (int)ini_get("error_reporting") === E_ALL ? "报告所有级别" : "只报告部分级别", "\n";错误会显示在页面上
报告所有级别display_errors on in production, any uncaught exception prints absolute paths, SQL and credentials onto the page — the first thing a pentester looks for.Custom error handler
<?php
error_reporting(E_ALL);
set_error_handler(function (int $no, string $msg, string $file, int $line): bool {
$level = match ($no) {
E_WARNING, E_USER_WARNING => "WARN",
E_NOTICE, E_USER_NOTICE => "NOTE",
default => "ERR",
};
printf("[%s] %s (%s:%d)\n", $level, $msg, basename($file), $line);
return true; // 返回 true 表示已处理,不再走默认处理
});
echo $undefined ?? "变量不存在", "\n";
trigger_error("这是一条自定义警告", E_USER_WARNING);
trigger_error("这是一条自定义提示", E_USER_NOTICE);
restore_error_handler();[WARN] 这是一条自定义警告 (error.php:14)
[NOTE] 这是一条自定义提示 (error.php:15)echo $undefined ?? "..." triggers no notice because ?? checks existence first. The two lines come from trigger_error.
false lets PHP's default handling proceed (e.g. logging). true takes over completely — including suppressing the log unless you write it yourself.Error vs Exception
Since PHP 7, engine errors (type errors, division by zero, missing methods) throw Error; both implement Throwable.
<?php
try {
intdiv(1, 0);
} catch (DivisionByZeroError $e) {
echo "除零: " . $e->getMessage(), "\n";
}
try {
$x = null;
$x->foo();
} catch (Error $e) {
echo "错误: " . $e->getMessage(), "\n";
}
try {
strlen([]);
} catch (TypeError $e) {
echo "类型: " . $e->getMessage(), "\n";
}
echo "脚本继续执行\n";除零: Division by zero
错误: Call to a member function foo() on null
类型: strlen(): Argument #1 ($str) must be of type string, array given
脚本继续执行Catching fatal errors
E_ERROR bypasses set_error_handler. To record it, use register_shutdown_function with error_get_last().
<?php
register_shutdown_function(function (): void {
$err = error_get_last();
if ($err === null) {
return;
}
$fatal = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
if (in_array($err["type"], $fatal, true)) {
// 生产环境:写日志 + 返回友好的 500 页面
echo "发生致命错误,已记录\n";
}
});
// 模拟:调用不存在的函数会触发 E_ERROR
// undefined_function();
echo "正常执行完毕\n";正常执行完毕The fatal line is commented out, so only the normal output appears. When triggered, the shutdown message prints as the script terminates.
Production error pages
<?php
ini_set("display_errors", "0");
ini_set("log_errors", "1");
set_exception_handler(function (Throwable $e): void {
error_log(sprintf("[%s] %s in %s:%d", get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()));
http_response_code(500);
echo "服务器开小差了,请稍后再试。";
});
throw new RuntimeException("数据库连接失败");服务器开小差了,请稍后再试。The user sees a plain sentence; the log keeps the full stack. Keep them separate — leaking the stack is a security issue, a blank page is a UX issue.
错误级别
| 级别 | 含义 | 可捕获 |
|---|---|---|
| E_ERROR | 致命运行时错误,脚本终止 | 否(需 shutdown 函数) |
| E_WARNING | 警告,继续执行 | 是 |
| E_NOTICE | 提示,可能是 bug | 是 |
| E_DEPRECATED | 使用了过时特性 | 是 |
| E_PARSE | 语法错误,无法运行 | 否 |
| E_ALL | 所有级别 | — |
开发与生产的配置
<?php
// 开发环境
ini_set("display_errors", "1");
ini_set("error_reporting", (string)E_ALL);
// 生产环境
// ini_set("display_errors", "0");
// ini_set("log_errors", "1");
// ini_set("error_log", "/var/log/php/error.log");
echo ini_get("display_errors") === "1" ? "错误会显示在页面上" : "错误只写日志", "\n";
echo (int)ini_get("error_reporting") === E_ALL ? "报告所有级别" : "只报告部分级别", "\n";错误会显示在页面上
报告所有级别display_errors 开着,任何一次未捕获的异常都会把文件绝对路径、SQL 语句、数据库账号打在页面上。这是渗透测试最先看的地方。自定义错误处理器
<?php
error_reporting(E_ALL);
set_error_handler(function (int $no, string $msg, string $file, int $line): bool {
$level = match ($no) {
E_WARNING, E_USER_WARNING => "WARN",
E_NOTICE, E_USER_NOTICE => "NOTE",
default => "ERR",
};
printf("[%s] %s (%s:%d)\n", $level, $msg, basename($file), $line);
return true; // 返回 true 表示已处理,不再走默认处理
});
echo $undefined ?? "变量不存在", "\n";
trigger_error("这是一条自定义警告", E_USER_WARNING);
trigger_error("这是一条自定义提示", E_USER_NOTICE);
restore_error_handler();[WARN] 这是一条自定义警告 (error.php:14)
[NOTE] 这是一条自定义提示 (error.php:15)echo $undefined ?? "..." 不会触发 notice,因为 ?? 会先判断存在性。上面两行输出来自 trigger_error。
false 会继续走 PHP 的默认处理(比如写进 error_log)。返回 true 就完全接管——这也意味着错误不会再进日志,除非你自己写。Error 与 Exception
PHP 7 起,引擎层的错误(类型错误、除零、调用不存在的方法)抛的是 Error,和 Exception 都实现 Throwable。
<?php
try {
intdiv(1, 0);
} catch (DivisionByZeroError $e) {
echo "除零: " . $e->getMessage(), "\n";
}
try {
$x = null;
$x->foo();
} catch (Error $e) {
echo "错误: " . $e->getMessage(), "\n";
}
try {
strlen([]);
} catch (TypeError $e) {
echo "类型: " . $e->getMessage(), "\n";
}
echo "脚本继续执行\n";除零: Division by zero
错误: Call to a member function foo() on null
类型: strlen(): Argument #1 ($str) must be of type string, array given
脚本继续执行捕获致命错误
E_ERROR 不能被 set_error_handler 捕获(它发生时已经没法继续了)。要记录这类错误,用 register_shutdown_function 加 error_get_last()。
<?php
register_shutdown_function(function (): void {
$err = error_get_last();
if ($err === null) {
return;
}
$fatal = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
if (in_array($err["type"], $fatal, true)) {
// 生产环境:写日志 + 返回友好的 500 页面
echo "发生致命错误,已记录\n";
}
});
// 模拟:调用不存在的函数会触发 E_ERROR
// undefined_function();
echo "正常执行完毕\n";正常执行完毕上面故意注释掉了致命错误那行,所以只输出正常结果。真触发时,先输出「发生致命错误」再终止——因为 shutdown 函数在脚本退出时执行。
生产环境的错误页面
<?php
ini_set("display_errors", "0");
ini_set("log_errors", "1");
set_exception_handler(function (Throwable $e): void {
error_log(sprintf("[%s] %s in %s:%d", get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()));
http_response_code(500);
echo "服务器开小差了,请稍后再试。";
});
throw new RuntimeException("数据库连接失败");服务器开小差了,请稍后再试。用户看到一句人话,日志里留下完整堆栈。两者必须分开——把堆栈给用户看是安全问题,只给用户看空白页是体验问题。