PHP 例外處理 (Exception)

例外處理是現代 PHP 中處理錯誤的標準方式。當程式執行過程中發生問題(例如檔案不存在、資料庫連線失敗、輸入驗證錯誤等),可以「拋出」一個例外,然後在適當的地方「捕獲」並處理它。這種機制讓錯誤處理程式碼與正常邏輯分離,使程式碼更清晰、更易維護。

try-catch 基本用法

try 區塊包含可能發生錯誤的程式碼,catch 區塊負責處理捕獲到的例外:

<?php
try {
    // 可能發生錯誤的程式碼
    $file = file_get_contents('nonexistent.txt');
    if ($file === false) {
        throw new Exception('檔案讀取失敗');
    }
} catch (Exception $e) {
    echo "錯誤:" . $e->getMessage();
}
?>

Exception 物件

Exception 物件包含例外的詳細資訊,可以透過以下方法取得:

<?php
try {
    throw new Exception('發生錯誤', 100);
} catch (Exception $e) {
    echo $e->getMessage();    // 發生錯誤
    echo $e->getCode();       // 100
    echo $e->getFile();       // 檔案路徑
    echo $e->getLine();       // 行號
    echo $e->getTraceAsString();  // 堆疊追蹤
}
?>

finally 區塊

finally 區塊中的程式碼無論是否發生例外都會執行,通常用於清理資源(如關閉檔案、釋放連線等):

<?php
$handle = null;

try {
    $handle = fopen('data.txt', 'r');
    // 處理檔案...
} catch (Exception $e) {
    echo "錯誤:" . $e->getMessage();
} finally {
    // 無論是否發生例外都會執行
    if ($handle) {
        fclose($handle);
    }
}
?>

多重 catch

可以使用多個 catch 區塊來處理不同類型的例外。PHP 會依序檢查,第一個符合的 catch 會被執行:

<?php
try {
    // ...
} catch (InvalidArgumentException $e) {
    echo "參數錯誤:" . $e->getMessage();
} catch (RuntimeException $e) {
    echo "執行錯誤:" . $e->getMessage();
} catch (Exception $e) {
    echo "其他錯誤:" . $e->getMessage();
}

// PHP 7.1+ 可以合併
try {
    // ...
} catch (InvalidArgumentException | RuntimeException $e) {
    echo "錯誤:" . $e->getMessage();
}
?>

自訂例外

你可以建立繼承自 Exception 的自訂例外類別,來封裝特定的錯誤資訊和邏輯:

<?php
class ValidationException extends Exception {
    private array $errors;
    
    public function __construct(array $errors) {
        parent::__construct('驗證失敗');
        $this->errors = $errors;
    }
    
    public function getErrors(): array {
        return $this->errors;
    }
}

class DatabaseException extends Exception {
    public function __construct(string $message, ?\Throwable $previous = null) {
        parent::__construct($message, 0, $previous);
    }
}

// 使用
try {
    $errors = ['email' => 'Email 格式不正確'];
    throw new ValidationException($errors);
} catch (ValidationException $e) {
    foreach ($e->getErrors() as $field => $message) {
        echo "$field: $message\n";
    }
}
?>

重新拋出例外

有時你需要捕獲例外、進行一些處理(如記錄日誌),然後重新拋出讓上層程式碼處理。也可以用自訂例外包裝原始例外:

<?php
function processData($data) {
    try {
        // 處理資料
    } catch (Exception $e) {
        // 記錄後重新拋出
        error_log($e->getMessage());
        throw $e;
    }
}

function processWithWrapper($data) {
    try {
        // 處理資料
    } catch (PDOException $e) {
        // 包裝成自訂例外
        throw new DatabaseException('資料庫操作失敗', $e);
    }
}
?>

全域例外處理

使用 set_exception_handler() 可以設定一個函數來處理所有未被捕獲的例外,這是最後的防線:

<?php
set_exception_handler(function (Throwable $e) {
    error_log($e->getMessage());
    http_response_code(500);
    echo "系統發生錯誤";
});

// 未捕獲的例外會被這個處理器處理
throw new Exception('未捕獲的例外');
?>

SPL 例外類別

PHP 提供多種內建例外:

<?php
// 邏輯相關
throw new LogicException('邏輯錯誤');
throw new InvalidArgumentException('參數無效');
throw new OutOfRangeException('超出範圍');

// 執行時相關
throw new RuntimeException('執行時錯誤');
throw new UnexpectedValueException('非預期的值');
throw new OutOfBoundsException('索引超出邊界');
?>

實際應用

<?php
class UserService {
    public function __construct(private PDO $db) {}
    
    public function createUser(array $data): int {
        $this->validate($data);
        
        try {
            $stmt = $this->db->prepare(
                "INSERT INTO users (name, email) VALUES (?, ?)"
            );
            $stmt->execute([$data['name'], $data['email']]);
            return (int) $this->db->lastInsertId();
        } catch (PDOException $e) {
            if ($e->getCode() === '23000') {
                throw new ValidationException(['email' => 'Email 已被使用']);
            }
            throw new DatabaseException('建立使用者失敗', $e);
        }
    }
    
    private function validate(array $data): void {
        $errors = [];
        
        if (empty($data['name'])) {
            $errors['name'] = '名稱為必填';
        }
        
        if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            $errors['email'] = 'Email 格式不正確';
        }
        
        if (!empty($errors)) {
            throw new ValidationException($errors);
        }
    }
}
?>