「線上關閉錯誤顯示」是常見最佳實踐,但若同時缺乏錯誤記錄與告警,系統就成了黑盒:隨機失敗無法複現、付款異常無跡可尋、長時間腳本靜默中斷。本文構建一套完整的 PHP 錯誤攔截器,涵蓋未捕獲異常、所有錯誤等級、致命錯誤三大類,實現「不顯示用戶、全記錄日誌、嚴重即時告警」的生產級目標。
| 攔截器 | 覆蓋範圍 | 關鍵特性 |
|---|---|---|
set_exception_handler | 未被 try-catch 捕獲的 Exception/Throwable | 最後防線,必須極度健壯 |
set_error_handler | E_ERROR 以外的所有錯誤(Warning, Notice, Deprecated 等) | 可轉換為異常統一處理 |
register_shutdown_function | 腳本結束時檢測 error_get_last() | 唯一能捕獲 E_ERROR/E_PARSE 等致命錯誤的方式 |
<?php
declare(strict_types=1);
class ErrorHandler {
private array $argv = [];
private int $memoryReserveSize = 262144; // 256KB 備用記憶體
private string $memoryReserve = '';
public function register(): void {
// 1. 關閉錯誤顯示
ini_set('display_errors', '0');
ini_set('error_reporting', '-1'); // 捕獲所有等級
// 2. 註冊攔截器(順序關鍵!)
set_exception_handler([$this, 'handleException']);
set_error_handler([$this, 'handleError']);
// 3. 預留記憶體,防致命錯誤時 OOM 導致處理邏輯無法執行
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 $e): void {
$this->unregister(); // 防止遞迴
try {
$this->logException($e);
} catch (Throwable) {
// 日誌寫入失敗也不應拋出新異常
}
exit(1);
}
// 錯誤轉異常統一處理
public function handleError(int $code, string $msg, string $file, int $line): bool {
// 僅處理受 error_reporting 影響且非輕微錯誤的類型
if (($code & error_reporting()) && !$this->isMinorError($code)) {
$exception = new ErrorException($msg, 0, $code, $file, $line);
// __toString 內發生錯誤不可拋異常,直接處理
if ($this->isInToString()) {
$this->handleException($exception);
exit(1);
}
throw $exception; // 交給 handleException
}
return false; // 交給內建處理器
}
// 致命錯誤捕獲
public function handleFatalError(): void {
$this->memoryReserve = ''; // 釋放備用記憶體
$error = error_get_last();
if ($error && $this->isFatalError($error['type'])) {
$exception = new ErrorException(
$error['message'], 0, $error['type'], $error['file'], $error['line']
);
$this->logException($exception);
exit(1);
}
}
// 異常日誌記錄(需子類實作具體存儲)
protected function logException(Throwable $e): void {
$data = [
'time' => time(),
'title' => $this->getErrorTitle($e->getCode()),
'message' => $this->formatExceptionChain($e),
'server_ip' => $this->getServerIp(),
'code' => $this->getLocalCode($e->getCode()),
'file' => $e->getFile(),
'line' => $e->getLine(),
'url' => $this->getCurrentUri(),
];
$this->persistLog($data); // 實作:寫資料庫/檔案/發送到 ELK/Sentry
}
// ... 輔助方法:getServerIp, getCurrentUri, getErrorTitle, isFatalError, persistLog 等
}
class ErrorCodeMap {
public const MAP = [
E_ERROR => ['code' => 4006, 'name' => 'PHP Fatal Error'],
E_WARNING => ['code' => 4015, 'name' => 'PHP Warning'],
E_PARSE => ['code' => 4008, 'name' => 'PHP Parse Error'],
E_NOTICE => ['code' => 4007, 'name' => 'PHP Notice'],
E_CORE_ERROR => ['code' => 4003, 'name' => 'PHP Core Error'],
E_CORE_WARNING => ['code' => 4004, 'name' => 'PHP Core Warning'],
E_COMPILE_ERROR => ['code' => 4001, 'name' => 'PHP Compile Error'],
E_COMPILE_WARNING => ['code' => 4002, 'name' => 'PHP Compile Warning'],
E_USER_ERROR => ['code' => 4012, 'name' => 'PHP User Error'],
E_USER_WARNING => ['code' => 4014, 'name' => 'PHP User Warning'],
E_USER_NOTICE => ['code' => 4013, 'name' => 'PHP User Notice'],
E_STRICT => ['code' => 4010, 'name' => 'PHP Strict Warning'],
E_RECOVERABLE_ERROR => ['code' => 4009, 'name' => 'PHP Recoverable Error'],
E_DEPRECATED => ['code' => 4005, 'name' => 'PHP Deprecated Warning'],
E_USER_DEPRECATED => ['code' => 4011, 'name' => 'PHP User Deprecated Warning'],
];
public static function getLocalCode(int $code): int {
return self::MAP[$code]['code'] ?? 4016;
}
public static function getName(int $code): string {
return self::MAP[$code]['name'] ?? 'Customer Error';
}
public static function isFatal(int $type): bool {
return in_array($type, [
E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING,
E_COMPILE_ERROR, E_COMPILE_WARNING
], true);
}
}
registerErrorHandler.php<?php
// 定義調試模式(建議透過環境變數控制)
define('DEBUG_ERROR', (bool)getenv('APP_DEBUG')); // 生產環境為 false
require_once __DIR__ . '/ErrorHandler.php';
if (DEBUG_ERROR) {
ini_set('display_errors', '1');
} else {
ini_set('display_errors', '0');
ini_set('error_reporting', '-1');
(new ErrorHandler())->register();
}
md5(code:file:line:message) 為鍵,Redis 設定 1 小時 TTL,避免錯誤風暴刷爆日誌$_SERVER、$_POST(脫敏)、堆疊追蹤、Git Commit SHA、部署版本號set_error_handler 無法捕獲:E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING → 這些屬於編譯/啟動階段錯誤,代碼根本無法執行到註冊行,屬正常行為auto_prepend_file 或框架引導第一行)$argv 變數獲取請求上下文<?php
require 'registerErrorHandler.php';
// 測試 1: 未捕獲異常
// throw new RuntimeException('Test Exception');
// 測試 2: 觸發 Warning
// trigger_error('Test Warning', E_USER_WARNING);
// 測試 3: 致命錯誤(需獨立腳本)
// require 'non_existent_file.php'; // E_COMPILE_ERROR
// eval('invalid code'); // E_PARSE
echo "Handler registered. Check logs for test entries.";
這套機制已在多個高併發專案穩定運行 3 年以上。核心價值在於:將「不可見的隨機失敗」轉化為「可查詢、可告警、可追蹤的結構化資料」。接入成本極低(單一 require),卻能為團隊賺回無數個排查夜晚的睡眠時間。建議配合 Sentry、SkyWalking 等 APM 工具形成「代碼級追蹤 + 業務級告警」雙層防禦體系。