PHP 5.3 引入**嵌套異常**(Previous Exception)與 **SPL 內置 13 種具體異常類型**,為構建清晰、可捕獲、可擴展的異常體系奠定基礎。本文深度解析嵌套機制、SPL 異常分類、組件級異常設計模式,並給出 Zend Framework / Symfony / Laravel 風格的最佳實踐代碼。
場景:適配器模式封裝第三方庫(PDO、Guzzle、AWS SDK),將其異常轉換為統一業務異常,**同時保留原始異常鏈**供深度排查。
<?php
class DatabaseAdapter {
private $pdo;
public function __construct(PDO $pdo) { $this->pdo = $pdo; }
public function execute(string $sql, array $params = []) {
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
} catch (PDOException $e) {
// 拋出業務異常,攜帶原始異常
throw new DatabaseException(
"SQL 執行失敗: {$sql}",
$e->getCode(), // 保留原錯誤碼
$e // 關鍵:Previous Exception
);
}
}
}
// 調用方捕獲
try {
$db->execute('SELECT * FROM users WHERE id = ?', [$id]);
} catch (DatabaseException $e) {
echo "業務錯誤: {$e->getMessage()}\n";
echo "SQLSTATE: {$e->getCode()}\n";
// 訪問原始 PDOException
$prev = $e->getPrevious();
if ($prev) {
echo "原始驅動錯誤: {$prev->getMessage()}\n";
echo "驅動錯誤碼: {$prev->getCode()}\n";
echo "堆疊跟蹤:\n{$prev->getTraceAsString()}\n";
}
}
?>
throw new MyException($msg, $code, $previous):構造函數第 3 參數接收 Throwable。$e->getPrevious():返回前一個異常對象或 null。A → B → C,可遞歸 getPrevious() 遍歷完整鏈。所有 SPL 異常繼承自 LogicException(邏輯錯誤,編碼時可預防)或 RuntimeException(運行時錯誤,依賴外部狀態)。
| 分類 | 異常類 | 繼承自 | 典型使用場景 |
|---|---|---|---|
| 基類 | LogicException | Exception | 代碼邏輯錯誤(參數無效、狀態不符) |
| RuntimeException | Exception | 運行時環境錯誤(IO、網絡、數據庫) | |
| 動態調用組 | BadFunctionCallException | LogicException | call_user_func() 目標不可調用、閉包未定義 |
| BadMethodCallException | BadFunctionCallException | __call() 未實現的方法、動態代理缺失方法 | |
| 邏輯組 | InvalidArgumentException | LogicException | 函數/方法參數類型、值、範圍不合法 |
| DomainException | LogicException | 值不符合業務領域約束(如枚舉值、狀態機非法轉換) | |
| LengthException | LogicException | 長度超出限制(字符串、數組、文件大小) | |
| OutOfRangeException | LogicException | 索引超出合法範圍(數組下標、分頁頁碼) | |
| 運行時組 | OutOfBoundsException | RuntimeException | 運行時訪問越界(流指鉤、結果集游標) |
| OverflowException | RuntimeException | 數值溢出(大整數、緩衝區寫入) | |
| RangeException | RuntimeException | 運行時值超出預期範圍(配置值、外部 API 返回) | |
| UnderflowException | RuntimeException | 數值下溢(遞減超出最小值) | |
| UnexpectedValueException | RuntimeException | 運行時遇到意外值(解析結果、反序列化、配置項) |
是否為代碼邏輯/參數錯誤(開發時可發現)?
├─ 是 → LogicException 子類
│ ├─ 參數無效 → InvalidArgumentException
│ ├─ 業務規則違反 → DomainException
│ ├─ 長度超限 → LengthException
│ └─ 索引越界 → OutOfRangeException
└─ 否 → RuntimeException 子類
├─ 訪問越界 → OutOfBoundsException
├─ 數值溢出 → OverflowException
├─ 值超出範圍 → RangeException
├─ 數值下溢 → UnderflowException
└─ 意外值 → UnexpectedValueException
目標:使組件內所有異常**同時具備**:
<?php
namespace Acme\Payment;
use InvalidArgumentException;
use RuntimeException;
use UnexpectedValueException;
// 1. Marker Interface(無任何方法)
interface PaymentException {}
// 2. 具體異常:繼承 SPL + 實現 Marker
class InvalidAmountException extends InvalidArgumentException implements PaymentException {}
class GatewayTimeoutException extends RuntimeException implements PaymentException {}
class InvalidResponseException extends UnexpectedValueException implements PaymentException {}
// 3. 組件代碼
class PaymentGateway {
public function charge(float $amount, string $currency): ChargeResult {
if ($amount <= 0) {
throw new InvalidAmountException("金額必須大於 0");
}
// ... 調用第三方網關
$response = $this->httpClient->post('/charge', compact('amount', 'currency'));
if ($response->getStatusCode() !== 200) {
throw new GatewayTimeoutException("網關超時", $response->getStatusCode());
}
$data = json_decode($response->getBody(), true);
if (!isset($data['transaction_id'])) {
throw new InvalidResponseException("響應缺少 transaction_id");
}
return new ChargeResult($data['transaction_id']);
}
}
// 4. 上層調用:多級捕獲
try {
$gateway->charge(100, 'CNY');
} catch (InvalidAmountException $e) {
// 極精確:參數錯誤,提示用戶修正
log_warning("參數錯誤: {$e->getMessage()}");
show_user_error("請輸入正確金額");
} catch (PaymentException $e) {
// 組件級:所有支付異常統一記錄、告警、返回通用錯誤頁
log_error("支付異常: {$e->getMessage()}", ['exception' => $e]);
alert_oncall("支付網關異常");
show_user_error("支付暫時不可用,請稍後重試");
} catch (Throwable $e) {
// 頂層保護:未知錯誤
log_critical("未知系統錯誤", ['exception' => $e]);
show_500();
}
| 捕獲類型 | 捕獲範圍 | 使用場景 |
|---|---|---|
| InvalidAmountException | 單一具體錯誤 | 精確業務處理、用戶提示 |
| PaymentException (Interface) | 支付組件所有異常 | 組件邊界統一處理、日志標記、告警路由 |
| LogicException | 所有邏輯錯誤 | 框架級參數驗證失敗統一處理 |
| RuntimeException | 所有運行時錯誤 | 框架級外部依賴失敗統一處理 |
| Throwable | 所有錯誤+異常 | 頂層保護、防止白屏 |
| 框架 | 基類 | 特色 |
|---|---|---|
| Laravel | Illuminate\Foundation\Exceptions\Handler | 渲染器模式、自動報告、驗證異常 ValidationException |
| Symfony | Symfony\Component\HttpKernel\Exception\HttpException | HTTP 狀態碼映射、ExceptionListener、DebugBundle |
| Zend/Laminas | Laminas\Mvc\Exception\ExceptionInterface | Marker Interface 模式、Module 級異常 |
| ThinkPHP | think\Exception | 輕量、支持錯誤碼、模板渲染 |
$previous。getMessage() 給開發者看,用戶提示走單獨渲染層(i18n、錯誤碼映射)。set_exception_handler / 框架 Handler 統一記錄、上報、友好頁面。expectException()、expectExceptionMessage()、expectExceptionCode() 覆蓋所有拋出路徑。構建規範的異常體系不是「造輪子」,而是**降低認知負荷、提升排查效率、實現精準錯誤治理**的基礎設施投資。從今天開始,為每個組件定義 Marker Interface,為每個 throw 選擇最精確的 SPL 異常。