PHP 錯誤分級:
set_error_handler 捕獲、try-catch 無效(非異常)set_error_handler 捕獲、可轉為 ErrorExceptiontry-catch 捕獲常見 Fatal Error:Allowed memory size exhausted、Call to undefined function、Class not found、Maximum execution time exceeded、TypeError (PHP 8+ 部分)。
註冊的關閉函數在腳本**即將結束時**執行(無論正常返回、exit、die、Fatal Error、甚至 E_COMPILE_ERROR 部分情況)。結合 error_get_last() 即可獲取最後發生的錯誤信息。
<?php
// 必須在腳本最早處註冊(自動加載文件、框架入口、公共引導文件)
register_shutdown_function(function () {
$error = error_get_last();
if ($error === null) return; // 正常結束
// 過濾只處理致命錯誤
$fatalTypes = [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR];
if (!in_array($error['type'], $fatalTypes, true)) return;
// 1. 記錄日誌(生產環境核心)
$msg = sprintf(
"[FATAL] %s in %s:%d\nStack: %s",
$error['message'],
$error['file'],
$error['line'],
json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), JSON_UNESCAPED_SLASHES)
);
error_log($msg, 3, __DIR__ . '/logs/fatal_' . date('Y-m-d') . '.log');
// 2. 用戶友好頁面(避免白屏/半截輸出)
// 若已有輸出,嘗試清空緩衝區
while (ob_get_level()) ob_end_clean();
http_response_code(500);
// 區分 CLI / Web
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "Fatal Error: {$error['message']} in {$error['file']}:{$error['line']}\n");
} else {
// 可引入獨立錯誤頁面模板
include __DIR__ . '/templates/500.php';
}
});
register_shutdown_function(function () {
$error = error_get_last();
if (!$error) return;
// 擴展錯誤類型映射
$typeMap = [
E_ERROR => 'E_ERROR',
E_WARNING => 'E_WARNING',
E_PARSE => 'E_PARSE',
E_NOTICE => 'E_NOTICE',
E_CORE_ERROR => 'E_CORE_ERROR',
E_CORE_WARNING => 'E_CORE_WARNING',
E_COMPILE_ERROR => 'E_COMPILE_ERROR',
E_COMPILE_WARNING => 'E_COMPILE_WARNING',
E_USER_ERROR => 'E_USER_ERROR',
E_USER_WARNING => 'E_USER_WARNING',
E_USER_NOTICE => 'E_USER_NOTICE',
E_STRICT => 'E_STRICT',
E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
E_DEPRECATED => 'E_DEPRECATED',
E_USER_DEPRECATED => 'E_USER_DEPRECATED',
];
$typeName = $typeMap[$error['type']] ?? 'UNKNOWN_' . $error['type'];
// 僅處理致命級別
$fatal = [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR];
if (!in_array($error['type'], $fatal, true)) return;
// 構建豐富上下文
$context = [
'timestamp' => date('c'),
'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid('req_', true),
'url' => ($_SERVER['REQUEST_URI'] ?? 'CLI'),
'method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'CLI',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'memory_peak' => memory_get_peak_usage(true),
'memory_limit' => ini_get('memory_limit'),
'execution_time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'],
'php_version' => PHP_VERSION,
'sapi' => php_sapi_name(),
];
// 發送到監控系統(Sentry、Datadog、自建 Elasticsearch 等)
// 這裡演示寫入結構化 JSON 日誌
$logEntry = array_merge([
'level' => 'fatal',
'type' => $typeName,
'message' => $error['message'],
'file' => $error['file'],
'line' => $error['line'],
], $context);
$logLine = json_encode($logEntry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
$logFile = __DIR__ . '/logs/fatal_structured_' . date('Y-m-d') . '.log';
@file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX);
// 如果是內存耗盡,嘗試釋放緩衝區輸出極簡錯誤頁
if (stripos($error['message'], 'Allowed memory size') !== false) {
while (ob_get_level()) ob_end_clean();
header('Content-Type: text/plain; charset=utf-8');
http_response_code(507); // Insufficient Storage
exit("Server memory exhausted. Please try again later.\n");
}
});
// bootstrap/app.php 中最早註冊
$app->singleton('fatal.handler', function () {
return new class {
public function handle() {
register_shutdown_function([$this, 'onShutdown']);
}
public function onShutdown() {
$error = error_get_last();
if (!$error || !in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) return;
// 交給 Laravel 異常處理器
$exception = new ErrorException(
$error['message'], 0, $error['type'], $error['file'], $error['line']
);
app('Illuminate\Contracts\Debug\ExceptionHandler')->report($exception);
if (app()->runningInConsole()) return;
$response = app('Illuminate\Contracts\Debug\ExceptionHandler')->render($request, $exception);
$response->send();
}
};
});
app('fatal.handler')->handle();
// public/index.php
$kernel = new Kernel($env, $debug);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response); // 在此處可捕獲 shutdown 階段錯誤
// 或註冊到 Kernel::terminate 事件
$kernel->addEventListener(KernelEvents::TERMINATE, function () {
$error = error_get_last();
// ...
});
// public/index.php 第一行
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) {
\think\Log::write('Fatal: ' . $error['message'], 'error');
// 返回 JSON 或渲染錯誤模板
if (request()->isAjax()) {
echo json_encode(['code' => 500, 'msg' => 'Server Error']);
} else {
echo view('error/500');
}
}
});
| 場景 | 行為 | 說明 |
|---|---|---|
| 語法錯誤 (Parse Error) | 無法捕獲 | 編譯階段失敗,腳本根本不會運行到註冊代碼 |
| CLI 腳本 | 可捕獲 | 同理生效,輸出到 STDERR |
| FastCGI (PHP-FPM) 進程崩潰 | 可能捕獲不到 | 進程被 SIGKILL/OOM Killer 殺死,無機會執行 shutdown |
| max_execution_time 超時 | 可捕獲 | 引擎會觸發 E_ERROR 然後調用 shutdown functions |
| memory_limit 耗盡 | 可捕獲但受限 | 若連關閉函數執行內存都不夠,會二次 Fatal;建議預留緩衝 |
| 多次 register_shutdown_function | 按註冊順序逆序執行 | 最後註冊最先執行,注意依賴順序 |
| ob_start 開啟輸出緩衝 | 需手動清理 | Fatal 發生前可能已有輸出,ob_end_clean() 避免輸出混亂 |
bootstrap.php、Composer autoload.php 之後第一行。logrotate 每日切割、保留 30 天、壓縮舊日誌。proxy_set_header X-Request-ID $request_id; 透傳,串聯訪問日誌、應用日誌、監控鏈路。trigger_error('test', E_USER_ERROR)),驗證日誌、告警、錯誤頁面、用戶體驗完整鏈路。register_shutdown_function + error_get_last() 是 PHP 獲取 Fatal Error 最後防線。雖無法捕獲 Parse Error、進程被殺等極端情況,但覆蓋了 95%+ 線上致命錯誤場景。將其封裝為標準組件、接入結構化日誌與告警體系、配合框架異常處理機制,構建「發現-告警-定位-恢復」閉環,是 PHP 應用可觀測性建設的基石。