Laravelのイベントとリスナーでアプリを拡張する:疎結合にして変更に強い実装へ
- 作成日 2026.02.10
- その他
Laravelのイベント/リスナーは「ある出来事が起きたら、関連処理をあとから追加できる」仕組み。コントローラやサービスに処理を詰め込まず、イベントを境界にして副作用(通知、ログ、外部API連携、集計更新など)を分離すると、機能追加や仕様変更が安全に進む。この記事は、イベント設計の考え方から、実装・テスト・キュー化・運用の注意点までを、実務でそのまま使える形でまとめる。
- 1. イベントで何が変わるか:責務の分離と“後付け拡張”が成立する
- 2. 発生条件:イベントが効く場面と、逆に重くなる場面
- 3. 設計の基本:イベント名は“過去形の事実”にする
- 4. payload設計:必要最小限+再取得できるIDが基本
- 5. 最小実装:ユーザー登録完了イベントを作る
- 6. リスナー実装:通知・ログ・初期化処理を分ける
- 7. 紐付け:EventServiceProviderでイベントとリスナーを登録する
- 8. イベントの発行場所:モデルイベントとドメインイベントを混ぜない
- 9. 同期か非同期か:重い処理はリスナーをキュー化する
- 10. 冪等性:同じイベントが複数回処理されても壊れないようにする
- 11. 例外時の扱い:失敗を見える化して“黙って落ちる”を防ぐ
- 12. テスト方針:イベント発行のテストとリスナーのテストを分ける
- 13. イベントの整理術:増えたら命名規約とディレクトリで統制する
- 14. 運用の現実:イベント駆動は“観測”がセット
- 15. まとめ:イベント/リスナーで拡張しやすいLaravelにする要点
イベントで何が変わるか:責務の分離と“後付け拡張”が成立する
イベントを使わない構成では「ユーザー登録→メール送信→Slack通知→初期データ作成→分析ログ…」が1箇所に集まりやすい。イベントを導入すると「登録が完了した」という事実だけを発行し、後続処理はリスナーへ分離できる。
結果として、追加機能は“リスナーを増やす”だけになり、既存処理の破壊が減る。
発生条件:イベントが効く場面と、逆に重くなる場面
効く場面:
・副作用が増え続ける(通知、ログ、外部連携、集計、キャッシュ更新)
・同じ“出来事”に対して処理が複数ある
・機能追加の頻度が高い
重くなる場面:
・イベント名やpayloadが場当たり的で、追跡できない
・重要処理まで全部イベントにして、失敗が見えなくなる
・同期/非同期の設計が無く、遅延や二重実行が増える
「何をイベントにするか」を決めないと分散しすぎる。
設計の基本:イベント名は“過去形の事実”にする
イベントは「Command(やれ)」ではなく「Event(起きた)」にする。
例:UserRegistered、OrderPlaced、PaymentSucceeded、InvoiceIssued。
過去形の事実にすると、リスナーが増えても意味が壊れにくい。
payload設計:必要最小限+再取得できるIDが基本
イベントに巨大な配列やモデル丸ごとを詰めると、シリアライズや互換性で詰まりやすい。
基本は「ID + 最低限の値」。詳細はリスナーでDBから再取得する。
・DB更新後に発行する
・リスナー側は“無かったら何もしない/安全に終わる”を意識する
最小実装:ユーザー登録完了イベントを作る
まずは最小の例で全体像を作る。ユーザー登録後にイベントを発行し、リスナーでメールやログを処理する。
// app/Events/UserRegistered.php
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use Dispatchable, SerializesModels;
public function __construct(
public int $userId
) {}
}
// 例:登録処理(Service/UseCase/Controllerなど)
use App\Events\UserRegistered;
$user = \App\Models\User::create($data);
// “登録が完了した”事実を発行
UserRegistered::dispatch($user->id);リスナー実装:通知・ログ・初期化処理を分ける
同じイベントに複数リスナーをぶら下げると、処理が増えても見通しが悪くなりにくい。
// app/Listeners/SendWelcomeEmail.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
$user = User::find($event->userId);
if (!$user) return;
// Mail::to($user->email)->send(new WelcomeMail($user));
}
}
// app/Listeners/LogUserRegistered.php
namespace App\Listeners;
use App\Events\UserRegistered;
class LogUserRegistered
{
public function handle(UserRegistered $event): void
{
logger()->info('user.registered', [
'user_id' => $event->userId,
]);
}
}紐付け:EventServiceProviderでイベントとリスナーを登録する
Laravelはイベントとリスナーの対応をプロバイダで管理できる。イベントが増えてもここで全体が見える。
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use App\Events\UserRegistered;
use App\Listeners\SendWelcomeEmail;
use App\Listeners\LogUserRegistered;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
LogUserRegistered::class,
],
];
}イベントの発行場所:モデルイベントとドメインイベントを混ぜない
LaravelにはEloquentのモデルイベント(created/updated/deleted)もある。
・モデルイベント:DB行の変化に紐づく(保存時に自動で飛ぶ)
・ドメインイベント:業務上の出来事に紐づく(「登録完了」「支払い成功」など)
実務で増やすなら、業務用はドメインイベントとして明示的にdispatchする方が読みやすい。
同期か非同期か:重い処理はリスナーをキュー化する
メール送信、外部API、PDF生成、集計更新などは同期でやるとレスポンスが遅くなる。リスナーをキューに回すとWeb処理が軽くなる。
// app/Listeners/SendWelcomeEmail.php(キュー化)
namespace App\Listeners;
use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendWelcomeEmail implements ShouldQueue
{
public $tries = 3; // リトライ回数
public $backoff = 30; // 秒(例)
public function handle(UserRegistered $event): void
{
// ...
}
}冪等性:同じイベントが複数回処理されても壊れないようにする
キューは再試行や二重実行が起きうる。
対策の方向性:
・「すでに送った/作った」をDBで判定する(フラグ、ログテーブル)
・外部APIは冪等キーを付ける
・集計は“加算”ではなく“再計算”に寄せる
冪等性が無いと、歓迎メールが2回飛ぶ、ポイントが二重付与される、などの事故になる。
例外時の扱い:失敗を見える化して“黙って落ちる”を防ぐ
イベント処理は分散しやすく、失敗が気づかれにくい。
・キュー失敗時の通知(Slack/メール)
・failed_jobs の監視
・リスナーごとに重要度を分ける(必須処理と任意処理)
必須処理なら同期で落として即時検知、任意処理なら非同期で再試行、などの切り分けが必要。
テスト方針:イベント発行のテストとリスナーのテストを分ける
テストは2層に分けると楽。
1) 「特定操作でイベントが発行される」を検証
2) 「リスナーが期待通り動く」を検証(単体 or 結合)
// イベントが発行されたことを確認(例)
use Illuminate\Support\Facades\Event;
use App\Events\UserRegistered;
Event::fake();
// 何らかの登録処理を実行...
Event::assertDispatched(UserRegistered::class, function ($e) use ($user) {
return $e->userId === $user->id;
});イベントの整理術:増えたら命名規約とディレクトリで統制する
増えると破綻しやすいポイントは命名と置き場所。
・Events/ と Listeners/ を機能単位サブディレクトリで分ける(例:Events/User、Events/Order)
・イベント名は過去形、リスナー名は動詞から始める(Send…, Sync…, Record…)
・payloadはID中心、破壊的変更はバージョンを切る(例:OrderPlacedV2)
運用の現実:イベント駆動は“観測”がセット
イベントを使うほど、原因調査は「どのイベントが発火し、どのリスナーが成功/失敗したか」を追う形になる。
・リクエストIDをログに残す(入口で生成し全ログへ)
・イベント名/リスナー名/実行時間をログ化
・失敗時はpayload(最低限)を残す
これが無いと、動いていないのか遅いのかが分からない。
まとめ:イベント/リスナーで拡張しやすいLaravelにする要点
・イベントは“過去形の事実”、リスナーは“副作用”を担当
・payloadはID中心で最小化し、詳細はリスナーで再取得
・重い処理はShouldQueueで非同期化し、冪等性を必ず入れる
・必須処理と任意処理を分け、失敗を見える化する
・テストは「発行される」層と「処理する」層で分ける
・命名規約とディレクトリ統制で、増えても壊れない形にする
-
前の記事
Laravelのデバッグテクニック:効率的なバグ修正とトラブルシュートの実務手順 2026.02.10
-
次の記事
記事がありません
コメントを書く