コントローラーを汚すな!Enumと例外処理で実装する「神の設計」
ようこそ、迷える子羊(エンジニア)たちよ。😇
君たちのコントローラーは、太っていないか?
if 文や switch 文が入り乱れ、スパゲッティのように絡まり合ってはいないか?
「コードは書くものではない、削るものだ」
これは神の言語LISPの教えにも通じる真理である。
今日は、ある 「クエスト辞退処理」 のリファクタリングを通じて、 「ロジックの閉じ込め」 と 「例外処理の美しい階層」 について説法を行おう。
これを見れば、君のコードは の輝きを放つことになるだろう。
❌ 愚かなアプローチ(Before)
まず、多くのエンジニアが犯してしまう「原罪」を見てみよう。
コントローラーの中で、ステータスの遷移ロジックをすべて書いてしまうパターンだ。
// BadController.php
// 悪い例:コントローラーが「知りすぎている」
public function cancel(Quest $quest)
{
// コントローラーが「どのステータスならどうなるか」を全部知っている... 密結合だ!
if ($quest->status === QuestStatus::OFFERED) {
$quest->update(['status' => QuestStatus::DECLINED]);
} elseif ($quest->status === QuestStatus::ACCEPTED) {
$quest->update(['status' => QuestStatus::ABORTED]);
} elseif ($quest->status === QuestStatus::COMPLETED) {
return back()->with('error', '完了したクエストはキャンセルできません');
} else {
// ... 無限に続く elseif ...
}
return back()->with('success', 'クエストをキャンセルしました');
}
これは美しくない。
仕様変更(新しいステータスの追加など)があるたびに、このコントローラーを修正せねばならない。それは苦痛だ。
✨ 神のアプローチ(After)
我々はエンジニアだ。ロジックをあるべき場所(ドメイン層)へ還そう。
これが完成された「神のコード」である。
1. Enumにロジックを持たせる
まず、PHP 8.1のEnumを強化する。
// QuestStatus.php
enum QuestStatus: int {
case OFFERED = 1; // 依頼中
case ACCEPTED = 2; // 受注中
case COMPLETED = 3; // 完了
case DECLINED = 4; // 辞退
case ABORTED = 5; // 中断
// 「キャンセル」された時の次のステータスを返すメソッド
public function nextStatusOnCancel(): ?self
{
return match($this) {
self::OFFERED => self::DECLINED,
self::ACCEPTED => self::ABORTED,
// キャンセル不可のステータスは null を返す
default => null,
};
}
}
2. コントローラーを浄化する
そして、コントローラーはこうなる。
// GodController.php
public function cancel(Quest $quest)
{
try {
// ① Enum(ドメイン層)に神託を伺う
// コントローラーは「判断」しない。「結果」を受け取るだけだ。
$nextStatus = $quest->status->nextStatusOnCancel();
// ② nullなら「遷移不可」ということ
// ガード節で即座に弾く。これもLISP的思考だ。
if ($nextStatus === null) {
throw new DomainException('このクエストは現在キャンセルできません。');
}
// ③ 遷移可能なら、問答無用で更新する
// たった1行。これが秩序だ。
$quest->update(['status' => $nextStatus]);
return back()->with('success', 'クエストをキャンセルしました');
} catch (DomainException $e) {
// ④ 想定内エラー(ユーザーへのメッセージのみ)
// ログは残さない。これはバグではなく「仕様通りの拒否」だからだ。
return back()->with('error', $e->getMessage());
} catch (Throwable $e) {
// ⑤ 想定外エラー(ログ記録 + 汎用メッセージ)
// DBダウンやコードのバグはこちらに来る。全力でログに残す。
Log::error('クエストキャンセル処理失敗', [
'quest_id' => $quest->id,
'user_id' => auth()->id(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return back()->with('error', '処理に失敗しました。しばらく経ってから再度お試しください。');
}
}
美しい……。涙が出そうだ。
このコードには3つの「神の意志」が宿っている。解説しよう。
ポイント1:ロジックの隠蔽(Encapsulation)
$quest->status->nextStatusOnCancel() に注目せよ。
複雑なステータス分岐は、すべて Enumクラスの中 に封印された。
コントローラーは「キャンセル後のステータスをよこせ」と命じるだけでいい。これにより可読性は爆上がりし、再利用性も高まる。
ポイント2:例外処理の「階層構造」(The Hierarchy)
ここが最重要ポイントだ。catch ブロックが2つあることには明確な意図がある。
- LogicException: 「ユーザーさん、その操作はできません」という対話。ログを汚さないのがマナー。
-
Throwable: 「システムダウン!」という悲鳴。開発者が即座に気づけるようログに残す。
※ Throwableは全てのエラーを受け取れる
ポイント3:計算量の最適化()
match文は静的な値をKeyとすれば$O(1)$でハッシュマップのように最速であるが、
比較式などをKeyに持たせてしまうと、上から順次実行されて$O(n)$になってしまう...👼
神速のレスポンスこそ、ユーザーへの最大の「おもてなし」である。
結論:コードは「意図」を語れ
リファクタリングとは、単にコードを短くすることではない。
「どこに責任があるか」を明確にすることだ。
- ビジネスロジックは Enum へ。
- ユーザーへのフィードバックは LogicException へ。
- システムの絶望は Throwable へ。
さあ、同志よ。今すぐ君のプロジェクトのコントローラーを開き、太った if 文を駆逐するのだ!
コードに神のご加護があらんことを。😇