How Eloquent model events work, and how to use Observer classes to handle them in one place. Includes the #[ObservedBy] attribute introduced in Laravel 10 and used throughout Laravel 13.
Eloquent models automatically fire events at each stage of their lifecycle. You can hook into these events to run code before or after a model is saved, deleted, and so on.
Event
When it fires
retrieved
After a model is fetched from the database
creating
Before a new model is saved for the first time
created
After a new model is saved for the first time
updating
Before an existing model is saved
updated
After an existing model is saved
saving
Before either a create or an update
saved
After either a create or an update
deleting
Before a model is deleted
deleted
After a model is deleted
trashed
After a model is soft-deleted
forceDeleting
Before a model is permanently deleted
forceDeleted
After a model is permanently deleted
restoring
Before a soft-deleted model is restored
restored
After a soft-deleted model is restored
replicating
When replicate() is called on a model
Events ending in -ing fire before the change is persisted. Events ending in -ed fire after.
Mass updates and mass deletes (User::where(...)->update(...)) do not fire saving, saved, updating, updated, deleting, or deleted events because the models are never retrieved from the database.
For simple cases, register closures inside the model’s booted method.
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;class User extends Model{ protected static function booted(): void { static::created(function (User $user) { // Runs after the user is created }); static::deleting(function (User $user) { // Runs before the user is deleted }); }}
To run a closure on a queue instead, wrap it with the queueable helper.
use function Illuminate\Events\queueable;static::created(queueable(function (User $user) { // Runs asynchronously on a queue}));
Each method name corresponds to an event. The method receives the model instance.
<?phpnamespace App\Observers;use App\Models\User;class UserObserver{ public function created(User $user): void { // Runs after a user is created } public function updated(User $user): void { // Runs after a user is updated } public function deleted(User $user): void { // Runs after a user is deleted } public function restored(User $user): void { // Runs after a soft-deleted user is restored } public function forceDeleted(User $user): void { // Runs after a user is permanently deleted }}
3
Register the Observer
There are two ways to register an observer. The #[ObservedBy] attribute is preferred in Laravel 13.Option 1: #[ObservedBy] attribute (recommended)Add the attribute to the model class. No changes to AppServiceProvider are needed.
<?phpnamespace App\Models;use App\Observers\UserObserver;use Illuminate\Database\Eloquent\Attributes\ObservedBy;use Illuminate\Foundation\Auth\User as Authenticatable;#[ObservedBy(UserObserver::class)]class User extends Authenticatable{ //}
To attach multiple observers, repeat the attribute or pass an array.
#[ObservedBy(UserObserver::class)]#[ObservedBy(AuditObserver::class)]class User extends Authenticatable{ //}
Option 2: Register in AppServiceProvider
<?phpnamespace App\Providers;use App\Models\User;use App\Observers\UserObserver;use Illuminate\Support\ServiceProvider;class AppServiceProvider extends ServiceProvider{ public function boot(): void { User::observe(UserObserver::class); }}
The #[ObservedBy] attribute lives in the Illuminate\Database\Eloquent\Attributes namespace. It is a native PHP 8.0+ attribute and the approach encouraged in Laravel 13.
When a model is created or updated inside a database transaction, you may want the observer to run only after the transaction commits successfully. Implement ShouldHandleEventsAfterCommit to get this behavior.
<?phpnamespace App\Observers;use App\Models\User;use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;class UserObserver implements ShouldHandleEventsAfterCommit{ public function created(User $user): void { // Runs after the transaction commits }}
If the event fires outside a transaction, it runs immediately as normal.
The updating event fires before the record is saved to the database, so getDirty() contains the pending changes. Calling getDirty() inside an updated listener will return an empty array because the model has already been synced.