mcpField
L
laravel-event-driven-architecture

laravel-event-driven-architecture

github.com/iserter开发4885 次安装27 周前更新
代码仓库

关于 laravel-event-driven-architecture

面向 Laravel 开发者的事件驱动架构实践指南,介绍事件、监听器、队列和模型观察者,帮助组织解耦的业务副作用。

主要功能

  1. 01事件与监听器结构
  2. 02监听器自动发现
  3. 03队列监听器及重试设置
  4. 04事务提交后的事件和队列处理
  5. 05事件订阅器与模型观察者
  6. 06事件及监听器测试

适用场景

  1. 01将订单处理副作用拆分为监听器
  2. 02异步生成发票或更新搜索索引
  3. 03确保事务提交后才处理事件
  4. 04测试事件分发和监听器逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/iserter/laravel-claude-agents --skill laravel-event-driven-architecture

安装后在 Claude Code 中输入/laravel-event-driven-architecture确认已加载。

详细介绍

Laravel 事件驱动架构

事件类结构

<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderPlaced
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public readonly Order $order,
    ) {}
}

监听器类结构

<?php

namespace App\Listeners;

use App\Events\OrderPlaced;

class SendOrderConfirmation
{
    public function handle(OrderPlaced $event): void
    {
        $event->order->user->notify(
            new OrderConfirmationNotification($event->order)
        );
    }
}

自动发现监听器

当监听器位于 App\Listeners 目录中,且其 handle 方法的类型提示为某个事件时,Laravel 会自动发现这些监听器。无需手动注册。

// ✅ Auto-discovered - just create the class with a typed handle method
class SendOrderConfirmation
{
    public function handle(OrderPlaced $event): void { /* ... */ }
}

// ✅ One listener handling multiple events
class AuditLogger
{
    public function handleOrderPlaced(OrderPlaced $event): void { /* ... */ }
    public function handleOrderCancelled(OrderCancelled $event): void { /* ... */ }
}

// ❌ Won't be discovered - missing type hint
class SendOrderConfirmation
{
    public function handle($event): void { /* ... */ }
}

分发事件

// ✅ Using static dispatch
OrderPlaced::dispatch($order);

// ✅ Using event helper
event(new OrderPlaced($order));

// ❌ Calling listeners directly instead of dispatching events
(new SendOrderConfirmation)->handle($order); // Tight coupling

队列监听器

use Illuminate\Contracts\Queue\ShouldQueue;

// ✅ Listener runs asynchronously on the queue
class GenerateInvoicePdf implements ShouldQueue
{
    public string $queue = 'invoices';
    public int $tries = 3;
    public array $backoff = [10, 60];

    public function handle(OrderPlaced $event): void
    {
        $pdf = PdfGenerator::fromOrder($event->order);
        Storage::put("invoices/{$event->order->id}.pdf", $pdf);
    }

    public function failed(OrderPlaced $event, \Throwable $exception): void
    {
        // Handle failure
    }

    // Conditionally handle
    public function shouldQueue(OrderPlaced $event): bool
    {
        return $event->order->total > 0;
    }
}

ShouldQueueAfterCommit

use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;

// ✅ Only dispatched to queue after the database transaction commits
class UpdateSearchIndex implements ShouldQueueAfterCommit
{
    public function handle(OrderPlaced $event): void
    {
        SearchIndex::update('orders', $event->order);
    }
}

用于确保事务安全的 ShouldDispatchAfterCommit

// ✅ Event only dispatches after the surrounding transaction commits
class OrderPlaced
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $afterCommit = true;

    public function __construct(
        public readonly Order $order,
    ) {}
}

// This prevents listeners from running on data that might be rolled back
DB::transaction(function () {
    $order = Order::create($data);
    OrderPlaced::dispatch($order); // Dispatched only after commit
});

事件订阅器

<?php

namespace App\Listeners;

use Illuminate\Events\Dispatcher;

class OrderEventSubscriber
{
    public function handleOrderPlaced(OrderPlaced $event): void
    {
        // Log order creation
    }

    public function handleOrderShipped(OrderShipped $event): void
    {
        // Send shipping notification
    }

    public function handleOrderCancelled(OrderCancelled $event): void
    {
        // Process refund
    }

    public function subscribe(Dispatcher $events): array
    {
        return [
            OrderPlaced::class => 'handleOrderPlaced',
            OrderShipped::class => 'handleOrderShipped',
            OrderCancelled::class => 'handleOrderCancelled',
        ];
    }
}

// Register in EventServiceProvider
protected $subscribe = [
    OrderEventSubscriber::class,
];

模型事件和观察器

<?php

namespace App\Observers;

use App\Models\Order;

class OrderObserver
{
    public function creating(Order $order): void
    {
        $order->reference = Order::generateReference();
    }

    public function created(Order $order): void
    {
        OrderPlaced::dispatch($order);
    }

    public function updating(Order $order): void
    {
        if ($order->isDirty('status') && $order->status === 'cancelled') {
            $order->cancelled_at = now();
        }
    }

    public function deleted(Order $order): void
    {
        Storage::deleteDirectory("orders/{$order->id}");
    }
}

// Register via model attribute (Laravel 11+)
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy(OrderObserver::class)]
class Order extends Model
{
    // ...
}

何时使用事件,何时直接调用

// ✅ USE EVENTS when:
// - Multiple side effects from one action
// - Side effects may change independently
// - Side effects can be async
class OrderService
{
    public function place(Order $order): void
    {
        $order->save();

        // Multiple listeners handle: email, invoice, inventory, analytics
        OrderPlaced::dispatch($order);
    }
}

// ✅ USE DIRECT CALLS when:
// - Core business logic that must succeed together
// - Single clear responsibility
// - Synchronous transactional requirement
class OrderService
{
    public function place(Order $order): void
    {
        DB::transaction(function () use ($order) {
            $order->save();
            $this->inventoryService->reserve($order); // Must succeed together
        });

        OrderPlaced::dispatch($order); // Side effects via events
    }
}

// ❌ Don't use events for core logic that must not fail silently
// ❌ Don't use events when you need the return value

测试事件

use Illuminate\Support\Facades\Event;

// ✅ Assert events were dispatched
public function test_placing_order_fires_event(): void
{
    Event::fake();

    $order = Order::factory()->create();
    $this->orderService->place($order);

    Event::assertDispatched(OrderPlaced::class, function ($event) use ($order) {
        return $event->order->id === $order->id;
    });
}

// ✅ Assert event not dispatched
public function test_cancelled_order_does_not_fire_placed(): void
{
    Event::fake();

    $order = Order::factory()->cancelled()->create();
    $this->orderService->place($order);

    Event::assertNotDispatched(OrderPlaced::class);
}

// ✅ Fake only specific events, let others run normally
public function test_order_with_real_listeners(): void
{
    Event::fake([OrderShipped::class]);

    // OrderPlaced listeners will run, OrderShipped will be faked
}

// ✅ Test listener directly
public function test_send_confirmation_listener(): void
{
    Notification::fake();

    $event = new OrderPlaced(Order::factory()->create());
    (new SendOrderConfirmation)->handle($event);

    Notification::assertSentTo($event->order->user, OrderConfirmationNotification::class);
}

检查清单

  • 事件是简单的承载数据的对象(不包含业务逻辑)
  • 每个监听器都只负责一项职责
  • 对于耗时操作(电子邮件、PDF、API 调用)使用队列监听器
  • 使用 ShouldQueueAfterCommit 或 $afterCommit 确保事务安全
  • 尽可能依赖自动发现,而不是手动注册
  • 谨慎使用观察器,且仅用于模型生命周期钩子
  • 不要将核心业务逻辑隐藏在事件监听器中
  • 使用 Event::fake 和 assertDispatched 测试事件
  • 通过直接调用 handle() 单独测试监听器
  • 使用 shouldQueue() 有条件地跳过排队

评论

支持 Markdown