skip to content

In Laravel 13, how do you create an event and a listener with Artisan, and how does the framework connect the listener to its event?

level: juniorimportance: must knowfreq 68%

answer

  1. the event is a plain data class
  2. make:listener with --event
  3. type-hint on handle() is the wiring
  4. app/Listeners scanned by default
  5. event:list prints the map

basics

~10 s

Run make:event OrderPlaced and make:listener ReserveInventory --event=OrderPlaced. Laravel 13 discovers listeners in app/Listeners and registers each handle* or __invoke method for the event class type-hinted on its first parameter; Event::listen registers one by hand.

solid answer

~40 s

`php artisan make:event OrderPlaced` creates a data class in `app/Events` using the `Dispatchable` and `SerializesModels` traits, and `php artisan make:listener ReserveInventory --event=OrderPlaced` creates a listener in `app/Listeners` whose `handle(OrderPlaced $event)` is already typed. Laravel 13 has no `$listen` array to fill: **event discovery** scans `app/Listeners` and registers every public method whose name starts with `handle` (or is `__invoke`) for the class type-hinted on its first parameter, and a union type registers it for several events. Other directories are added with `->withEvents(discover: [...])` in `bootstrap/app.php`; `Event::listen(OrderPlaced::class, ReserveInventory::class)` in `AppServiceProvider::boot()` registers a listener by hand. You fire it with `OrderPlaced::dispatch($order)` or `event(new OrderPlaced($order))`, and `php artisan event:list` shows what is wired to what.

code

bash · 4 lines
bash
php artisan make:event OrderPlaced
php artisan make:listener ReserveInventory --event=OrderPlaced
php artisan make:listener SendOrderConfirmation --event=OrderPlaced --queued
php artisan event:list --event=OrderPlaced

go deeper

for a junior

Recall the two Artisan commands, the --event option, and that the type-hint on handle() is what connects a listener to its event.

for a middle

Explain how discovery scans app/Listeners, which method names it reads, how withEvents() changes the paths, and why mixing discovery with Event::listen double-registers.

for a senior

Show you debug wiring with event:list, keep one registration route per team, and know the discovered map is cached at deploy so a stale cache can hide a new listener.

for a principal

Weigh convention-based discovery against explicit registration for a large codebase: discoverability of side effects, onboarding cost, and how domain folders map to discovery paths.

## What an event and a listener are An **event** in Laravel is a plain PHP class that describes something that already happened in the application, such as an order being placed. It carries data (usually a model) and no behaviour. A **listener** is a class that reacts to that event: it reserves stock, sends the confirmation email or records an analytics row. One event can have many listeners, and none of them needs to know about the others or about the code that fired the event. For an order checkout the fan-out looks like this: - `App\Events\OrderPlaced` — holds the `Order` model - `App\Listeners\ReserveInventory` — decrements stock - `App\Listeners\SendOrderConfirmation` — emails the customer - `App\Listeners\RecordOrderAnalytics` — writes a reporting row ## Generating the classes Laravel ships two generators: 1. `php artisan make:event OrderPlaced` writes `app/Events/OrderPlaced.php`. The stub uses the `Dispatchable`, `InteractsWithSockets` and `SerializesModels` traits and also includes a `broadcastOn()` method you can delete if the event is not broadcast. 2. `php artisan make:listener ReserveInventory --event=OrderPlaced` writes `app/Listeners/ReserveInventory.php` with `handle(OrderPlaced $event): void` already imported and typed. Without `--event` the command prompts for it. 3. Add `--queued` to `make:listener` when the listener should run on the queue; the plain stub imports `ShouldQueue` but does not implement it, so a generated listener is synchronous by default. The `Dispatchable` trait gives the event static `dispatch()`, `dispatchIf()` and `dispatchUnless()` methods, each of which passes its arguments to the constructor and hands the new object to the `event()` helper. ## How discovery wires them Since the slim skeleton of Laravel 11 there is no `EventServiceProvider` in a new app, and Laravel 13 keeps it that way. `Application::configure()` calls `withEvents()`, which turns on **event discovery**: - the framework scans `app/Listeners` (or the paths you pass to `withEvents(discover: [...])`, where `*` wildcards such as `app/Domain/*/Listeners` are allowed) - for every instantiable class it inspects the public methods whose names match `handle*` or are `__invoke` - the class names type-hinted on the **first parameter** decide the event, so `handle(OrderPlaced|OrderCancelled $event)` registers the method for both events - a listener implementing `ShouldBeDiscovered` can return `false` from its static `shouldBeDiscovered()` to opt out The class name of the listener plays no part: `ReserveInventory` is attached to `OrderPlaced` only because of the type-hint. ## Registering by hand Discovery is not the only way in. The options compare like this: | Approach | Where it lives | Typical use | |---|---|---| | Discovery | listener classes in scanned directories | the default for application listeners | | `Event::listen(Event::class, Listener::class)` | `AppServiceProvider::boot()` | listeners outside the scanned paths, package code | | `Event::listen(function (OrderPlaced $e) { ... })` | `AppServiceProvider::boot()` | tiny closures; the closure's type-hint names the event | | `withEvents(discover: false)` | `bootstrap/app.php` | turning discovery off and registering everything by hand | Pick one route per listener. `Event::listen` appends to the dispatcher's list without checking for duplicates, so a listener that discovery already found and that you also register by hand runs **twice** for every event. ## Closure and wildcard listeners `Event::listen()` also accepts a closure on its own. Laravel reads the closure's first parameter type to find the event, so `Event::listen(function (OrderPlaced $event) { ... })` needs no event name. Wrapping the closure in `Illuminate\Events\queueable()` sends it to the queue. A string pattern with `*`, such as `Event::listen('orders.*', function (string $eventName, array $data) { ... })`, registers a **wildcard listener** that receives the event name and the payload array; it is useful for logging string-named events but loses the typed event object, so class-based events with typed listeners remain the normal choice. ## Dispatching and checking the wiring A controller or action fires the event after the order exists: ```php OrderPlaced::dispatch($order); // or event(new OrderPlaced($order)); ``` Listeners are resolved from the service container, so constructor dependencies are injected. `php artisan event:list` prints every event with its listeners (filter with `--event=`, output JSON with `--json`), which is the first thing to run when a listener never fires or fires twice. In production the discovered map is cached as part of the deploy optimisation step rather than scanned on each request. ## Common mistakes - expecting a listener to be matched to an event by a similar name - putting business logic in the event class instead of the listeners - keeping discovery on and adding `Event::listen` for the same class - forgetting `--queued`, then wondering why an email listener slows the request

  • A listener in app/Listeners runs twice per event after someone added Event::listen for it in AppServiceProvider; why?
    Discovery already registered it, and `Event::listen` appends a second entry without checking for duplicates, so the dispatcher calls it twice. `php artisan event:list` shows the listener listed twice under the event. Remove the manual call, or turn discovery off with `->withEvents(discover: false)` and register everything by hand; mixing the two for the same class is the bug.
  • How do you make one listener method handle two different events?
    Type-hint a union on the first parameter, for example `handle(OrderPlaced|OrderCancelled $event)`. Discovery reads every class in the union and registers the method for each. Inside the method, branch on `instanceof` if the two events need different work.
  • Your listeners live in app/Domain/Orders/Listeners; how do you get Laravel 13 to find them?
    Pass the paths to `withEvents()` in `bootstrap/app.php`, for example `->withEvents(discover: [__DIR__.'/../app/Domain/*/Listeners'])`. The `*` wildcard matches every domain folder. The paths you pass replace the default `app/Listeners`, so include it too if you still use it.

saying these in an interview costs you the question

  • Every listener must be added to the $listen array of EventServiceProvider in Laravel 13.
  • Discovery pairs listeners with events by matching their class names.
  • make:listener generates a queued listener unless you pass a flag.
  • Registering a discovered listener again with Event::listen is harmless because duplicates are ignored.
  • The event class should contain the code that reserves stock and sends the email.