Events & Listeners

Decouple with events

Events let you react to things without cluttering your controllers.

Event
// app/Events/OrderPlaced.php
class OrderPlaced
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
Controller
// app/Http/Controllers/OrderController.php
public function store(StoreOrderRequest $request)
{
    $order = Order::create($request->validated());

    OrderPlaced::dispatch($order);

    // ...
}
Listener
// app/Listeners/UpdateOrderStatus.php
class UpdateOrderStatus
{
    public function handle(OrderPlaced $event): void
    {
        if ($event->order->total <= 0) {
            $event->order->update(['status' => 'cancelled']);
            return;
        }

        $event->order->update(['status' => 'processing']);
    }
}
Feature Test — assert event is dispatched
// tests/Feature/Http/Controllers/OrderController/StoreTest.php
Event::fake();

$this->post(route('orders.store'), $data);

Event::assertDispatched(OrderPlaced::class);
Unit Test — assert listener logic
// tests/Unit/Listeners/UpdateOrderStatusTest.php
class UpdateOrderStatusTest extends TestCase
{
    #[Test]
    public function listener_is_triggered_when_event_is_dispatched(): void
    {
        Event::assertListening(
            OrderPlaced::class,
            UpdateOrderStatus::class
        );
    }

    #[Test]
    public function status_is_processing_when_order_has_total(): void
    {
        $order = Order::factory()->create(['total' => 100]);
        $event = new OrderPlaced($order);
        $listener = new UpdateOrderStatus;

        $listener->handle($event);

        $this->assertSame('processing', $order->fresh()->status);
    }

    #[Test]
    public function status_is_cancelled_when_order_is_free(): void
    {
        $order = Order::factory()->create(['total' => 0]);
        $event = new OrderPlaced($order);
        $listener = new UpdateOrderStatus;

        $listener->handle($event);

        $this->assertSame('cancelled', $order->fresh()->status);
    }
}