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

PHP 異常與錯誤完整攔截方案:致命錯誤報警、日誌記錄、生產環境最佳實踐

     編輯:PHP 相關知識

PHP 異常與錯誤完整攔截方案:致命錯誤報警、日誌記錄、生產環境最佳實踐

生產環境關閉 display_errors 卻未記錄日誌,會導致隨機錯誤無法追蹤、付款回調丟失無感知、長時間腳本靜默中斷。本文提供一套完整的錯誤/異常/致命錯誤攔截機制,包含自定義異常類別、錯誤處理器、關閉函式、去重記錄、報警整合,已驗證於高併發線上環境。

核心機制三大支柱

  1. set_exception_handler:攔截未捕獲的 Exception/Error(PHP 7+)。
  2. set_error_handler:攔截 E_WARNING、E_NOTICE 等非致命錯誤,轉為異常統一處理。
  3. register_shutdown_function:腳本結束時檢查 error_get_last() 捕獲 E_ERROR、E_PARSE 等致命錯誤。

一、自定義異常類別:ErrorHandlerException

<?php
/**
 * 統一錯誤異常類別:封裝代碼、訊息、檔案、行號、嚴重度、本地代碼/名稱
 */
class ErrorHandlerException extends ErrorException {
    // 本地錯誤代碼映射(便於資料庫索引、報警分類)
    public static $localCode = [
        E_ERROR             => 4006,
        E_WARNING           => 4015,
        E_PARSE             => 4008,
        E_NOTICE            => 4007,
        E_CORE_ERROR        => 4003,
        E_CORE_WARNING      => 4004,
        E_COMPILE_ERROR     => 4001,
        E_COMPILE_WARNING   => 4002,
        E_USER_ERROR        => 4012,
        E_USER_WARNING      => 4014,
        E_USER_NOTICE       => 4013,
        E_STRICT            => 4010,
        E_RECOVERABLE_ERROR => 4009,
        E_DEPRECATED        => 4005,
        E_USER_DEPRECATED   => 4011,
        4016                => 4016, // 自定義錯誤
    ];

    // 使用者友好名稱
    public static $localName = [
        E_ERROR             => 'PHP Fatal Error',
        E_WARNING           => 'PHP Warning',
        E_PARSE             => 'PHP Parse Error',
        E_NOTICE            => 'PHP Notice',
        E_CORE_ERROR        => 'PHP Core Error',
        E_CORE_WARNING      => 'PHP Core Warning',
        E_COMPILE_ERROR     => 'PHP Compile Error',
        E_COMPILE_WARNING   => 'PHP Compile Warning',
        E_USER_ERROR        => 'PHP User Error',
        E_USER_WARNING      => 'PHP User Warning',
        E_USER_NOTICE       => 'PHP User Notice',
        E_STRICT            => 'PHP Strict Warning',
        E_RECOVERABLE_ERROR => 'PHP Recoverable Error',
        E_DEPRECATED        => 'PHP Deprecated Warning',
        E_USER_DEPRECATED   => 'PHP User Deprecated Warning',
        4016                => 'Custom Error',
    ];

    public function __construct($message = '', $code = 0, $severity = 1, $filename = __FILE__, $line = __LINE__, Exception $previous = null) {
        parent::__construct($message, $code, $severity, $filename, $line, $previous);
    }

    // 判斷是否為致命錯誤
    public static function isFatalError(array $error): bool {
        $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING];
        return isset($error['type']) && in_array($error['type'], $fatalTypes, true);
    }

    public static function getLocalCode(int $code): int {
        return self::$localCode[$code] ?? self::$localCode[4016];
    }

    public static function getName(int $code): string {
        return self::$localName[$code] ?? self::$localName[4016];
    }
}

二、錯誤處理器核心類:ErrorHandler

<?php
require_once 'ErrorHandlerException.php';

class ErrorHandler {
    public $argvs = []; // CLI 參數
    public $memoryReserveSize = 262144; // 256KB 備用記憶體
    private $_memoryReserve = '';

    // 註冊攔截器
    public function register(): void {
        ini_set('display_errors', '0');
        set_exception_handler([$this, 'handleException']);
        set_error_handler([$this, 'handleError']);
        // 預留記憶體,確保致命錯誤處理有可用空間
        if ($this->memoryReserveSize > 0) {
            $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
        }
        register_shutdown_function([$this, 'handleFatalError']);
    }

    // 取消註冊
    public function unregister(): void {
        restore_error_handler();
        restore_exception_handler();
    }

    // 處理未捕獲異常
    public function handleException(Throwable $exception): void {
        $this->unregister();
        try {
            $this->logException($exception);
            exit(1);
        } catch (Throwable $e) {
            exit(1);
        }
    }

    // 處理一般錯誤 → 轉為異常
    public function handleError(int $code, string $message, string $file, int $line): bool {
        // 僅處理 error_reporting 範圍內且非輕微錯誤
        if ((error_reporting() & $code) && !in_array($code, [E_NOTICE, E_WARNING, E_USER_NOTICE, E_USER_WARNING, E_DEPRECATED], true)) {
            $exception = new ErrorHandlerException($message, $code, $code, $file, $line);
            
            // __toString 中發生錯誤不可拋出異常,直接處理
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
            array_shift($trace);
            foreach ($trace as $frame) {
                if (($frame['function'] ?? '') === '__toString') {
                    $this->handleException($exception);
                    exit(1);
                }
            }
            throw $exception;
        }
        return false; // 繼續內建處理器
    }

    // 處理致命錯誤
    public function handleFatalError(): void {
        unset($this->_memoryReserve); // 釋放備用記憶體
        $error = error_get_last();
        if ($error && ErrorHandlerException::isFatalError($error)) {
            $exception = new ErrorHandlerException(
                $error['message'], $error['type'], $error['type'], $error['file'], $error['line']
            );
            $this->logException($exception);
            exit(1);
        }
    }

    // 取得伺服器 IP
    final public function getServerIp(): string {
        return $_SERVER['SERVER_ADDR'] 
            ?? $_SERVER['LOCAL_ADDR'] 
            ?? (isset($_SERVER['HOSTNAME']) ? gethostbyname($_SERVER['HOSTNAME']) : '')
            ?? getenv('SERVER_ADDR') 
            ?? 'unknown';
    }

    // 取得當前請求 URI
    public function getCurrentUri(): string {
        if (isset($_SERVER['REMOTE_ADDR'])) { // Web 模式
            return 'https://' . ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']) . $_SERVER['REQUEST_URI'];
        } else { // CLI 模式
            $params = $this->argvs;
            $uri = array_shift($params);
            foreach ($params as $param) $uri .= ' ' . $param;
            return $uri;
        }
    }

    // 記錄異常(含歷史鏈)
    final public function logException(Throwable $e): void {
        $messages = [];
        do {
            $messages[] = (string)$e; // ErrorException::__toString 包含檔案、行號、代碼、訊息
        } while ($e = $e->getPrevious());
        
        $errorData = [
            'add_time'   => time(),
            'title'      => ErrorHandlerException::getName($e->getCode()),
            'message'    => implode("\r\n", $messages),
            'server_ip'  => $this->getServerIp(),
            'code'       => ErrorHandlerException::getLocalCode($e->getCode()),
            'file'       => $e->getFile(),
            'line'       => $e->getLine(),
            'url'        => $this->getCurrentUri(),
        ];
        $this->logError($errorData);
    }

    // 實際記錄邏輯(需自行實作:資料庫、檔案、Syslog、Sentry 等)
    public function logError(array $error): void {
        // 範例:寫入檔案(生產環境建議異步寫入資料庫/日誌系統)
        $logLine = json_encode($error, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
        @file_put_contents('/var/log/php_errors.json', $logLine, FILE_APPEND | LOCK_EX);
        
        // 進階:去重機制(MD5(code+file+line+message) + Redis 過期 1 小時)
        // 進階:報警整合(郵件、Slack、DingTalk、Prometheus Alertmanager)
    }
}

三、啟動引導檔:registerErrorHandler.php

<?php
/**
 * 入口檔案最早引入此檔案
 * 定義 DEBUG_ERROR=1 開啟調試模式(顯示錯誤、不記錄)
 * 定義 DEBUG_ERROR=0 生產模式(不顯示、記錄日誌、報警)
 */
define('DEBUG_ERROR', 0); // 生產環境設 0

require_once 'ErrorHandler.php';

class RegisterErrorHandler {
    public static function register(): void {
        global $argv;
        if (DEBUG_ERROR) {
            ini_set('display_errors', '1');
            error_reporting(-1);
            return;
        }
        
        ini_set('error_reporting', '-1'); // 記錄所有等級
        ini_set('display_errors', '0');
        ini_set('log_errors', '1');
        ini_set('error_log', '/var/log/php_errors.log'); // PHP 內建錯誤日誌
        
        $handler = new ErrorHandler();
        $handler->argvs = $argv;
        $handler->register();
    }
}

RegisterErrorHandler::register();

四、入口檔案整合範例

<?php
// public/index.php 或 CLI 入口
require_once __DIR__ . '/../bootstrap/registerErrorHandler.php'; // 最優先引入

// 其餘框架啟動代碼...
$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->run();

五、關鍵細節與避坑指南

項目說明
set_error_handler 限制無法攔截 E_ERROR、E_PARSE、E_CORE_ERROR、E_CORE_WARNING、E_COMPILE_ERROR、E_COMPILE_WARNING。但這些屬編譯/核心錯誤,上線前必須修復,不影響運行期監控。
致命錯誤記憶體不足預留 memoryReserveSize (256KB) 確保 handleFatalError 有空間執行 error_get_last()、序列化、寫日誌。
錯誤去重使用 md5($code.$file.$line.$message) 作為 Key,Redis SETEX key 3600 1,僅首次記錄並報警。
CLI 相容性儲存 $argv 供 getCurrentUri() 重建命令列。
錯誤日誌格式建議 JSON Lines 格式,便於 ELK/Filebeat 採集、Kibana 分析。
報警整合logError() 中判斷 $error['code'] 為致命代碼 → 發送郵件/Slack/DingTalk Webhook。

六、進階擴展建議

  1. 整合 Monolog:替換 logError() 為 $logger->error(),支援多 Handler(Stream、Slack、Syslog、Elasticsearch)。
  2. Sentry 整合:Sentry\init(['dsn' => '...']) 自動捕獲未處理異常、致命錯誤,提供堆疊追蹤、版本關聯、使用者上下文。
  3. 非同步日誌寫入:使用 swoole_async_writefile 或投遞至 Redis List,由獨立 Worker 消費寫入磁碟/資料庫,避免阻塞請求。
  4. 分散式追蹤:在 logException 中注入 trace_id、span_id,串聯微服務調用鏈。

總結

本方案透過 set_exception_handler、set_error_handler、register_shutdown_function 三重保障,實現 PHP 所有錯誤類型的統一攔截、轉換、記錄與報警。核心優勢在於:區分調試/生產模式、預留記憶體處理致命錯誤、自定義本地代碼便於分類、支援 CLI/Web 雙模式、可擴展去重與報警。直接引入 registerErrorHandler.php 並實作 logError() 即可落地生產環境。

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