PHP 7+ 重寫了錯誤處理引擎,引入 Throwable 接口統一 Error 和 Exception 體系。理解兩者區別、轉換機制、捕獲策略,是編寫健壯 PHP 應用的基石。本文從類層級、語義、實戰場景三個維度深度解析。
Throwable (interface)
├── Error
│ ├── ArithmeticError
│ │ └── DivisionByZeroError
│ ├── AssertionError
│ ├── CompileError
│ │ └── ParseError
│ ├── TypeError
│ │ ├── ArgumentCountError
│ │ └── ValueError
│ └── UnhandledMatchError
└── Exception
├── LogicException
│ ├── BadFunctionCallException
│ ├── BadMethodCallException
│ ├── DomainException
│ ├── InvalidArgumentException
│ ├── LengthException
│ └── OutOfRangeException
├── RuntimeException
│ ├── OutOfBoundsException
│ ├── OverflowException
│ ├── RangeException
│ ├── UnderflowException
│ └── UnexpectedValueException
└── \Php\Exception (PHP 8.3+ 內部預留)
throw |
| **繼承根** | Error | Exception |
| **捕獲方式** | catch (Error $e) | catch (Exception $e) |
| **統一捕獲** | catch (Throwable $e) (PHP 7+) | 同上 |
| **錯誤級別** | E_ERROR, E_PARSE, E_TYPE_ERROR 等 | 無級別概念,由代碼控制 |
| **性能影響** | 較大(引擎級回溯) | 較小(用戶態拋出) |
throw new InvalidArgumentException('Email 格式無效')throw new DomainException('用戶不存在')throw new LogicException('庫存不足')throw new RuntimeException('支付網關超時')TypeError)Error)DivisionByZeroError)Error,無法捕獲恢復)PHP 7+ 可通過 set_error_handler 將傳統錯誤 (E_WARNING, E_NOTICE 等) 轉為 ErrorException:
<?php
function errorToException(int $severity, string $message, string $file, int $line): void
{
// 忽略錯誤抑制符 @
if (error_reporting() === 0) return;
$errorTypes = [
E_WARNING => 'Warning',
E_NOTICE => 'Notice',
E_USER_WARNING => 'UserWarning',
E_USER_NOTICE => 'UserNotice',
E_STRICT => 'Strict',
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'UserDeprecated',
];
$type = $errorTypes[$severity] ?? 'Error';
throw new ErrorException($message, 0, $severity, $file, $line);
}
// 註冊(建議在 bootstrap 階段)
set_error_handler('errorToException', E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
// 現在所有錯誤都可用 try-catch 捕獲
try {
$content = file_get_contents('/not/exist'); // 觸發 Warning → ErrorException
} catch (ErrorException $e) {
// 統一處理
logError($e);
}
<?php
namespace App\Domain\Exception;
class UserNotFoundException extends DomainException {}
class InsufficientBalanceException extends LogicException {}
class InvalidCouponException extends DomainException {}
<?php
namespace App\Infrastructure\Exception;
class DatabaseConnectionException extends RuntimeException {}
class CacheMissException extends RuntimeException {}
class ExternalApiException extends RuntimeException {}
<?php
class ExceptionHandler
{
public function handle(Throwable $e): Response
{
return match (true) {
$e instanceof UserNotFoundException => new JsonResponse(['error' => $e->getMessage()], 404),
$e instanceof InsufficientBalanceException => new JsonResponse(['error' => $e->getMessage()], 400),
$e instanceof InvalidArgumentException => new JsonResponse(['error' => $e->getMessage()], 422),
$e instanceof DatabaseConnectionException => $this->serverError($e),
$e instanceof ExternalApiException => new JsonResponse(['error' => '服務暫時不可用'], 503),
default => $this->serverError($e),
};
}
private function serverError(Throwable $e): Response
{
// 記錄完整堆疊到日誌,不暴露給用戶
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
return new JsonResponse(['error' => '內部服務器錯誤'], 500);
}
}
<?php
function transfer($from, $to, $amount) {
$user = getUser($from);
if (!$user) return ['code' => 404, 'msg' => '用戶不存在'];
if ($user->balance < $amount) return ['code' => 400, 'msg' => '餘額不足'];
$target = getUser($to);
if (!$target) return ['code' => 404, 'msg' => '目標用戶不存在'];
$result = deductBalance($from, $amount);
if (!$result) return ['code' => 500, 'msg' => '扣款失敗'];
$result = addBalance($to, $amount);
if (!$result) {
// 回滾?代碼極其複雜
return ['code' => 500, 'msg' => '入賬失敗'];
}
return ['code' => 200, 'msg' => '成功'];
}
<?php
function transfer(string $from, string $to, float $amount): void
{
DB::transaction(function () use ($from, $to, $amount) {
$payer = User::findOrFail($from); // 拋出 ModelNotFoundException → 404
$payee = User::findOrFail($to); // 同上
if ($payer->balance < $amount) {
throw new InsufficientBalanceException('餘額不足');
}
$payer->decrement('balance', $amount);
$payee->increment('balance', $amount);
// 記錄流水...
TransferLog::create([...]);
});
// 事務自動提交或回滾,無需手動判斷
}
ExceptionExceptionHandler 映射 HTTP 響應catch { } 空捕獲是大忌,至少記錄日誌finally 中釋放TypeError 等通常意味著代碼 Bug,應修復而非捕獲Error 是引擎發出的「紅燈」,多為代碼缺陷,應在開發測試階段消滅;Exception 是業務發出的「黃燈」,預期內的偏離,應被捕獲、記錄、轉化為友好響應。PHP 7+ 的 Throwable 統一接口,配合分層異常體系與統一處理器,讓錯誤處理從「到處是 if-else」進化為「清晰的契約驅動設計」。