| 類型 | 基類 | 可捕獲 | 典型場景 |
E_ERROR | Error | 否 (致命) | 語法錯誤、記憶體耗盡、調用不存在方法 |
E_WARNING | - | 是 (錯誤處理器) | 檔案不存在、除零、類型不匹配 |
E_NOTICE | - | 是 | 未定義變數、陣列鍵不存在 |
E_DEPRECATED | - | 是 | 棄用功能使用 |
Exception | Throwable | 是 (try-catch) | 業務邏輯拋出、庫拋出 |
Error | Throwable | 是 (try-catch) | TypeError、ArgumentCountError、ParseError |
統一處理器架構
<?php
declare(strict_types=1);
// 1. 配置:區分環境
$isDev = (getenv('APP_ENV') ?? 'prod') === 'dev';
error_reporting($isDev ? E_ALL : E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
ini_set('display_errors', $isDev ? '1' : '0');
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/logs/php_errors.log');
// 2. 統一日誌格式化器
function formatLogEntry(string $level, string $message, string $file, int $line, array $context = []): string {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5);
$caller = $trace[1] ?? [];
return json_encode([
'time' => date('c'),
'level' => $level,
'message' => $message,
'file' => $file,
'line' => $line,
'caller' => $caller['function'] ?? 'unknown',
'class' => $caller['class'] ?? '',
'context' => $context,
'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid('req_'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
}
// 3. 錯誤處理器 (捕獲 Warning, Notice, Deprecated 等)
set_error_handler(function(int $errno, string $errstr, string $errfile, int $errline): bool {
if (!(error_reporting() & $errno)) return false; // 未在報告範圍內
$level = match ($errno) {
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => 'ERROR',
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => 'WARNING',
E_NOTICE, E_USER_NOTICE => 'NOTICE',
E_DEPRECATED, E_USER_DEPRECATED => 'DEPRECATED',
default => 'UNKNOWN',
};
$log = formatLogEntry($level, $errstr, $errfile, $errline);
file_put_contents(ini_get('error_log'), $log, FILE_APPEND | LOCK_EX);
if ($isDev) {
// 開發環境直接輸出 (避免 headers_sent 問題用 CLI 風格)
echo "[$level] $errstr in $errfile:$errline
";
}
return true; // 阻止內建處理器
});
// 4. 異常處理器 (捕獲未捕獲的 Exception/Error)
set_exception_handler(function(Throwable $e): void {
$log = formatLogEntry('EXCEPTION', $e->getMessage(), $e->getFile(), $e->getLine(), [
'code' => $e->getCode(),
'trace' => $e->getTraceAsString(),
'previous'=> $e->getPrevious()?->getMessage(),
]);
file_put_contents(ini_get('error_log'), $log, FILE_APPEND | LOCK_EX);
http_response_code(500);
if ($isDev) {
echo "Uncaught Exception
";
echo "" . htmlspecialchars($e->__toString()) . "
";
} else {
echo "服務暫時不可用
我們已記錄錯誤,請稍後重試。
";
}
});
// 5. 關閉函數 (捕獲致命錯誤 E_ERROR, E_PARSE 等)
register_shutdown_function(function(): void {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true)) {
// 避免重複記錄 (若 error_handler 已處理)
$log = formatLogEntry('FATAL', $error['message'], $error['file'], $error['line']);
file_put_contents(ini_get('error_log'), $log, FILE_APPEND | LOCK_EX);
if (!$isDev) {
// 生產環境輸出友好頁面 (注意:此時可能已有輸出)
echo "系統錯誤
請聯繫管理員。
";
}
}
});
// --- 業務代碼示範 ---
class UserService {
public function getProfile(int $id): array {
if ($id <= 0) throw new InvalidArgumentException('Invalid user ID');
// 模擬資料庫錯誤
if ($id === 999) trigger_error('DB connection failed', E_USER_WARNING);
return ['id' => $id, 'name' => 'User ' . $id];
}
}
try {
$svc = new UserService();
echo json_encode($svc->getProfile(1));
// 故意觸發錯誤
$svc->getProfile(-1);
} catch (Throwable $e) {
// 业务层捕获并处理
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
?>
關鍵協作機制圖解
請求進入
│
├─ 非致命錯誤 (Warning, Notice...) ──→ set_error_handler ──→ 記錄日誌 ──→ 返回 true (阻止內建)
│
├─ 拋出異常 (throw) ──→ try-catch 捕獲 ──→ 業務處理
│ │
│ └─ 未捕獲 ──→ set_exception_handler ──→ 記錄日誌 ──→ 返回 500 頁面
│
└─ 致命錯誤 (E_ERROR, Parse Error...) ──→ 腳本中止
│
└─ register_shutdown_function ──→ error_get_last() ──→ 記錄日誌 ──→ 友好頁面
生產環境最佳實踐清單
- 結構化日誌:JSON 格式,含
request_id、trace_id (分散式追蹤)、環境標籤,便於 ELK/Splunk/Datadog 接入。
- 錯誤碼規範:定義業務錯誤碼 (如
1001: USER_NOT_FOUND),前端據此顯示國際化提示。
- 敏感資訊過濾:日誌中脫敏密碼、Token、信用卡號、PII (個人身份信息)。
- 異步寫入:高併發下
file_put_contents(LOCK_EX) 會阻塞。改用 Monolog + Redis/Redis Handler 或 Kafka 非同步寫入。
- 錯誤追蹤平台:接入 Sentry、Rollbar、Bugsnag,自動聚合、去重、告警、關聯版本發布。
- 優雅降級:非核心功能 (推薦、日誌、統計) 失敗不阻塞主流程,用
try-catch 靜默吞掉並記錄。
常用工具庫推薦
- Monolog:PSR-3 標準日誌庫,支援無數 Handler (文件、Redis、Slack、Sentry、Elasticsearch)。
- Whoops:開發環境絕美錯誤頁面,支援代碼片段、變數檢查、AJAX/CLI 友好輸出。
- Symfony ErrorHandler:生產級錯誤渲染、HTTP 狀態碼映射、調試工具欄整合。
Monolog 快速整合範例
composer require monolog/monolog
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Formatter\JsonFormatter;
$log = new Logger('app');
$log->pushHandler(new StreamHandler(__DIR__.'/logs/app.log', Logger::DEBUG));
$log->pushHandler(new SlackWebhookHandler('https://hooks.slack.com/...', Logger::ERROR));
$log->info('User login', ['user_id' => 123, 'ip' => $_SERVER['REMOTE_ADDR']]);
$log->error('Payment failed', ['order_id' => 'ABC', 'exception' => $e]);
?>
建立「統一捕獲、結構化日誌、分級響應、異步寫入、平台接入」的異常處理體系,是 PHP 專案從玩具級邁向企業級的關鍵基礎設施。不要等到線上事故無法定位時才後悔,從專案第一天就接入 Monolog + Sentry,養成「所有錯誤最終都在日誌系統可查」的習慣。
- 上一篇文章: PHP 獲取 Bing 每日壁紙並直接輸出圖片流
- 下一篇文章: PHP 反射機制實戰:動態獲取類/函數定義位置、依賴注入與註解解析
Copyright ©
程式碼之家 All Rights Reserved