skip to content

Lifecycle Events & Observers

The creating, saving, updated and deleted hooks a model fires, observer classes and booted() closures that handle them, and saveQuietly. Interviewers ask why a mass update fired no observer.

on this pageshow

explore

questions

5

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
open as a page

An Eloquent observer syncs listings to a search index and audit log, yet Listing::where('status', 'pending')->update([...]) triggered neither; why, and how do you fix it?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Builder::update() runs one UPDATE and returns the affected-row count; no Listing models are loaded, so saving, updating, updated and saved never fire. Load and save each model, or keep the bulk write and reindex and audit the affected IDs explicitly.

open as a page

For Eloquent model events, when would you choose booted() closures, an observer class, or a $dispatchesEvents map, and why?

level: middleimportance: should knowfreq 40%

basics

~20 s

Use booted() closures for one or two small hooks that belong to the model, an observer when there are several handlers or they need injected services, and $dispatchesEvents when the model moment should become an event class that other listeners handle.

open as a page

With Eloquent, in what order do a model's lifecycle events fire on insert, update and delete, and how can a handler cancel the write?

level: middleimportance: should knowfreq 46%

basics

~20 s

Insert fires saving, creating, created, saved; update fires saving, updating, updated, saved; delete fires deleting then deleted. Returning false from a before-event such as saving or deleting cancels the write, and save() or delete() returns false.

open as a page

In Eloquent, what do saveQuietly() and Model::withoutEvents() suppress, and what surprises teams that rely on them?

level: middleimportance: should knowfreq 34%

basics

~20 s

saveQuietly() saves one model with Eloquent events muted; Model::withoutEvents() runs a closure with them muted. Observers, booted() closures and $dispatchesEvents are all skipped, timestamps are still written, and the muting covers every model class, not only the one named.

open as a page