イベント&リスナーパターンとは
LaravelのイベントとリスナーはPub/Sub(発行・購読)パターンを実装したものです。
- イベント:何かが起きたことを表すクラス(例:
OrderPlaced) - リスナー:イベントを受け取って処理するクラス(例:
SendOrderConfirmation)
コントローラーやサービスがリスナーに直接依存しないため、処理を追加・削除しても既存コードを変更する必要がありません。これを疎結合と言います。
ObserverとEvent/Listenerの使い分け
| Observer | Event/Listener | |
|---|---|---|
| トリガー | Eloquentモデルのライフサイクル | 明示的にdispatch() |
| リスナー数 | 1つのモデルに1つのObserver | 1つのイベントに複数のリスナー |
| 向いている用途 | モデル変更に密結合した副作用 | ビジネスイベントの通知・非同期処理 |
イベントとリスナーの作成
# イベントとリスナーをまとめて作成
php artisan make:event OrderPlaced
php artisan make:listener SendOrderConfirmation --event=OrderPlaced
php artisan make:listener UpdateInventory --event=OrderPlaced
php artisan make:listener NotifyAdmin --event=OrderPlaced
イベントクラス
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderPlaced
{
use Dispatchable, SerializesModels;
public function __construct(
public readonly Order $order,
public readonly array $metadata = [],
) {}
}
リスナークラス
<?php
namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Mail\OrderConfirmation;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
// ShouldQueue を実装するとキューで非同期実行
class SendOrderConfirmation implements ShouldQueue
{
public string $queue = 'notifications';
public int $tries = 3;
public int $backoff = 60; // 失敗後60秒待ってリトライ
public function handle(OrderPlaced $event): void
{
Mail::to($event->order->customer_email)
->send(new OrderConfirmation($event->order));
}
public function failed(OrderPlaced $event, \Throwable $e): void
{
Log::error("注文確認メール送信失敗: #{$event->order->id}", [
'error' => $e->getMessage(),
]);
}
}
// 在庫更新リスナー
class UpdateInventory implements ShouldQueue
{
public string $queue = 'inventory';
public function handle(OrderPlaced $event): void
{
foreach ($event->order->items as $item) {
$item->product->decrement('stock', $item->quantity);
}
}
}
EventServiceProviderに登録
// app/Providers/EventServiceProvider.php
protected $listen = [
OrderPlaced::class => [
SendOrderConfirmation::class,
UpdateInventory::class,
NotifyAdmin::class,
],
UserRegistered::class => [
SendWelcomeMail::class,
CreateDefaultSettings::class,
TrackRegistrationAnalytics::class,
],
];
イベントのdispatch
// コントローラー
class OrderController extends Controller
{
public function store(OrderRequest $request): JsonResponse
{
$order = DB::transaction(function () use ($request) {
$order = Order::create($request->validated());
$order->items()->createMany($request->items);
return $order;
});
// イベントを発火(登録済みのリスナーが全て実行される)
OrderPlaced::dispatch($order, ['source' => 'web']);
return response()->json(new OrderResource($order), 201);
}
}
イベントのブロードキャスト
リアルタイム更新(WebSocket)が必要な場合は ShouldBroadcast を実装します。
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, SerializesModels;
public function __construct(public readonly Order $order) {}
// フロントエンドが購読するチャンネル名
public function broadcastOn(): array
{
return [
new PrivateChannel("orders.{$this->order->id}"),
];
}
public function broadcastAs(): string
{
return 'order.status.updated';
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'status' => $this->order->status,
];
}
}
テスト
class OrderEventTest extends TestCase
{
use RefreshDatabase;
public function test_注文作成時に複数のイベントリスナーが実行される(): void
{
Event::fake([OrderPlaced::class]);
$order = Order::factory()->create();
OrderPlaced::dispatch($order);
Event::assertDispatched(OrderPlaced::class, function ($event) use ($order) {
return $event->order->id === $order->id;
});
}
}
まとめ
Laravelのイベント&リスナーを使うことで、「注文が入ったら何をするか」という処理を複数のリスナーに分散でき、機能追加・削除が容易になります。弊社では注文管理・予約システム・会員登録フローで活用しています。
LaravelによるWebシステム開発のご相談はお気軽にどうぞ。