skip to content

In Laravel 13, how do you generate an Eloquent observer class and register it so its methods run on model events?

level: juniorimportance: must knowfreq 60%

answer

  1. one class, one method per event
  2. make:observer with --model
  3. lands in app/Observers
  4. #[ObservedBy] on the model
  5. or observe() in AppServiceProvider::boot

basics

~10 s

Run php artisan make:observer ListingObserver --model=Listing, which writes app/Observers/ListingObserver.php with methods named after events, then register it with #[ObservedBy([ListingObserver::class])] on the model or Listing::observe() in AppServiceProvider::boot().

solid answer

~30 s

An observer is a plain class whose public methods are named after Eloquent events (`created`, `updated`, `deleted`, `saving`...) and receive the model. `php artisan make:observer ListingObserver --model=Listing` writes it to `app/Observers` with `created`, `updated`, `deleted`, `restored` and `forceDeleted` stubs; you add or remove methods as needed. It only runs once registered: put `#[ObservedBy([ListingObserver::class])]` on the model, or call `Listing::observe(ListingObserver::class)` in `AppServiceProvider::boot()`. Laravel matches methods by name and resolves the observer from the container, so constructor injection works. Older apps listed observers in an `EventServiceProvider` `$observers` array; the Laravel 11+ skeleton has no such provider.

code

php · 13 lines
php
<?php

namespace App\Models;

use App\Observers\ListingObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

#[ObservedBy([ListingObserver::class])]
class Listing extends Model
{
    //
}

go deeper

for a junior

Remember the two steps: generate with make:observer --model, then register with #[ObservedBy] or observe() in AppServiceProvider::boot.

for a middle

Explain that methods match events by exact name, that the observer is container-resolved so it can inject services, and why the stub's five methods are not the full list.

for a senior

Decide between the attribute and provider registration, and keep observers thin, delegating to services, so side effects stay testable and visible.

for a principal

Set a team rule for when model hooks are acceptable, since observers make writes trigger work that the calling code does not show.

## What an observer is An **observer** is a plain PHP class that groups the handlers for one Eloquent model's lifecycle events. Each public method is named after an event (`created`, `updated`, `deleted`, `saving`, `retrieved` and so on) and receives the affected model instance as its only argument. The observer does not extend or implement anything; Laravel links it to the model by method name. It is the tidy alternative to scattering `static::created(...)` closures through a model's `booted()` method once more than one or two hooks exist, or once the hooks need services. ## Generating one The `make:observer` Artisan command writes the class: ```bash php artisan make:observer ListingObserver --model=Listing ``` - The file lands in **`app/Observers/ListingObserver.php`**; Artisan creates the directory if it is missing. - With `--model` (short `-m`), the stub contains `created`, `updated`, `deleted`, `restored` and `forceDeleted` methods typed to the model. - Without `--model`, the class is empty. - `--force` (`-f`) overwrites an existing file. The generated methods are a starting set, not the full list. You may add `creating`, `updating`, `saving`, `saved`, `deleting`, `trashed`, `restoring`, `forceDeleting`, `retrieved` or `replicating` by adding a method with that name. Methods for events you do not care about can simply be deleted. ## Registering it Generating the class does nothing on its own; the observer must be attached to the model. Laravel 13 gives two supported ways: | Where | How | When it takes effect | |---|---|---| | On the model | `#[ObservedBy([ListingObserver::class])]` from `Illuminate\Database\Eloquent\Attributes\ObservedBy` | the first time the model class boots | | In a provider | `Listing::observe(ListingObserver::class)` inside `AppServiceProvider::boot()` | while the app boots | The attribute keeps the link visible next to the model, is repeatable, and accepts a list of classes. `observe()` also takes an array, and is useful when the model lives in a package you cannot edit. Registration works by method discovery: for every observable event name, if the observer has a method with that name, Laravel registers `ListingObserver@<event>` as a listener for that model's event. A misspelt method, such as `update` instead of `updated`, is silently never called. ## How the observer runs 1. A model method such as `save()` fires, for example, `updated`. 2. The dispatcher resolves `ListingObserver` **from the service container** at that moment. 3. It calls `updated($listing)` with the same model instance. Because the class is container-resolved, constructor injection works: an observer can type-hint a search-index client or a logger and receive it. Handlers run synchronously in the same request or job that saved the model, unless you opt into deferral (for example the `ShouldHandleEventsAfterCommit` interface, which delays the after-events until a surrounding transaction commits). ## What the handler sees The observer receives the **same model instance** that is being saved, not a copy, so what it can read depends on the event: - In `creating` and `updating`, attribute changes you make are still written by the pending INSERT or UPDATE. - In `created`, the model has its new primary key, so it is the first point where you can index or audit it by ID. - In `updated`, Laravel has already recorded the changes (so `wasChanged('asking_price')` works), but it has not yet synced the original attributes; `getOriginal('asking_price')` still returns the old price until the save completes, after the `saved` event. - In `deleted`, the row is gone (or soft-deleted), but the in-memory model still holds its attributes, which is what lets an observer remove the listing from a search index by ID. Keep each method small. A handler that grows business logic is better moved to a service the observer calls, so the same logic can be tested and reused without saving a model. ## Where this changed across versions - Applications created before Laravel 11 usually had an `app/Providers/EventServiceProvider.php` with a `$observers` array mapping models to observers. The framework's base `EventServiceProvider` still reads that property if you create one. - The Laravel 11 and later skeleton, including Laravel 13's, ships only `AppServiceProvider` in `bootstrap/providers.php`. The documented choices are therefore the `#[ObservedBy]` attribute or `observe()` in `AppServiceProvider::boot()`. - Observers are **not** picked up by event discovery. Discovery scans `app/Listeners` by default for listener classes; an observer in `app/Observers` that is never registered simply never runs. ## Common mistakes - Expecting `make:observer` to register the class. - Naming a method after the wrong tense (`update`, `delete`) so it never matches an event. - Registering in a provider's `register()` method instead of `boot()`: the model event dispatcher is set only when providers boot, so the call can silently attach nothing. - Assuming the observer sees query-builder writes such as `Listing::where(...)->update(...)`; those load no models and fire no model events.

  • Your ListingObserver has a method named update, and it never runs. Why?
    Registration loops over the observable event names and registers only methods whose names match one of them. The event is `updated` (or `updating`), so a method called `update` is never attached and nothing reports the mismatch. Rename it to `updated`, the post-write event, or `updating` if it must run before the UPDATE.
  • When would you register an Eloquent observer with observe() in a provider rather than with #[ObservedBy]?
    When you cannot or should not edit the model, such as a model shipped by a package, or when registration depends on configuration, for example attaching an audit observer only when a feature flag is on. Otherwise `#[ObservedBy]` keeps the link next to the model, where a reader looks first.

saying these in an interview costs you the question

  • make:observer registers the observer automatically
  • Observers must extend a base Observer class or implement an interface
  • Event discovery finds observers in app/Observers without registration
  • Observers go in the $listen array of EventServiceProvider
  • An observer method can have any name as long as it is public