ようこそ、迷える子羊(エンジニア)たちよ。
君たちは普段、エラーに出会ったとき、どんな例外(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を使う。 - 適切な石(例外)を選べば、コードの堅牢性は劇的に上がる。