錯誤處理演進層級
- die()/exit():最原始,停止執行,輸出信息,無法捕獲、無法恢復。
- trigger_error() + set_error_handler():自定義錯誤處理器,可轉為異常、記錄日誌、發送郵件。
- try/catch + throw Exception:現代面向對象異常體系,支援類型捕獲、堆疊追蹤、重新拋出。
- Throwable 接口 (PHP 7+):統一 Error 和 Exception,\Throwable 可捕獲所有可拋出對象。
錯誤級別常量速查
| 常量 | 值 | 描述 | 可被 set_error_handler 捕獲 |
| E_ERROR | 1 | 致命運行時錯誤(內存耗盡) | 否 |
| E_WARNING | 2 | 運行時警告(非致命) | 是 |
| E_PARSE | 4 | 編譯時語法錯誤 | 否 |
| E_NOTICE | 8 | 運行時通知(如未定義變量) | 是 |
| E_DEPRECATED | 8192 | 棄用特性警告 | 是 |
| E_USER_ERROR | 256 | 用戶觸發致命錯誤 | 否 |
| E_USER_WARNING | 512 | 用戶觸發警告 | 是 |
| E_USER_NOTICE | 1024 | 用戶觸發通知 | 是 |
| E_RECOVERABLE_ERROR | 4096 | 可捕獲致命錯誤(類型提示失敗) | 是 |
| E_ALL | 32767 | 除 E_STRICT 外所有 | - |
自定義錯誤處理器最佳實踐
<?php
// bootstrap.php 或入口文件最早引入
function appErrorHandler(int $level, string $message, string $file, int $line, array $context = []): bool {
// 1. 過濾 error_reporting 不包含的級別
if (!(error_reporting() & $level)) return false; // 交給 PHP 內置處理
// 2. 忽略 @ 抑制的錯誤
if (error_reporting() === 0) return false;
// 3. 構建統一日誌格式
$log = [
'time' => date('c'),
'level' => errorLevelToString($level),
'message' => $message,
'file' => $file,
'line' => $line,
'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5),
];
// 4. 記錄日誌(Monolog/文件/系統日誌)
error_log(json_encode($log, JSON_UNESCAPED_UNICODE), 3, __DIR__ . '/logs/php_errors.log');
// 5. 生產環境:致命錯誤發送告警(郵件/Slack/釘釘)
if (in_array($level, [E_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR], true)) {
// 異步發送告警...
}
// 6. 返回 true 表示已處理,阻止 PHP 內置輸出
return true;
}
set_error_handler('appErrorHandler');
// 將錯誤轉為異常(便於 try/catch 統一處理)
function errorToException(int $level, string $message, string $file, int $line): void {
if (!(error_reporting() & $level)) return;
throw new ErrorException($message, 0, $level, $file, $line);
}
set_error_handler('errorToException', E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
異常處理現代模式
<?php
// 1. 定義業務異常層級
namespace App\Exception;
class BusinessException extends \Exception {
public function __construct(string $message, int $code = 400, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
class NotFoundException extends BusinessException {
public function __construct(string $message = '資源不存在', \Throwable $previous = null) {
parent::__construct($message, 404, $previous);
}
}
class ValidationException extends BusinessException {
public array $errors;
public function __construct(array $errors, \Throwable $previous = null) {
$this->errors = $errors;
parent::__construct('驗證失敗', 422, $previous);
}
}
// 2. 業務代碼拋出
function transfer(int $from, int $to, float $amount): void {
if ($amount <= 0) throw new ValidationException(['amount' => '金額必須大於 0']);
$fromUser = User::find($from);
if (!$fromUser) throw new NotFoundException("轉出用戶 {$from} 不存在");
if ($fromUser->balance < $amount) throw new BusinessException('餘額不足', 400);
// ... 執行轉賬
}
// 3. 統一異常處理入口(框架通常提供)
try {
// 路由分發、控制器執行
$response = $app->handle($request);
} catch (ValidationException $e) {
$response = jsonResponse(['code' => $e->getCode(), 'msg' => $e->getMessage(), 'errors' => $e->errors], $e->getCode());
} catch (NotFoundException $e) {
$response = jsonResponse(['code' => 404, 'msg' => $e->getMessage()], 404);
} catch (BusinessException $e) {
$response = jsonResponse(['code' => $e->getCode(), 'msg' => $e->getMessage()], $e->getCode());
} catch (\Throwable $e) {
// 記錄完整堆疊
Log::critical($e->getMessage(), ['trace' => $e->getTraceAsString()]);
// 生產環境不暴露內部細節
$response = jsonResponse(['code' => 500, 'msg' => '服務器內部錯誤'], 500);
}
// 4. 註冊 shutdown 捕獲致命錯誤
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
// 記錄致命錯誤
Log::emergency('Fatal Error', $error);
// 可選:輸出友好頁面
if (php_sapi_name() !== 'cli') {
http_response_code(500);
echo '系統繁忙,請稍後重試';
}
}
});
日誌記錄最佳實踐
- 使用 Monolog 實現 PSR-3 標準,支援多通道(文件、Syslog、Slack、Elasticsearch)。
- 日誌級別:
debug < info < notice < warning < error < critical < alert < emergency。
- 結構化日誌(JSON),包含:
request_id、user_id、trace_id、span_id,便於鏈路追蹤。
- 敏感數據脫敏(密碼、Token、身份證、銀行卡)。
- 日誌輪轉:按天/大小切割,保留 30 天,壓縮歸檔。
生產環境配置清單
; php.ini
display_errors = Off ; 絕不向用戶顯示錯誤
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/error.log
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
; 生產環境建議關閉嚴格標準提示,減少噪音