生產環境應用程式離不開日誌記錄。優秀的日誌類應具備:單例模式避免重複開啟檔案控制代碼、自動分檔防止單一檔案過大、錯誤分級區分一般事件與嚴重錯誤、效能優化減少 I/O 開銷。本文實作一個生產就緒的 Log 類別,並說明設計考量。
<?php
/**
* 單例模式日誌處理類
* 功能:自動分檔、支援多級別、延遲開啟檔案、執行結束自動關閉
*/
class Log {
// 單例實例
private static ?self $instance = null;
// 檔案控制代碼(靜態,跨實例共享)
private static ?resource $handle = null;
// 設定參數(從常數或建構子注入)
private bool $enabled;
private string $dir;
private int $max_size; // 位元組
private string $prefix; // 檔案前綴,如 'log_'
// 私有建構子,防止外部實例化
private function __construct() {
// 這裡假設專案定義了常數,實際專案請改為配置檔注入
$this->enabled = defined('LOG_SWITCH') ? LOG_SWITCH : true;
$this->dir = defined('LOG_FILE_PATH') ? LOG_FILE_PATH : __DIR__ . '/logs/';
$this->max_size = defined('LOG_MAX_LEN') ? LOG_MAX_LEN : 10 * 1024 * 1024; // 10MB
$this->prefix = 'log_';
// 確保目錄存在
if (!is_dir($this->dir)) {
@mkdir($this->dir, 0755, true);
}
}
// 取得單例
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// 禁止克隆與反序列化
private function __clone() {}
public function __wakeup() {
throw new Exception("不能反序列化單例");
}
/**
* 寫入日誌
* @param int $type 0=一般事件(THING), 1=錯誤(ERROR), 2=警告(WARN), 3=除錯(DEBUG)
* @param string $msg 日誌訊息
* @param string $time 時間字串,預設當前時間
*/
public function log(int $type, string $msg, string $time = ''): void {
if (!$this->enabled) return;
$time = $time ?: date('Y-m-d H:i:s');
$prefix = match($type) {
1 => 'ERROR',
2 => 'WARN',
3 => 'DEBUG',
default => 'INFO',
};
$line = "[$time] [$prefix] $msg" . PHP_EOL;
// 延遲開啟檔案
if (self::$handle === null) {
$this->openLogFile();
}
// 寫入前檢查檔案大小,超過則輪替
if (ftell(self::$handle) + strlen($line) > $this->max_size) {
fclose(self::$handle);
$this->openLogFile();
}
fwrite(self::$handle, $line);
}
// 開啟/建立最新日誌檔
private function openLogFile(): void {
$suffix = $this->getNextSuffix();
$filename = $this->dir . $this->prefix . $suffix . '.log';
self::$handle = fopen($filename, 'a'); // 追加模式
if (self::$handle === false) {
throw new RuntimeException("無法開啟日誌檔案: $filename");
}
}
// 取得下一個可用後綴號(掃描現有檔案)
private function getNextSuffix(): int {
$max = 0;
if ($dh = opendir($this->dir)) {
while (($file = readdir($dh)) !== false) {
if ($file === '.' || $file === '..') continue;
if (preg_match('/^' . preg_quote($this->prefix, '/') . '(\d+)\.log$/', $file, $m)) {
$max = max($max, (int)$m[1]);
}
}
closedir($dh);
}
// 檢查最新檔案是否已超過大小限制
$latest_file = $this->dir . $this->prefix . $max . '.log';
if (file_exists($latest_file) && filesize($latest_file) >= $this->max_size) {
$max++;
}
return $max;
}
// 關閉檔案控制代碼(可在腳本結束手動呼叫,或註冊 shutdown_function)
public function close(): void {
if (self::$handle !== null) {
fclose(self::$handle);
self::$handle = null;
}
}
// 解構子自動關閉
public function __destruct() {
$this->close();
}
}
// 註冊關閉函數確保資源釋放
register_shutdown_function([Log::getInstance(), 'close']);
?>
<?php
require_once 'Log.php';
// 取得單例
$logger = Log::getInstance();
// 記錄一般事件
$logger->log(0, '使用者登入成功', '2024-01-15 10:30:00');
// 記錄錯誤
try {
// 業務邏輯...
} catch (Exception $e) {
$logger->log(1, '資料庫連線失敗: ' . $e->getMessage());
}
// 記錄警告
$logger->log(2, 'API 回應逾時,重試中...');
// 記錄除錯資訊(正式環境可關閉)
$logger->log(3, 'SQL 查詢耗時: 120ms', '2024-01-15 10:30:05');
// 程式結束前會自動關閉,也可手動呼叫
// $logger->close();
?>
全程只建立一個 Log 實例與一個檔案控制代碼(self::$handle),避免多次 fopen/fclose 開銷,亦避免並發寫入導致檔案損毀。
log_N.log。ftell() 當前指標位置 + 即將寫入長度是否超過 $max_size。log_{N+1}.log 繼續寫入。只有首次呼叫 log() 時才 fopen,若程式執行期間完全未寫日誌,則不佔用檔案描述符。
使用 match 表達式(PHP 8.0+)對應級別前綴,易於擴充(如加入 NOTICE、CRITICAL)。
close() 公開方法。__destruct() 自動關閉。register_shutdown_function 確保即使發生致命錯誤也能釋放資源。$max_files 參數,超過時刪除最舊檔案。本文實作的 Log 類別展示了單例模式在資源管理上的優勢,結合自動分檔、多級別、延遲開啟與安全關閉,滿足中小型專案的核心日誌需求。若專案規模增大,建議遷移至成熟函式庫(如 Monolog),獲得更豐富的 Handler、Formatter、Processor 生態系統。