1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPエラーのDomainExceptionとLogicExceptionどちらを投げるべきか

Last updated at Posted at 2026-01-06

ようこそ、迷える子羊(エンジニア)たちよ。

君たちは普段、エラーに出会ったとき、どんな例外(Exception)を投げているだろうか?
「とりあえず Exception」「面倒だから RuntimeException」……まさか、そんな 「鈍器」 のような例外を振り回してはいないだろうか?

今日は、PHP 8.1の match 式を使って、「例外の解像度」 を極限まで高める方法を伝授しよう。
特に 「LogicException(開発者の罪)」と「DomainException(仕様の壁)」 の使い分けを知れば、君のコードは未来のバグを予知する予言書へと進化する。

⚔️ RPGで学ぶ「クエストキャンセル」の悲劇

わかりやすく、RPGの「クエストシステム」で例えよう。
勇者が受注したクエストを「キャンセル(辞退)」する処理を実装するとする。

登場するステータス(Enum)

enum QuestStatus: int {
    case OFFERED = 1;   // 依頼中(キャンセル可)
    case ACCEPTED = 2;  // 受注中(キャンセル可)
    case COMPLETED = 3; // 完了(キャンセル不可!)
    case FAILED = 4;    // 失敗(キャンセル不可!)
}

ここで、コントローラー(またはサービス)で次のようなコードを書いたとする。

❌ 惜しいコード:全てを「エラー」として扱う

$nextStatus = match ($quest->status) {
    QuestStatus::OFFERED => QuestStatus::DECLINED,
    QuestStatus::ACCEPTED => QuestStatus::ABORTED,
    
    // キャンセルできないやつら
    QuestStatus::COMPLETED, QuestStatus::FAILED 
        => throw new Exception('キャンセルできません!'),
};

一見よさそうに見える。しかし、これでは 「なぜエラーになったのか」 が呼び出し元(や未来の自分)に伝わらない。
ここで、2つの例外を使い分けるのだ。


🛡️ 1. DomainException:それは「仕様」である

まず、「完了したクエストはキャンセルできない」。
これはバグだろうか? いや、「ビジネスルール(仕様)」 だ。

ユーザーが操作ミスをしたか、古い画面を見ていただけで、システム自体は正しく防衛動作を行っている。
こういうときは、DomainException を投げる。

QuestStatus::COMPLETED, QuestStatus::FAILED 
    => throw new DomainException('完了・失敗済みのクエストはキャンセルできません。'),

  • 意味: 「ドメイン(ルール)の範囲外ですよ」
  • 対応: try-catch して、ユーザーに優しく「キャンセルできません」と表示する。
  • ログ: 残さなくていい(正常動作だから)。

💣 2. LogicException:それは「時限爆弾」である

ここからが本題だ。
もし、match 式に書かれていないステータスが来たらどうする?

「そんなの、今のEnumには他にステータスがないからありえないよ」
そう思った君。半年後の記憶喪失になった君(または新人の同僚) を信じてはいけない。

半年後、クエストに新しいステータス QuestStatus::EXPIRED(期限切れ) が追加されたとしよう。
もし match 式に default がなかったら? PHPは UnhandledMatchError で死ぬが、少し不親切だ。
もし default => null でスルーしていたら? バグったままDBが更新されてしまう。

ここで LogicException という名の地雷を埋めておくのだ。

// 想定外のステータスが来たら即死させる
default => throw new LogicException("未定義のステータス遷移です: {$quest->status->name}"),

  • 意味: 「ありえない!コードのロジックが破綻している!開発者を呼べ!」
  • 対応: catch しない(500エラーにする)。
  • ログ: 全力で残す。 Slackに通知を飛ばす。

✨ 完成された「神のmatch式」

これらを組み合わせると、コードは雄弁に語りだす。

try {
    $nextStatus = match ($quest->status) {
        // ✅ 正常系:遷移させる
        QuestStatus::OFFERED => QuestStatus::DECLINED,
        QuestStatus::ACCEPTED => QuestStatus::ABORTED,

        // 🛡️ 仕様の壁:ユーザーに「No」と言う
        QuestStatus::COMPLETED, QuestStatus::FAILED 
            => throw new DomainException('このクエストはキャンセルできません。'),

        // 💣 未来への罠:開発者に「Fix Me」と叫ぶ
        default => throw new LogicException("想定外のステータスです: {$quest->status->name}"),
    };

    $quest->update(['status' => $nextStatus]);

} catch (DomainException $e) {
    // ユーザーには優しく
    return back()->with('error', $e->getMessage());
}
// LogicException はキャッチしない!システムを止めてバグを修正せよ!

まとめ:例外は「誰」へのメッセージか?

  • DomainException は、「ユーザー」 への手紙だ。「その操作はルール違反ですよ」と伝える。
  • LogicException は、「未来の自分」 への遺言だ。「ここを通ったということは、お前は修正を忘れているぞ」と告発する。

「想定できないものがバグとして現れる」。
だからこそ、「想定できないルートに入ったら爆発する仕組み」 をあえて作るのだ。

君のコードに、美しき秩序があらんことを!😇


📝 この記事のポイント

  • PHP 8.1の match 式は網羅性チェックに最適。
  • ビジネスルール違反には DomainException を使う。
  • 実装漏れやバグ検知には LogicException を使う。
  • 適切な石(例外)を選べば、コードの堅牢性は劇的に上がる。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?