Eloquentイベント vs ドメインイベント: フレームワークのフックだけでは不十分な理由

Eloquentイベント vs ドメインイベント: フレームワークのフックだけでは不十分な理由

アプリケーションが単純なイベントリスナーの枠を超えたとき

あなたのEloquentリスナーは完璧に思えました: それをOrderモデルの saved イベントに結びつけ、注文が保存されたときに確認メールを送信すれば、それで完了です。デモではうまくいきます。しかし本番環境にデプロイすると、サポートチケットが舞い込みます。ある顧客は同じ購入に対して確認メールを2通受け取りました。別の顧客はキャンセル通知なしで返金の領収書を受け取りました。ログをチェックしても、何も異常は見つかりません。メールは送信されています。コードは問題なさそうです。問題は微妙なものであり、アーキテクチャの中に潜んでいます。

今日(2026年7月)これが重要である理由

2026年において、週末のプロジェクトでは機能するものの本番環境で破綻するようなコードは、かつてないほど高くつきます。チームはアプリがスケールすることを期待しています。顧客は信頼性を期待しています。フレームワークのフックは便利ですが、便利さはしばしばコントロールとの引き換えになります。アプリケーションが概念実証(PoC)を超えて成長するにつれて、メールの送信などの副作用をどのように処理するかについての決定が、アプリが保守可能であり続けるか、それとも暗黙の依存関係の迷路になるかを決定します。

Eloquentイベントの魅力

Eloquentイベントは簡単に行える手段です。LaravelのORMはライフサイクルフックを提供します: creating, created, updating, updated, saved, deleted。リスナーを登録すると、イベントが発生したときにそれが発火します。設定も面倒な手続きもありません。

Order::saved(function ($order) {
    Mail::send(new OrderConfirmation($order));
});

これは表面上はクリーンなコードです。意図は明確です: 注文が保存されたら、確認を送信する。それは機能します。そして単純なプロジェクトであれば、それで十分です。問題が表面化するのは、複数のリスナーがある場合や、同じモデルが異なる理由で保存された場合です。

隠れた問題: 意図とコンテキスト

ここで問題が発生します。Eloquentの saved イベントは、なぜ保存されたかに関係なく、注文が保存されるたびに発火します。あなたが作成したのかもしれません。ステータスを'pending'から'confirmed'に更新したのかもしれません。配送先住所を更新したのかもしれません。その saved イベントは気にしません——いずれにせよ同じように発火します。

さて、このシナリオを想像してください。返金フローが refunded ステータスで注文を保存します。別のプロセス——スケジュールされたジョブか、あるいは決済代行業者からのWebhookかもしれません——も注文を更新します。両方がその saved イベントをトリガーします。2つのリスナーが実行されます。1つは返金メールを送信し、もう1つは異なる通知を送信します。顧客は両方のメッセージを受け取り、どちらが本物なのか混乱します。

根本的な原因は、Eloquentイベントがビジネスアクションではなくデータベース操作に結びついていることです。あなたのコードは「注文が保存された」と言っていますが、あなたが本当に意味しているのは「注文が作成された」か、「返金が発行された」か、「配送の詳細が更新された」ということです。これらは異なる副作用を伴う異なるアクションです。

フレームワークイベントはスケールしない

アプリケーションが成長するにつれて、この問題は複合的に悪化します。あなたは新しいリスナーを追加します。それは機能します。あなたはもう1つ追加します。今、あなたはその saved イベントに5つのリスナーを持っており、それぞれが何をするのか思い出せません。バグが現れたとき、どれが原因かを見つけるために5つすべてを追跡しなければなりません。依存関係は暗黙的であり、コードベース全体に散在しています。

さらに悪いことに、2つのリスナーがお互いに依存している場合——一方が他方の前に実行される必要がある場合——その順序を強制する方法はありません。Eloquentはそれらを登録順に発火させますが、これは脆弱です。誰かがファイル内の間違った場所にリスナーを追加すると、副作用が間違った順序で発生します。

ドメインイベント: ファーストクラスとしての意図

ドメインイベントは、ドメイン駆動設計(DDD)から借りた異なるアプローチです。データベース操作に依存するのではなく、ビジネスロジックで実際に何が起こったかを表すイベントを発行します。

その saved イベントの代わりに、あなたは OrderCreated イベントや OrderRefunded イベントを発行するでしょう。各イベントはコンテキストを運びます: 何が起こったのか、そしてなぜか。あなたのリスナーは、関心のあるイベントを購読します。

class CreateOrderAction
{
    public function execute(CreateOrderRequest $request)
    {
        $order = Order::create([...]);
        
        event(new OrderCreated($order));
        
        return $order;
    }
}

今、あなたの確認メールリスナーは OrderCreatedのみを購読し、 OrderSavedは購読しません。返金メールは OrderRefundedのみを購読します。各副作用は、データベース操作ではなく、それが表すアクションに結びついています。

副作用を分離する

ドメインイベントはまた、ドメインロジックをフレームワークから切り離します。Eloquentの saved イベントはLaravelの概念です。ドメインイベントはそうではありません。もしビジネスロジックを別のフレームワークに移行したい場合や、コンソールコマンドで使用したい場合、あるいは分離してテストしたい場合、ドメインイベントはそれを可能にします。フレームワークイベントはそれをより困難にします。

ドメインイベントを使用すると、ビジネスロジックはORMから独立して、アクションクラスやサービスに存在します。フレームワークはあなたのロジックが絡み合うものではなく、あなたが使うツールになります。

具体的な例: 返金フロー

返金プロセスを想像してください。起こるべき3つのことがあります: 注文ステータスの更新、マーチャントアカウントからの資金の差し引き、そして顧客への返金メールの送信。

Eloquentイベントを使う場合、あなたはリスナーを saved イベントに結びつけるでしょう。しかしそれは雑です。マーチャントアカウントの差し引きは、注文が保存されることとは何の関係もありません。それは返金が開始されたときに起こるべきです。

class ProcessRefund
{
    public function execute(Order $order, RefundDetails $details)
    {
        $order->status = 'refunded';
        $order->save();
        
        event(new OrderRefunded($order, $details));
    }
}

今、あなたは OrderRefunded を購読し、それぞれの関心事を個別に処理できます。マーチャントアカウントの差し引き、メール、会計元帳のエントリ——各リスナーは1つのことを処理します。

移行の道筋

今日、すべてのEloquentリスナーを取り除く必要はありません。移行は段階的に行うことができます。アクションクラスからドメインイベントを発行することから始めましょう。時間の経過とともに、リスナーを移行していきます。置き換えたらEloquentリスナーを削除してください。

結論

フレームワークのイベントフックは便利ですが、コードが成長するにつれて明確さと保守性を犠牲にする近道です。ドメインイベントは最初にもう少し考える必要がありますが、スケールします。それらは何がなぜ起こっているのかについて明示的です。それらはビジネスロジックをフレームワークから切り離します。バグが現れたとき、あなたはどこを見るべきか正確に知っています。

メリット

  • イベントはデータベース操作ではなく、実際のビジネスアクションを表す
  • どのアクションに対してどの副作用がトリガーされるかを理解しやすい
  • リスナー間に暗黙の依存関係がない
  • フレームワーク非依存——あなたのビジネスロジックはLaravelに結びついていない
  • フレームワークのセットアップなしに分離してテスト可能
  • リスナーを意図的に順序付けし、調整できる

デメリット

  • フレームワークのリスナーよりも多くのボイラープレートが必要
  • イベントを手動で発行する必要がある; 自動的には発生しない
  • 開発中の精神的な負担がより大きい
  • 規律が必要——イベントの発行を忘れやすい
  • イベントがログに記録されていない場合、デバッグがわずかに困難になる

注意

この記事のサンプルコードとパターン名は一般的な説明であり、特定の本番コードではありません。本番環境にデプロイする前に、イベントフローを徹底的にテストしてください。自己責任で進めてください。現在Eloquentイベントを使用している場合、ドメインイベントへの移行はリファクタリングタスクであり、副作用に対する適切なテストカバレッジを備えた上で慎重に行う必要があります。

よくある質問

  • 同じアクションに反応する必要がある複数のドメインがある場合はどうすればよいですか?
  • イベントの順序付けやリスナー間の依存関係をどのように処理すればよいですか?
  • Eloquentでドメインイベントを使用できますか、それとも別のORMが必要ですか?
  • イベントリスナーが失敗した場合はどうなりますか——トランザクション全体がロールバックされますか?
  • ドメインイベントリスナーを分離してテストするにはどうすればよいですか?
  • データベースに保存する前と後のどちらでドメインイベントを発行するべきですか?
  • ドメインイベントとWebhookの違いは何ですか?
  • ドメインイベントを使用する場合、結果整合性をどのように処理すればよいですか?

タグ

#laravel #domainDrivenDesign #architecture #eventSourcing #PHP #cleanArchitecture #refactoring #scalability

Free field guide

Kubernetes Security Checklist

Harden cluster access, workload identity, pod security, network boundaries, software supply chain, secrets, and operational monitoring.