Menu
Coddy logo textTech

カスタム例外の階層構造

CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 67/91。

PHPの組み込み例外クラスは一般的なシナリオをカバーしていますが、実際のアプリケーションでは、より具体的なエラータイプが必要になることがよくあります。カスタム例外階層を作成することで、ドメインに一致する例外を定義し、さまざまな具体性のレベルでそれらをキャッチできるようになります。

カスタム例外を作成するには、単に Exception クラスまたはそのサブクラスの1つを継承します:

<?php
class PaymentException extends Exception {}

class InsufficientFundsException extends PaymentException {
    public function __construct(
        private float $required,
        private float $available
    ) {
        parent::__construct("Need $required but only $available available");
    }
    
    public function getShortfall(): float {
        return $this->required - $this->available;
    }
}

InsufficientFundsExceptionExceptionを直接継承するのではなく、PaymentExceptionを継承している点に注目してください。これにより、1つのcatchブロックですべての支払い関連の例外をキャッチすることも、特定の例外を個別に処理することもできる階層構造が作成されます。

カスタム例外には、エラーに関連する追加のプロパティやメソッドを含めることができます。ここでは、getShortfall() が、汎用的な例外では提供できない有用な情報を提供しています:

<?php
class Wallet {
    public function __construct(private float $balance) {}
    
    public function pay(float $amount): void {
        if ($amount > $this->balance) {
            throw new InsufficientFundsException($amount, $this->balance);
        }
        $this->balance -= $amount;
    }
}

適切に設計された例外階層は、コードのメンテナンス性を向上させます。例外をキャッチする際、必要に応じて広範囲に、あるいは具体的に指定することができます。そして、例外オブジェクト自体が、何が問題だったのかについての意味のあるコンテキストを保持します。

challenge icon

チャレンジ

簡単

さまざまな種類の注文エラーを処理する、カスタム例外階層を備えた注文処理システムを構築しましょう。何が問題だったのかについて意味のあるコンテキストを持つ例外を作成し、エラーハンドリングをより正確で情報量の多いものにします。

コードは4つのファイルに分けて構成します:

  • OrderException.phpExceptionを継承したベースとなる OrderException クラスを作成します。これはすべての注文関連の例外の親として機能し、必要に応じて単一の catch ブロックで注文に関するあらゆる問題をキャッチできるようにします。
  • OutOfStockException.phpOrderException を継承した OutOfStockException クラスを作成します。この例外は、コンストラクタで商品名とリクエストされた数量を受け取る必要があります。これらのプライベートプロパティにはコンストラクタのプロパティ昇格(constructor promotion)を使用してください。親のコンストラクタを "[productName] is out of stock (requested: [quantity])" という形式のメッセージで呼び出します。商品名を返す getProductName(): string メソッドを追加してください。
  • InvalidQuantityException.phpOrderException を継承した InvalidQuantityException クラスを作成します。この例外は、コンストラクタのプロパティ昇格を使用して、プライベートプロパティとして数量の値を受け取る必要があります。親のコンストラクタを "Invalid quantity: [quantity]" というメッセージで呼び出します。数量の値を返す getQuantity(): int メソッドを追加してください。
  • main.php — 3つの例外ファイルをすべてインクルードします。商品名と数量(整数に変換する必要がある文字列として)の2つの入力を受け取ります。

    数量の値に基づいて、適切な例外をスローします:

    • 数量が1未満の場合は、InvalidQuantityException をスローします
    • 数量が100を超える場合は、OutOfStockException をスローします(在庫不足をシミュレートします)
    • それ以外の場合は、"Order placed: [quantity]x [productName]" と出力します

    階層構造を利用して例外をキャッチします。まず InvalidQuantityException をキャッチし、例外のメッセージと getQuantity() メソッドを使用して "Quantity error: [message] (value: [quantity])" と出力します。次に OutOfStockException をキャッチし、例外のメッセージと getProductName() メソッドを使用して "Stock error: [message] (product: [productName])" と出力します。

この階層構造により、特定の例外を個別に処理できる一方で、ベースとなる OrderException クラスを通じて注文に関連するすべての問題をキャッチするオプションも保持できます。各例外のカスタムメソッドは、汎用的な例外では提供できないコンテキストを提供します。

チートシート

Exceptionクラスまたはそのサブクラスを継承して、カスタム例外を作成します:

<?php
class PaymentException extends Exception {}

class InsufficientFundsException extends PaymentException {
    public function __construct(
        private float $required,
        private float $available
    ) {
        parent::__construct("Need $required but only $available available");
    }
    
    public function getShortfall(): float {
        return $this->required - $this->available;
    }
}

カスタム例外は、他のカスタム例外を継承することで階層を形成できます。これにより、異なる具体性のレベルで例外をキャッチできるようになります。例えば、PaymentExceptionですべての支払い関連の例外をキャッチしたり、InsufficientFundsExceptionのような特定の例外を個別に処理したりできます。

カスタム例外には、エラーに関するコンテキストを提供するために、追加のプロパティやメソッドを含めることができます:

<?php
class Wallet {
    public function __construct(private float $balance) {}
    
    public function pay(float $amount): void {
        if ($amount > $this->balance) {
            throw new InsufficientFundsException($amount, $this->balance);
        }
        $this->balance -= $amount;
    }
}

コンストラクタのプロパティ昇格(constructor promotion)を使用して、コンストラクタの引数で直接プロパティを定義します。例外メッセージを設定するには、parent::__construct()を呼び出します。

自分で試してみよう

<?php
// すべての例外ファイルをインクルードする
require_once 'OrderException.php';
require_once 'OutOfStockException.php';
require_once 'InvalidQuantityException.php';

// 入力を読み込む
$productName = trim(fgets(STDIN));
$quantity = intval(trim(fgets(STDIN)));

// TODO: try-catchを使用して注文処理をハンドルする
// tryブロック内:
//   - quantity < 1 の場合、InvalidQuantityExceptionをスローする
//   - quantity > 100 の場合、OutOfStockExceptionをスローする
//   - それ以外の場合、"Order placed: [quantity]x [productName]" と出力する
//
// 最初に InvalidQuantityException をキャッチする:
//   - getMessage() と getQuantity() を使用して "Quantity error: [message] (value: [quantity])" と出力する
//
// OutOfStockException をキャッチする:
//   - getMessage() と getProductName() を使用して "Stock error: [message] (product: [productName])" と出力する

?>
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン