意味例外

「過ちて改めざる、これを過ちという」

  —孔子『論語』(紀元前551-479年)

意味のある失敗

汎用例外は何が起きたかを伝えます:

catch (Exception $e) {
    echo $e->getMessage();  // "検証に失敗しました"
}

それに対して意味例外はなぜ存在できないかを伝えます:

catch (SemanticVariableException $e) {
    foreach ($e->getErrors()->exceptions as $exception) {
        echo get_class($exception) . ": " . $exception->getMessage();
        // EmptyNameException: 名前は空にできません
        // InvalidEmailException: メール形式が無効です
    }
}

ドメイン例外クラス

すべての例外はDomainExceptionを継承します。ドメイン層では技術的例外(RuntimeExceptionInvalidArgumentException等)を使わず、常にドメイン例外を使います。失敗は常にドメインの意味を持つ失敗として表現されます:

abstract class DomainException extends Exception {}

final readonly class EmptyNameException extends DomainException {}

final readonly class InvalidEmailException extends DomainException
{
    public function __construct(public string $invalidEmail)
    {
        parent::__construct("メール形式が無効です: {$invalidEmail}");
    }
}

// 年齢関連の存在失敗
abstract class AgeException extends DomainException {}
final readonly class NegativeAgeException extends AgeException {}
final readonly class AgeTooHighException extends AgeException {}

ドメイン例外はメッセージだけでなく構造化データを持ちます。$invalidEmailプロパティから、プログラムは無効なメールアドレスの値にアクセスできます——表示、APIレスポンス、ログなど、さまざまな用途に:

catch (InvalidEmailException $e) {
    $logData = [
        'invalid_email' => $e->invalidEmail,    // プログラムからアクセス可能
        'user_ip' => $request->getClientIp(),
        'timestamp' => now()
    ];
    Logger::warning('Invalid email attempt', $logData);
}

多言語メッセージ

#[Message]属性で、例外はユーザーの言語で話します:

#[Message([
    'en' => 'Name cannot be empty.',
    'ja' => '名前は空にできません。',
    'es' => 'El nombre no puede estar vacío.'
])]
final readonly class EmptyNameException extends DomainException {}

#[Message([
    'en' => 'Age must be at least {min} years.',
    'ja' => '年齢は最低{min}歳でなければなりません。'
])]
final readonly class AgeTooYoungException extends DomainException
{
    public function __construct(public int $min = 13) {}
}

エラー収集

フレームワークは最初のエラーで止まらず、すべての検証エラーを収集します:

try {
    $user = $becoming(new UserInput('', 'invalid-email', 10));
} catch (SemanticVariableException $e) {
    // 3つのエラーが同時に収集される:
    // - EmptyNameException
    // - InvalidEmailException
    // - AgeTooYoungException

    $messages = $e->getErrors()->getMessages('ja');
    // ["名前は空にできません", "メール形式が無効です", "年齢は最低13歳でなければなりません"]
}

最初のエラーで即座に失敗するのではなく、すべての問題を一度に把握できます。

エラーも存在の1つ

エラー状態も変容の有効な結果として扱えます:

#[Be([ValidUser::class, InvalidUser::class])]
final readonly class UserValidation
{
    public ValidUser|InvalidUser $being;

    public function __construct(#[Input] string $data)
    {
        try {
            $this->being = new ValidUser($data);
        } catch (ValidationException $e) {
            $this->being = new InvalidUser($e->getErrors());
        }
    }
}

例外で実行を止めるのではなく、エラーを型として表現する。失敗も成功と同じく、変容の正当な結果です。


フレームワークの全体像はリファレンスへ ➡️