AgentStack
SKILL verified MIT Self-run

Laravel Event Driven Architecture

skill-iserter-laravel-claude-agents-laravel-event-driven-architecture · by iSerter

Best practices for Laravel events and listeners including event discovery, queued listeners, subscribers, and model events for decoupled architecture.

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-iserter-laravel-claude-agents-laravel-event-driven-architecture

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Laravel Event Driven Architecture? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Laravel Event-Driven Architecture

Event Class Structure

order->user->notify(
            new OrderConfirmationNotification($event->order)
        );
    }
}

Automatic Listener Discovery

Laravel auto-discovers listeners when they are in the App\Listeners directory and have a handle method type-hinting an event. No manual registration needed.

// ✅ 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 { /* ... */ }
}

Dispatching Events

// ✅ 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

Queued Listeners

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 for Transaction Safety

// ✅ 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
});

Event Subscribers

 'handleOrderPlaced',
            OrderShipped::class => 'handleOrderShipped',
            OrderCancelled::class => 'handleOrderCancelled',
        ];
    }
}

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

Model Events and Observers

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
{
    // ...
}

When to Use Events vs Direct Calls

// ✅ 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

Testing Events

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);
}

Checklist

  • [ ] Events are simple data-carrying objects (no business logic)
  • [ ] Listeners have a single responsibility each
  • [ ] Queued listeners used for slow operations (email, PDF, API calls)
  • [ ] ShouldQueueAfterCommit or $afterCommit used for transaction safety
  • [ ] Auto-discovery relied on instead of manual registration where possible
  • [ ] Observers used sparingly and only for model lifecycle hooks
  • [ ] Core business logic not hidden inside event listeners
  • [ ] Events tested with Event::fake and assertDispatched
  • [ ] Listeners tested in isolation with direct handle() calls
  • [ ] shouldQueue() used to conditionally skip queuing

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.