Events and Listeners
An event is a class that records something that happened; listeners react to it, inline or on a queue.
OrderShipped::dispatch($order); // same as event(new OrderShipped($order))An event is a plain class that records something that happened: OrderShipped, UserRegistered. A listener is a class with a handle method that reacts to one. The order-shipping code does not need to know who cares. One event can have five listeners, and none of them knows about the others.
How the wiring happens
Generate the pair with php artisan make:event OrderShipped and php artisan make:listener SendShipmentNotification --event=OrderShipped. Nothing then registers the listener by hand. Since Laravel 11 the framework discovers it: at boot it scans app/Listeners, finds methods named handle or __invoke, and registers each one for the event type-hinted in its signature. The type hint is the registration. Change it and the listener stops firing. Scan extra directories with withEvents(discover: [...]) in bootstrap/app.php, and run php artisan event:list to see what is really bound.
Dispatch with OrderShipped::dispatch($order). That is shorthand for event(new OrderShipped($order)): the static method comes from the Dispatchable trait and hands the same object to the same dispatcher.
Queueing a listener
Add the Illuminate\Contracts\Queue\ShouldQueue interface and the dispatcher pushes the listener onto the queue instead of running it inline, so a worker must be running. In Laravel 13 the connection, queue and delay are set with the #[Connection], #[Queue] and #[Delay] attributes, or at runtime by the equivalent viaConnection, viaQueue and withDelay methods.
Example
<?php
// app/Events/OrderShipped.php
declare(strict_types=1);
namespace App\Events;
use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, SerializesModels;
public function __construct(
public Order $order,
) {}
}
<?php
// app/Listeners/SendShipmentNotification.php
declare(strict_types=1);
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification
{
// The type hint below is the registration: discovery reads it at boot.
public function handle(OrderShipped $event): void
{
$order = $event->order;
// Notify the customer, write an audit row, update a dashboard.
}
}When to use it
- An order ships and one listener emails the customer while another posts to a Slack channel, with neither knowing the other exists.
- A sign-up has to create a CRM record and send a welcome email, but the HTTP response should go out before either runs.
- A package dispatches its own events, such as Illuminate\Auth\Events\Login, and you need to append an audit row on every sign-in.
- Two teams own two consequences of the same fact, and you want one file per consequence rather than one service calling five others.
More examples
Dispatching: shorthand and longhand
Arguments passed to dispatch() go straight to the event's constructor, which is why the static call looks shorter. Recognise both forms: older code and package code tends to use event(new ...), while generated code uses the static method.
use App\Events\OrderShipped;
// Shorthand, provided by the Dispatchable trait on the event class:
OrderShipped::dispatch($order);
// The same thing written out; both reach the same dispatcher:
event(new OrderShipped($order));
// Conditional forms, also from Dispatchable:
OrderShipped::dispatchIf($order->notifiable, $order);
OrderShipped::dispatchUnless($order->silent, $order);A queued listener with retries and a failure path
The attributes and the via* methods are two spellings of the same settings; the attribute is fixed at the class, the method can read the event. Reach for this whenever the listener sends mail or calls an HTTP API, so a slow third party cannot slow down the response.
<?php
// app/Listeners/SendShipmentNotification.php
declare(strict_types=1);
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\Queue;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;
use Throwable;
#[Queue('notifications')]
#[Tries(3)]
#[Backoff(10)]
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
public function handle(OrderShipped $event): void
{
// Runs in a worker process, not in the web request.
}
// Longhand for #[Queue('notifications')], decided at runtime instead:
// public function viaQueue(): string
// {
// return 'notifications';
// }
public function failed(OrderShipped $event, Throwable $exception): void
{
// Called after the attempts are used up.
}
}Registering a listener by hand
Use this when a listener sits in a domain folder that discovery does not scan, or when the reaction is two lines and does not deserve a class. Listeners are resolved through the container, so a class listener may type-hint its dependencies in the constructor.
<?php
// app/Providers/AppServiceProvider.php
declare(strict_types=1);
namespace App\Providers;
use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use function Illuminate\Events\queueable;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Needed when the listener lives outside the discovered directories.
Event::listen(OrderShipped::class, SendShipmentNotification::class);
// A closure listener: the event is read from the parameter type.
Event::listen(function (OrderShipped $event): void {
// ...
});
// The same closure, run on the queue.
Event::listen(queueable(function (OrderShipped $event): void {
// ...
})->onQueue('notifications'));
}
}
Discussion