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

PHP try-catch 異常處理完整指南:從基礎語法到企業級最佳實踐

     編輯:PHP 相關知識

前言

PHP 5 引入異常機制,PHP 7/8 進一步強化型別系統與錯誤層級。掌握 try-catch 不僅能優雅處理錯誤,更是建構健壯、可維護應用的關鍵。本文涵蓋語法基礎、異常層級、自訂異常、全域處理、測試策略與現代最佳實踐。

基礎語法

<?php
try {
    // 可能拋出異常的程式碼
    $result = riskyOperation($input);
} catch (SpecificException $e) {
    // 處理特定異常
    handleSpecific($e);
} catch (AnotherException $e) {
    // 處理另一類異常
    handleAnother($e);
} catch (Throwable $e) {
    // 兜底:捕獲所有 Exception 與 Error (PHP 7+)
    logError($e);
    throw $e; // 可選:重新拋出讓上層處理
} finally {
    // 無論是否拋出異常都會執行(資源釋放、鎖解除、日誌完成)
    cleanup();
}

異常層級架構 (PHP 8.1+)

Throwable (介面)
├── Exception (類別)
│   ├── LogicException
│   │   ├── InvalidArgumentException
│   │   ├── LengthException
│   │   └── DomainException
│   ├── RuntimeException
│   │   ├── OutOfBoundsException
│   │   ├── OverflowException
│   │   └── UnexpectedValueException
│   └── 自訂業務異常...
└── Error (類別)
    ├── TypeError
    ├── ValueError
    ├── ArithmeticError
    └── ParseError (編譯錯誤,無法捕獲)

自訂業務異常設計

<?php
namespace App\Exceptions;

use Exception;

abstract class BusinessException extends Exception
{
    public function __construct(
        string $message = '',
        int $code = 0,
        ?Throwable $previous = null,
        public readonly array $context = [] // PHP 8.1+ readonly 屬性攜帶上下文
    ) {
        parent::__construct($message, $code, $previous);
    }
}

final class UserNotFoundException extends BusinessException {}
final class InsufficientBalanceException extends BusinessException {}
final class InvalidCouponException extends BusinessException {}

// 使用
throw new UserNotFoundException('用戶不存在', 404, null, ['user_id' => $id]);

實戰範例:服務層異常處理

<?php
namespace App\Services;

use App\Exceptions\InsufficientBalanceException;
use App\Exceptions\UserNotFoundException;
use App\Models\User;
use App\Models\Wallet;
use PDO;

class TransferService
{
    public function __construct(private PDO $db) {}

    public function transfer(int $fromUserId, int $toUserId, float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('轉帳金額必須大於 0');
        }

        $this->db->beginTransaction();
        try {
            // 鎖定雙方錢包 (SELECT ... FOR UPDATE)
            $fromWallet = $this->lockWallet($fromUserId);
            $toWallet   = $this->lockWallet($toUserId);

            if ($fromWallet['balance'] < $amount) {
                throw new InsufficientBalanceException(
                    '餘額不足',
                    400,
                    null,
                    ['user_id' => $fromUserId, 'balance' => $fromWallet['balance'], 'amount' => $amount]
                );
            }

            // 扣款/入帳
            $this->db->prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?')->execute([$amount, $fromUserId]);
            $this->db->prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?')->execute([$amount, $toUserId]);

            // 記錄流水
            $this->recordTransaction($fromUserId, -$amount, 'transfer_out', $toUserId);
            $this->recordTransaction($toUserId, $amount, 'transfer_in', $fromUserId);

            $this->db->commit();
        } catch (Throwable $e) {
            $this->db->rollBack();
            throw $e; // 重新拋出讓上層 Controller/Handler 處理
        }
    }

    private function lockWallet(int $userId): array|false
    {
        $stmt = $this->db->prepare('SELECT * FROM wallets WHERE user_id = ? FOR UPDATE');
        $stmt->execute([$userId]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: false;
    }

    private function recordTransaction(int $userId, float $amount, string $type, int $relatedId): void
    {
        $this->db->prepare('INSERT INTO transactions (user_id, amount, type, related_user_id) VALUES (?, ?, ?, ?)')
            ->execute([$userId, $amount, $type, $relatedId]);
    }
}

Controller 層統一異常回應

<?php
namespace App\Http\Controllers;

use App\Exceptions\BusinessException;
use App\Services\TransferService;
use Throwable;

class TransferController
{
    public function __construct(private TransferService $service) {}

    public function store(): void
    {
        header('Content-Type: application/json; charset=utf-8');

        try {
            $input = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
            $this->service->transfer(
                (int)($_SESSION['user_id'] ?? 0),
                (int)($input['to_user_id'] ?? 0),
                (float)($input['amount'] ?? 0)
            );
            echo json_encode(['success' => true, 'message' => '轉帳成功']);
        } catch (BusinessException $e) {
            http_response_code($e->getCode() ?: 400);
            echo json_encode([
                'success' => false,
                'error'   => $e->getMessage(),
                'code'    => $e->getCode(),
                'context' => $e->context,
            ]);
        } catch (Throwable $e) {
            // 系統級錯誤:記錄完整堆疊,回傳通用訊息
            error_log($e->getMessage() . '\n' . $e->getTraceAsString());
            http_response_code(500);
            echo json_encode(['success' => false, 'error' => '系統繁忙,請稍後再試']);
        }
    }
}

全域異常處理器

<?php
// bootstrap/exception_handler.php
set_exception_handler(function (Throwable $e) {
    // 1. 記錄完整錯誤
    error_log(
        "[{$e::class}] {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n{$e->getTraceAsString()}"
    );

    // 2. 區分環境回應
    if (($_ENV['APP_DEBUG'] ?? false) === true) {
        // 開發環境:顯示詳細錯誤頁
        header('Content-Type: text/html; charset=utf-8', true, 500);
        echo "<pre>{$e->getMessage()}\n\n{$e->getTraceAsString()}</pre>";
    } else {
        // 生產環境:友善錯誤頁
        http_response_code(500);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['error' => '伺服器內部錯誤,請稍後再試']);
    }
});

// 註冊致命錯誤處理 (PHP 7+)
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)) {
        // 模擬 Throwable 物件
        $e = new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']);
        // 重用上述邏輯...
    }
});

測試異常處理

<?php
// tests/TransferServiceTest.php (Pest/PHPUnit)
use App\Exceptions\InsufficientBalanceException;
use App\Services\TransferService;

test('餘額不足時拋出 InsufficientBalanceException', function () {
    $pdo = new PDO('sqlite::memory:');
    // 建立 schema、插入測試資料...
    $service = new TransferService($pdo);

    expect(fn() => $service->transfer(1, 2, 1000))
        ->toThrow(InsufficientBalanceException::class)
        ->and($e->context['balance'] < 1000);
});

最佳實踐清單

原則說明
具體捕獲優先 catch 具體異常 (BusinessException),最後才 catch Throwable
異常即文件自訂異常類別名稱即為業務規則文件,減少註解
攜帶上下文PHP 8.1+ readonly 屬性附帶關鍵參數,便於日誌追蹤與前端提示
finally 釋放資源鎖、檔案控制代碼、資料庫連線、暫存檔必在 finally 釋放
不要吞沒異常catch 後若無法處理,必須 throw 或記錄後轉為適當回應
區分業務/系統錯誤業務異常回 4xx + 明確訊息;系統異常記錄堆疊、回 500 + 通用訊息
單一職責Service 拋異常,Controller/Handler 負責轉換為 HTTP 回應
測試異常路徑每個拋出異常的分支都要有對應測試案例

總結

try-catch 不只是語法糖,而是領域模型與基礎設施分離的關鍵機制。善用自訂異常承載業務語意、finally 保證資源釋放、全域處理器統一錯誤回應、測試覆蓋異常路徑,能讓 PHP 應用在面對預期與非預期錯誤時,都能優雅、安全、可觀測地運行。

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