完善的日誌系統是專案穩定運行的基石。本文解析 PHP 框架級日誌系統架構:錯誤捕獲三大函數、日誌記錄器與寫入器分離設計、等級過濾、多通道記錄,參考 Kohana 設計模式實作。
| 函數 | 用途 | 觸發時機 |
|---|---|---|
| set_exception_handler | 捕獲未處理的 Exception | throw new Exception 且未被 catch |
| set_error_handler | 捕獲 PHP 錯誤 (Warning/Notice/Error) | trigger_error、內部錯誤、型別不匹配等 |
| register_shutdown_function | 捕獲致命錯誤 (E_ERROR, E_PARSE 等) | 腳本異常終止時 (error_get_last()) |
<?php
// 註冊到自訂異常處理類別
set_exception_handler([MyException::class, 'exceptionHandler']);
set_error_handler([MyException::class, 'errorHandler']);
register_shutdown_function([MyException::class, 'shutdownHandler']);
?>
<?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(),
]);
}
}
?>
參考 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;
}
?>
controller:UserController)| 原則 | 實作方式 |
|---|---|
| 單一職責 | Log 只負責分發,Writer 只負責寫入 |
| 開放封閉 | 新增 Writer 無需修改 Log 核心 |
| 等級過濾 | 掛載時指定允許等級,寫入前快速判斷 |
| 錯誤不丟失 | shutdown 捕獲致命錯誤,確保寫入 |
| 生產友善 | 每日一檔、LOCK_EX、UTF-8、可讀時間戳 |
希望本文助您建立專業級的 PHP 日誌系統。