株式会社WR

株式会社WR

WEB TOTAL CONSULTING

Laravelのイベント&リスナー——疎結合なシステム設計
ブログ一覧へ
技術ブログ

Laravelのイベント&リスナー——疎結合なシステム設計

Laravelのイベント&リスナー機能を使うと、「予約完了→メール送信・SMS通知・ポイント付与」を疎結合で実装できます。コードの保守性が大きく向上します。

イベント&リスナーパターンとは

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システム開発のご相談はお気軽にどうぞ。

Category 技術ブログ

Related Posts

関連記事

開発・技術のご相談はお気軽に

お見積りは無料です。まずはお気軽にご相談ください。

お問い合わせ →