首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 異常處理完整指南:自訂異常、全域捕獲、錯誤轉異常與最佳實踐

     編輯:PHP 綜合知識

異常體系基礎

Throwable (PHP 7+)
├── Error (引擎錯誤:TypeError, ParseError, ArithmeticError...)
└── Exception (使用者代碼拋出)
    ├── LogicException (邏輯錯誤:InvalidArgument, DomainException...)
    └── RuntimeException (執行期錯誤:OutOfBounds, UnexpectedValue...)

自訂異常類別設計

<?php
// 基礎業務異常
class BusinessException extends Exception {
    protected int $code = 400; // HTTP 狀態碼
    public function __construct(string $message = '', int $code = 0, Throwable $previous = null) {
        parent::__construct($message, $code ?: $this->code, $previous);
    }
}

// 具體業務異常
class UserNotFoundException extends BusinessException { protected $code = 404; }
class InsufficientBalanceException extends BusinessException { protected $code = 402; }
class ValidationException extends BusinessException {
    private array $errors;
    public function __construct(array $errors, string $message = '驗證失敗') {
        $this->errors = $errors;
        parent::__construct($message, 422);
    }
    public function getErrors(): array { return $this->errors; }
}

// 系統層異常
class DatabaseException extends RuntimeException { protected $code = 500; }
class ExternalApiException extends RuntimeException { protected $code = 502; }

拋出與捕獲基本模式

<?php
function transfer(int $from, int $to, float $amount): void {
    $db = getDb();
    $db->beginTransaction();
    try {
        $fromUser = $db->query("SELECT * FROM users WHERE id=$for UPDATE")->fetch();
        if (!$fromUser) throw new UserNotFoundException("轉出用戶 $from 不存在");
        if ($fromUser['balance'] < $amount) throw new InsufficientBalanceException("餘額不足");

        $db->exec("UPDATE users SET balance=balance-$amount WHERE id=$from");
        $db->exec("UPDATE users SET balance=balance+$amount WHERE id=$to");
        $db->commit();
    } catch (BusinessException $e) {
        $db->rollBack();
        throw $e; // 向上拋出,讓上層決定如何回應
    } catch (Throwable $e) {
        $db->rollBack();
        throw new DatabaseException('轉帳失敗: ' . $e->getMessage(), 0, $e);
    }
}

// 控制器層統一處理
try {
    transfer(1, 2, 100.50);
    apiResponse(0, '成功');
} catch (UserNotFoundException $e) {
    apiResponse(404, $e->getMessage());
} catch (InsufficientBalanceException $e) {
    apiResponse(402, $e->getMessage());
} catch (ValidationException $e) {
    apiResponse(422, $e->getMessage(), ['errors' => $e->getErrors()]);
} catch (BusinessException $e) {
    apiResponse($e->getCode(), $e->getMessage());
} catch (Throwable $e) {
    logError($e);
    apiResponse(500, '系統錯誤');
}

全域異常處理器(捕獲未捕獲異常)

<?php
function globalExceptionHandler(Throwable $e): void {
    // 記錄完整堆疊
    logError([
        'message' => $e->getMessage(),
        'code' => $e->getCode(),
        'file' => $e->getFile(),
        'line' => $e->getLine(),
        'trace' => $e->getTraceAsString(),
        'request' => [
            'uri' => $_SERVER['REQUEST_URI'],
            'method' => $_SERVER['REQUEST_METHOD'],
            'ip' => $_SERVER['REMOTE_ADDR'] ?? 'cli',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? ''
        ]
    ]);

    // 區分環境輸出
    if (isCli()) {
        fwrite(STDERR, "FATAL: {$e->getMessage()}\n");
        exit(1);
    }

    http_response_code($e->getCode() ?: 500);
    header('Content-Type: application/json; charset=utf-8');
    if (DEBUG_MODE) {
        echo json_encode([
            'error' => get_class($e),
            'message' => $e->getMessage(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
            'trace' => explode("\n", $e->getTraceAsString())
        ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    } else {
        echo json_encode(['error' => 'Internal Server Error', 'message' => '系統暫時不可用,請稍後再試'], JSON_UNESCAPED_UNICODE);
    }
}

set_exception_handler('globalExceptionHandler');

// 將 Error 也轉為異常捕獲 (PHP 7+)
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    if (!(error_reporting() & $severity)) return false; // @ 抑制的錯誤
    throw new ErrorException($message, 0, $severity, $file, $line);
});

// 嚴重錯誤 (E_ERROR 等) 無法由 error_handler 捕獲,需 register_shutdown_function
register_shutdown_function(function () {
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        globalExceptionHandler(new ErrorException($error['message'], $error['type'], 0, $error['file'], $error['line']));
    }
});
?>

錯誤轉異常最佳實踐

場景做法
資料庫 PDO$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
檔案操作使用 SplFileObject 或封裝檢查回傳值拋異常
外部 API檢查 HTTP 碼非 2xx 拋 ExternalApiException
JSON 解析json_decode(...); if (json_last_error()) throw new ValidationException(...)

異常處理原則(SOLID 風格)

  1. 單一職責:每個異常類別代表一種具體失敗原因。
  2. 開放封閉:新增業務錯誤只需新增異常類,不修改現有捕獲邏輯(利用多型)。
  3. 里氏替換:捕獲 BusinessException 可處理所有子類。
  4. 介面隔離:ValidationException 提供 getErrors(),呼叫端依介面而非實作。
  5. 依賴倒置:高層模組依賴抽象異常介面,不依賴具體 PDOException。

常見錯誤與修正

錯誤做法問題正確做法
catch (Exception $e) {} 吞掉所有異常隱藏 Bug、難除錯精確捕獲具體異常,最後再 catch Throwable 記錄並回應 500
在 catch 中 throw $e 丟失堆疊PHP 7+ 保留堆疊,但舊版本會丟失用 throw new NewException($msg, 0, $e) 串聯 previous
業務邏輯用 die()/exit()無法被上層捕獲、測試困難拋出異常,讓控制器/處理器決定輸出
異常訊息洩露敏感資訊SQL、路徑、堆疊暴露給用戶生產環境只回應通用訊息,詳細記錄到日誌

測試異常流程

<?php
// PHPUnit 範例
public function testTransferInsufficientBalance() {
    $this->expectException(InsufficientBalanceException::class);
    $this->expectExceptionMessage('餘額不足');
    transfer(1, 2, 999999);
}
?>

建立清晰的異常體系、統一全域處理、將錯誤轉為異常、精確捕獲並記錄,讓 PHP 應用具備企業級的錯誤韌性與可維護性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved