首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 捕獲 Fatal Error 終極方案:register_shutdown_function + error_get_last 完整實戰指南

     編輯:PHP 相關知識

為什麼 try-catch 捕獲不了 Fatal Error?

PHP 錯誤分級:

  • E_ERROR (Fatal):腳本終止、無法被 set_error_handler 捕獲、try-catch 無效(非異常)
  • E_WARNING / E_NOTICE / E_DEPRECATED:非致命、可被 set_error_handler 捕獲、可轉為 ErrorException
  • Exception / Error (PHP 7+):可被 try-catch 捕獲

常見 Fatal Error:Allowed memory size exhausted、Call to undefined function、Class not found、Maximum execution time exceeded、TypeError (PHP 8+ 部分)。

核心機制:register_shutdown_function

註冊的關閉函數在腳本**即將結束時**執行(無論正常返回、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");
    }
});

框架集成模式

Laravel:渲染異常頁面


// 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();

Symfony:Kernel::terminate


// 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();
    // ...
});

ThinkPHP:公共引導文件


// 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() 避免輸出混亂

生產環境部署清單

  1. 統一註冊點:框架入口、公共 bootstrap.php、Composer autoload.php 之後第一行。
  2. 日誌輪轉:logrotate 每日切割、保留 30 天、壓縮舊日誌。
  3. 告警接入:關鍵 Fatal(內存溢出、未定義函數、核心類缺失)對接 PagerDuty/企業微信/釘釘機器人秒級告警。
  4. 請求追蹤 ID:Nginx proxy_set_header X-Request-ID $request_id; 透傳,串聯訪問日誌、應用日誌、監控鏈路。
  5. 定期演練:混沌工程注入 Fatal Error(trigger_error('test', E_USER_ERROR)),驗證日誌、告警、錯誤頁面、用戶體驗完整鏈路。

register_shutdown_function + error_get_last() 是 PHP 獲取 Fatal Error 最後防線。雖無法捕獲 Parse Error、進程被殺等極端情況,但覆蓋了 95%+ 線上致命錯誤場景。將其封裝為標準組件、接入結構化日誌與告警體系、配合框架異常處理機制,構建「發現-告警-定位-恢復」閉環,是 PHP 應用可觀測性建設的基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved