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

PHP 自定義錯誤日誌實作指南:完整捕獲錯誤、異常與致命錯誤

     編輯:PHP 綜合知識

前言

在專案開發中,錯誤日誌的即時記錄與處理至關重要。PHP 內建的錯誤機制雖然方便,但在生產環境下往往缺乏彈性,例如無法自訂日誌格式、無法整合郵件或簡訊通知、也難以捕獲致命錯誤與腳本中斷的情況。本文將詳細介紹如何透過 register_shutdown_function、set_error_handler 與 set_exception_handler 三大核心函數,建構一套完整的自定義錯誤日誌系統。

核心機制解析

1. register_shutdown_function:腳本結束時的最後防線

register_shutdown_function 允許註冊一個在 PHP 腳本執行結束時自動呼叫的函數。無論腳本是正常結束、發生致命錯誤、執行超時,或是使用者強制停止載入,該函數都會被觸發。這使其成為捕獲 E_ERROR、E_PARSE 等無法透過 set_error_handler 捕獲的嚴重錯誤的關鍵手段。

常見觸發場景:

  • 使用者瀏覽器強制停止載入頁面
  • 腳本執行時間超過 max_execution_time 限制
  • PHP 代碼執行完成,但過程中發生錯誤、警告或異常

2. set_error_handler:自訂錯誤處理流程

透過 set_error_handler 可攔截大多數非致命錯誤(如 E_WARNING、E_NOTICE、E_USER_ERROR 等),並自訂處理邏輯,例如寫入日誌檔、發送通知等。搭配 trigger_error() 可主動觸發自訂錯誤。

3. set_exception_handler:統一異常處理入口

所有未被 try-catch 捕獲的異常,最終都會流向此處理器。可在此統一格式化異常資訊、記錄堆疊追蹤、回報監控系統等。

完整實作範例:PhpError 類別

以下封裝一個靜態類別 PhpError,整合上述三大機制,支援自訂日誌格式、IP 記錄、錯誤類型中文化對照,並採用 file_put_contents 以 FILE_APPEND 模式寫入日誌檔(生產環境建議搭配日誌輪轉機制)。

<?php
class PhpError
{
    /**
     * 一般錯誤處理器 (對應 set_error_handler)
     */
    public static function errorHandler($errno, $errstr, $errfile, $errline)
    {
        $errType = self::parseErrorType($errno);
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'cli';
        $msg = sprintf(
            "%s [%s] [%d] [-] [%s] [application] %s in %s:%d\n",
            date('Y-m-d H:i:s'),
            $ip,
            $errno,
            $errType,
            $errstr,
            $errfile,
            $errline
        );
        self::writeLog($msg);
        // 返回 false 讓 PHP 繼續執行內建錯誤處理器 (視需求決定)
        return false;
    }

    /**
     * 關閉處理器 (對應 register_shutdown_function)
     * 僅能獲取最後一個發生的錯誤
     */
    public static function shutdownFunction()
    {
        $lastError = error_get_last();
        if (!$lastError) {
            return; // 正常結束
        }

        // 過濾掉已由 errorHandler 處理過的非致命錯誤,避免重複記錄
        $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING];
        if (!in_array($lastError['type'], $fatalTypes, true)) {
            return;
        }

        $errType = self::parseErrorType($lastError['type']);
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'cli';
        $msg = sprintf(
            "%s [%s] [%d] [-] [%s] [application] %s in %s:%d\n",
            date('Y-m-d H:i:s'),
            $ip,
            $lastError['type'],
            $errType,
            $lastError['message'],
            $lastError['file'],
            $lastError['line']
        );
        self::writeLog($msg);
    }

    /**
     * 未捕獲異常處理器 (對應 set_exception_handler)
     */
    public static function appException($exception)
    {
        $msg = sprintf(
            "%s [exception] [%s] in %s:%d\nStack trace:\n%s\n",
            date('Y-m-d H:i:s'),
            $exception->getMessage(),
            $exception->getFile(),
            $exception->getLine(),
            $exception->getTraceAsString()
        );
        self::writeLog($msg);

        // 生產環境建議顯示友善錯誤頁面,而非直接輸出訊息
        if (ini_get('display_errors')) {
            echo "Exception: " . $exception->getMessage();
        }
    }

    /**
     * 寫入日誌檔案
     */
    private static function writeLog(string $msg): void
    {
        $logPath = __DIR__ . '/logs/app.log';
        // 確保目錄存在
        $dir = dirname($logPath);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        // 生產環境需注意檔案鎖定、大小限制與輪轉
        file_put_contents($logPath, $msg, FILE_APPEND | LOCK_EX);
    }

    /**
     * 錯誤代碼轉中文名稱
     */
    private static function parseErrorType(int $type): string
    {
        return match ($type) {
            E_ERROR             => 'Fatal Error',
            E_WARNING           => 'Warning',
            E_PARSE             => 'Parse Error',
            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 Standards',
            E_RECOVERABLE_ERROR => 'Recoverable Error',
            E_DEPRECATED        => 'Deprecated',
            E_USER_DEPRECATED   => 'User Deprecated',
            default             => 'Unknown (' . $type . ')',
        };
    }
}

// 註冊處理器
set_error_handler(['PhpError', 'errorHandler']);
set_exception_handler(['PhpError', 'appException']);
register_shutdown_function(['PhpError', 'shutdownFunction']);

關鍵細節與最佳實踐

  1. 避免重複記錄:shutdown_function 中使用 error_get_last() 只能拿到最後一個錯誤,且無堆疊資訊。建議僅記錄致命類型錯誤,非致命錯誤交由 errorHandler 處理即可。
  2. 日誌檔案權限與輪轉:範例使用 FILE_APPEND | LOCK_EX 確保寫入原子性。實際部署建議搭配 logrotate 或自行實作按日期/大小分割。
  3. CLI 環境下的 $_SERVER['REMOTE_ADDR']:命令列執行時不存在該變數,範例以 ?? 'cli' 兼容。
  4. 效能考量:錯誤處理器不宜執行耗時操作(如發送 HTTP 請求),若需異步通知請寫入隊列由背景 Worker 處理。
  5. 錯誤類型完整對照:parseErrorType 使用 PHP 8.0+ 的 match 表達式,涵蓋所有核心錯誤常數,便於日誌分析與篩選。
  6. 啟用方式

    將上述類別檔案 (如 PhpError.php) 在專案啟動早期 require_once 並註冊三大處理器即可。配合 php.ini 設定 log_errors = On、error_log 指定系統錯誤日誌路徑,可構建雙層日誌保障體系。

    結語

    自定義錯誤日誌系統能大幅提升問題排查效率,特別是在高流量、高可用性要求的專案中。本文提供的 PhpError 類別具備結構化日誌格式、完整錯誤類型覆蓋、防重複記錄等實戰特性,可直接套用或依專案需求擴充(如整合 Monolog、Sentry、ELK Stack 等)。希望能幫助你建立更穩健的 PHP 應用監控體系。

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