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

PHP 框架日誌系統設計實作:從錯誤捕獲到檔案記錄

     編輯:PHP 相關知識

PHP 框架日誌系統設計實作:從錯誤捕獲到檔案記錄

完善的日誌系統是專案穩定運行的基石。本文解析 PHP 框架級日誌系統架構:錯誤捕獲三大函數、日誌記錄器與寫入器分離設計、等級過濾、多通道記錄,參考 Kohana 設計模式實作。

核心錯誤捕獲三大函數

函數用途觸發時機
set_exception_handler捕獲未處理的 Exceptionthrow new Exception 且未被 catch
set_error_handler捕獲 PHP 錯誤 (Warning/Notice/Error)trigger_error、內部錯誤、型別不匹配等
register_shutdown_function捕獲致命錯誤 (E_ERROR, E_PARSE 等)腳本異常終止時 (error_get_last())

統一入口註冊 (bootstrap.php)

<?php
// 註冊到自訂異常處理類別
set_exception_handler([MyException::class, 'exceptionHandler']);
set_error_handler([MyException::class, 'errorHandler']);
register_shutdown_function([MyException::class, 'shutdownHandler']);
?>

異常處理類別:統一轉換為 Exception

<?php
class MyException {
    // 1. 異常處理:已經是 Exception 物件
    public static function exceptionHandler(Throwable $e) {
        self::logException($e);
        // 正式環境顯示友善頁面
        if (ENV_PRODUCTION) {
            self::renderErrorPage(500, '系統異常,請稍後再試');
        } else {
            self::renderDebugPage($e);
        }
    }

    // 2. 錯誤轉異常:將 PHP Error 轉為 ErrorException
    public static function errorHandler(int $severity, string $message, string $file, int $line) {
        if (!(error_reporting() & $severity)) return false; // 不在報告等級內
        throw new ErrorException($message, 0, $severity, $file, $line);
    }

    // 3. 關閉時檢查致命錯誤
    public static function shutdownHandler() {
        $error = error_get_last();
        if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
            $e = new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']);
            self::exceptionHandler($e);
        }
    }

    // 統一日誌記錄入口
    private static function logException(Throwable $e) {
        $log = Log::instance();
        $log->write(Log::ERROR, [
            'message' => $e->getMessage(),
            'file'    => $e->getFile(),
            'line'    => $e->getLine(),
            'trace'   => $e->getTraceAsString(),
            'code'    => $e->getCode(),
        ]);
    }
}
?>

日誌系統核心:Log (容器) + LogWriter (寫入器)

參考 Kohana 設計:Log 管理多個 LogWriter,每個 Writer 對應一個儲存通道 (檔案、資料庫、Syslog、API)。

<?php
class Log {
    const EMERGENCY = 0; const ALERT = 1; const CRITICAL = 2; const ERROR = 3;
    const WARNING = 4; const NOTICE = 5; const INFO = 6; const DEBUG = 7;

    private static $instance;
    private $writers = []; // [writer => minLevel]

    public static function instance(): self {
        return self::$instance ??= new self();
    }

    /** 掛載寫入器
     * @param LogWriter $writer 寫入器實例
     * @param int|array $levels 允許的等級 (單一值或陣列)
     */
    public function attach(LogWriter $writer, $levels = self::DEBUG) {
        $this->writers[$writer] = $levels;
    }

    /** 寫入日誌 */
    public function write(int $level, array $data) {
        foreach ($this->writers as $writer => $levels) {
            $allowed = is_array($levels) ? $levels : range(0, $levels); // 0~level
            if (in_array($level, $allowed)) {
                $writer->write($level, $data);
            }
        }
    }

    // 便利方法
    public function emergency($msg, $ctx=[]) { $this->write(self::EMERGENCY, ['message'=>$msg]+$ctx); }
    public function alert($msg, $ctx=[])    { $this->write(self::ALERT, ['message'=>$msg]+$ctx); }
    public function critical($msg, $ctx=[]) { $this->write(self::CRITICAL, ['message'=>$msg]+$ctx); }
    public function error($msg, $ctx=[])    { $this->write(self::ERROR, ['message'=>$msg]+$ctx); }
    public function warning($msg, $ctx=[])  { $this->write(self::WARNING, ['message'=>$msg]+$ctx); }
    public function notice($msg, $ctx=[])   { $this->write(self::NOTICE, ['message'=>$msg]+$ctx); }
    public function info($msg, $ctx=[])     { $this->write(self::INFO, ['message'=>$msg]+$ctx); }
    public function debug($msg, $ctx=[])    { $this->write(self::DEBUG, ['message'=>$msg]+$ctx); }
}
?>

檔案寫入器實作

<?php
class LogWriter {
    private $path;
    private $dateFormat = 'Y-m-d H:i:s';

    public function __construct(string $path) {
        $this->path = rtrim($path, '/');
        if (!is_dir($this->path)) mkdir($this->path, 0755, true);
    }

    public function write(int $level, array $data) {
        $levelName = $this->levelName($level);
        $date = date($this->dateFormat);
        $message = $data['message'] ?? '';
        $context = '';
        if (count($data) > 1) {
            unset($data['message']);
            $context = ' ' . json_encode($data, JSON_UNESCAPED_UNICODE);
        }

        // 每日一檔:logs/2024-01-15.log
        $file = $this->path . '/' . date('Y-m-d') . '.log';
        $line = "{$date} {$levelName}: {$message}{$context}\n";

        // LOCK_EX 避免並發寫入衝突
        file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
    }

    private function levelName(int $level): string {
        return [
            0=>'EMERGENCY',1=>'ALERT',2=>'CRITICAL',3=>'ERROR',
            4=>'WARNING',5=>'NOTICE',6=>'INFO',7=>'DEBUG'
        ][$level] ?? 'UNKNOWN';
    }
}
?>

啟動配置範例

<?php
// bootstrap.php
Log::instance()
    ->attach(new LogWriter(__DIR__ . '/../logs/debug'), Log::DEBUG)    // DEBUG 以上全記錄
    ->attach(new LogWriter(__DIR__ . '/../logs/error'), Log::ERROR)    // 僅 ERROR 以上
    ->attach(new LogWriter(__DIR__ . '/../logs/notice'), [Log::NOTICE, Log::WARNING, Log::ERROR]); // 指定等級
?>

使用範例

<?php
// 業務代碼中
Log::instance()->info('用戶登入', ['user_id' => 123, 'ip' => $_SERVER['REMOTE_ADDR']]);
Log::instance()->warning('API 回應緩慢', ['api' => '/user/list', 'duration' => 2.5]);

try {
    $payment->charge($order);
} catch (PaymentException $e) {
    Log::instance()->error('付款失敗', ['order_id' => $order->id, 'error' => $e->getMessage()]);
    throw $e;
}
?>

進階擴展方向

  • 多通道:資料庫 Writer、Syslog Writer、Slack/DingTalk Webhook Writer、Elasticsearch Writer
  • 日誌分級:依模組/控制器自動加上 context (如 controller:UserController)
  • 結構化日誌:JSON 格式 + 輸出到 Logstash/Fluentd
  • 效能:非同步寫入 (Redis 隊列 + 背景 Worker)、批次寫入
  • 輪轉:logrotate 系統層面處理,或 PHP 端依大小/日期切割

關鍵設計原則總結

原則實作方式
單一職責Log 只負責分發,Writer 只負責寫入
開放封閉新增 Writer 無需修改 Log 核心
等級過濾掛載時指定允許等級,寫入前快速判斷
錯誤不丟失shutdown 捕獲致命錯誤,確保寫入
生產友善每日一檔、LOCK_EX、UTF-8、可讀時間戳

希望本文助您建立專業級的 PHP 日誌系統。

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