PHP 預設錯誤輸出直接暴露文件路徑、行號、代碼片段——這對開發者友好,對攻擊者更友好。生產環境若關閉 display_errors,又失去調試能力與用戶告知機制。set_error_handler()(PHP 4.1.0+)提供了解決方案:
string set_error_handler ( callable $error_handler [, int $error_types = E_ALL | E_STRICT ] )
$error_handler:回調函數,接收 4 參數 ($errno, $errstr, $errfile, $errline)$error_types:要處理的錯誤級位掩碼,預設所有非致命錯誤以下錯誤不會進入自定義處理器,仍按預設方式處理:
E_ERROR - 致命運行時錯誤E_PARSE - 編譯時解析錯誤E_CORE_ERROR - PHP 核心啟動錯誤E_CORE_WARNING - PHP 核心啟動警告E_COMPILE_ERROR - 編譯錯誤E_COMPILE_WARNING - 編譯警告補救:使用 register_shutdown_function() 捕獲致命錯誤(見文章 144890)。
註冊自定義處理器後,error_reporting() 設定將被忽略,所有非上述類型的錯誤均交給自定義函數處理。需在處理器內部自行過濾級別。
$isAdmin = checkAdminIdentity(); // IP白名單、登錄用戶等
function myErrorHandler($errno, $errstr, $errfile, $errline) {
global $isAdmin;
// 非管理員:過濾物理路徑
if (!$isAdmin) {
$errfile = str_replace(getcwd(), '', $errfile);
$errstr = str_replace(getcwd(), '', $errstr);
}
switch ($errno) {
case E_ERROR:
case E_CORE_ERROR:
case E_COMPILE_ERROR:
case E_USER_ERROR:
$msg = "ERROR: [ID $errno] $errstr (Line: $errline of $errfile)";
error_log($msg);
if (!$isAdmin) {
echo "系統錯誤,請聯系管理員。";
} else {
echo "$msg
";
}
exit(1);
case E_WARNING:
case E_CORE_WARNING:
case E_COMPILE_WARNING:
case E_USER_WARNING:
$msg = "WARNING: [ID $errno] $errstr (Line: $errline of $errfile)";
error_log($msg);
if ($isAdmin) echo "$msg
";
break;
case E_NOTICE:
case E_USER_NOTICE:
// 生產環境通常忽略
if ($isAdmin) error_log("NOTICE: [ID $errno] $errstr");
break;
default:
error_log("UNKNOWN: [ID $errno] $errstr");
}
// 返回 true 表示已處理,不再調用內部處理器
return true;
}
set_error_handler('myErrorHandler');
function advancedErrorHandler($errno, $errstr, $errfile, $errline) {
$context = [
'timestamp' => date('c'),
'level' => getErrorLevelName($errno),
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5),
'url' => $_SERVER['REQUEST_URI'] ?? '',
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'user_id' => $_SESSION['user_id'] ?? null,
];
// 寫入結構化日誌(JSON Lines 格式)
$logLine = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
file_put_contents(__DIR__ . '/logs/errors-' . date('Y-m-d') . '.log', $logLine, FILE_APPEND | LOCK_EX);
// 發送到監控系統(Sentry、Slack、鉚釘等)
if (in_array($errno, [E_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR])) {
sendAlert($context);
}
// 用戶端響應
if (isCli()) {
echo "ERROR: {$context['message']}\n";
} else {
http_response_code(500);
if ($isAdmin) {
echo "" . htmlspecialchars($context['message']) . "
";
} else {
include __DIR__ . '/templates/500.html';
}
}
return true;
}
function getErrorLevelName($errno) {
static $map = [
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',
];
return $map[$errno] ?? "UNKNOWN($errno)";
}
// 手動觸發用戶級錯誤
if (!$db->connect()) {
trigger_error("資料庫連接失敗: {$db->error}", E_USER_ERROR);
}
// 示例:模擬 MySQL 連接失敗
$link = @mysql_pconnect("localhost:3333", "root", "password"); // 故意錯誤端口
set_error_handler('myErrorHandler');
if (!$link) {
trigger_error("出錯了", E_USER_ERROR);
}
set_exception_handler() + register_shutdown_function() 形成完整體系return true 表示已處理,但致命錯誤仍需 exit相關函數:set_exception_handler()、register_shutdown_function()、error_get_last()、trigger_error()、error_log()