Sitemap

Mastering Laravel Model Observers: Observer Classes, Model Boot Methods, and Beyond

9 min readMar 14, 2025

--

Press enter or click to view image in full size
Photo by Pixabay: https://www.pexels.com/photo/red-theodolite-under-blue-sky-and-white-clouds-221538/

Laravel provides several elegant ways to handle model events in your application. When you need to perform certain actions when a model is created, updated, or deleted, you have multiple options at your disposal. In this comprehensive guide, we’ll explore Laravel Model Observers and other approaches to handling model events efficiently.

Introduction to Model Observers

What Are Model Observers?

Model Observers in Laravel are dedicated classes that allow you to listen for and react to various lifecycle events on Eloquent models. These events occur during the normal operation of your application as models are created, retrieved, updated, and deleted.

Eloquent models dispatch several events during their lifecycle, including:

  • retrieved: After a record has been retrieved from the database
  • creating / created: Before/after a record is created
  • updating / updated: Before/after a record is updated
  • saving / saved: Before/after a record is saved (created or updated)
  • deleting / deleted: Before/after a record is deleted
  • restoring / restored: Before/after a soft-deleted record is restored
  • replicating: When a model is being replicated

Why Use Model Observers?

Model Observers provide significant benefits when working with Eloquent models:

  1. Clean, Organized Code: They separate event handling logic from your model definitions, keeping your codebase organized.
  2. DRY Principle: Instead of duplicating event handling code across controllers or other parts of your application, centralize it in observers.
  3. Simplified Testing: Observer classes can be unit tested in isolation.
  4. Maintainability: When event handling logic needs to change, you only need to update it in one place.

For example, you might use an observer to automatically send a welcome email when a user is created, update search indices when a product is modified, or clean up related resources when a post is deleted.

Using Observer Classes

Creating and Registering Observer Classes

Laravel provides an Artisan command to generate observer classes:

php artisan make:observer UserObserver --model=User

This command creates an observer class with method stubs for all the common model events.

Here’s an example of a UserObserver class:

<?php

namespace App\Observers;

use App\Models\User;
use App\Services\UserActivityLogger;

class UserObserver
{
protected $logger;

public function __construct(UserActivityLogger $logger)
{
$this->logger = $logger;
}

/**
* Handle the User "creating" event.
*/
public function creating(User $user): void
{
// Set default values or validate before creation
if (empty($user->referral_code)) {
$user->referral_code = \Str::random(10);
}
}

/**
* Handle the User "created" event.
*/
public function created(User $user): void
{
// Send welcome email
\Mail::to($user->email)->send(new \App\Mail\WelcomeEmail($user));

// Log the activity
$this->logger->log("User {$user->name} was created");
}

/**
* Handle the User "updating" event.
*/
public function updating(User $user): void
{
// Check if email is being changed and require reverification
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
}

/**
* Handle the User "updated" event.
*/
public function updated(User $user): void
{
$this->logger->log("User {$user->name} was updated");
}

/**
* Handle the User "deleted" event.
*/
public function deleted(User $user): void
{
// Clean up user resources
$user->posts()->delete();
$user->comments()->delete();

$this->logger->log("User {$user->name} was deleted");
}
}

Registering the Observer

You need to register your observer with the model in a service provider. Typically, this is done in the boot method of your AppServiceProvider:

<?php
namespace App\Providers;

use App\Models\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
User::observe(UserObserver::class);
}
}

Alternatively, you can register observers in your EventServiceProvider:

<?php

namespace App\Providers;

use App\Models\User;
use App\Observers\UserObserver;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
User::observe(UserObserver::class);
}
}

Alternatively, you can register observers in your Model (this only works on laravel 11 or above):

use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
//
}

Using Model Boot Method

Defining Event Handlers in the Boot Method

Instead of creating a separate observer class, you can define event handlers directly within the model’s boot method:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
// ... other model code ...

/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::creating(function (Product $product) {
if (empty($product->slug)) {
$product->slug = \Str::slug($product->name);
}
});

static::updating(function (Product $product) {
if ($product->isDirty('name') && !$product->isDirty('slug')) {
$product->slug = \Str::slug($product->name);
}
});

static::deleted(function (Product $product) {
// Clean up associated images
$product->images()->delete();

// Clear cache
\Cache::forget("product_{$product->id}");
});
}
}

When to Use the Boot Method Approach

Using the booted method directly in the model is ideal for:

  1. Simple Event Handling: When the logic for handling events is straightforward.
  2. Model-Specific Logic: When the event handling is intrinsically tied to the model itself.
  3. Small Projects: For smaller applications where creating separate observer classes might be overkill.

Here’s a comparison between using an observer class and the boot method:

Using an Observer Class:

// ProductObserver.php
public function creating(Product $product): void
{
if (empty($product->slug)) {
$product->slug = \Str::slug($product->name);
}
}
// AppServiceProvider.php
public function boot(): void
{
Product::observe(ProductObserver::class);
}

Using the Model Boot Method:

// Product.php
protected static function booted(): void
{
static::creating(function (Product $product) {
if (empty($product->slug)) {
$product->slug = \Str::slug($product->name);
}
});
}

The boot method approach is more concise for simple cases but can lead to bloated model classes if the event handling logic becomes complex.

Global Model Events in Service Providers

Listening to Model Events Globally

Laravel allows you to listen for model events globally in your service providers using the Event facade:

<?php

namespace App\Providers;

use Illuminate\Database\Events\MigrationsStarted;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Model;

class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Listen for any model being saved
Event::listen('eloquent.saving: *', function ($event, $models) {
foreach ($models as $model) {
// Perform action on any model being saved
\Log::info(get_class($model) . ' model is being saved');
}
});

// Listen for specific model events
Event::listen('eloquent.created: App\Models\Post', function ($event, $models) {
foreach ($models as $model) {
\Cache::forget('all-posts');
}
});
}
}

You can also observe multiple models at once:

use App\Models\Post;
use App\Models\Comment;
use App\Observers\ActivityObserver;

// In a service provider
public function boot(): void
{
// Apply the same observer to multiple models
foreach ([Post::class, Comment::class] as $model) {
$model::observe(ActivityObserver::class);
}
}

Using Model::observe() Globally

You can create more generic observers for multiple models:

<?php

namespace App\Observers;

use Illuminate\Database\Eloquent\Model;

class AuditObserver
{
public function created(Model $model): void
{
$this->logActivity($model, 'created');
}

public function updated(Model $model): void
{
$this->logActivity($model, 'updated');
}

public function deleted(Model $model): void
{
$this->logActivity($model, 'deleted');
}

protected function logActivity(Model $model, string $action): void
{
// Log changes to the audit trail
\App\Models\AuditLog::create([
'model_type' => get_class($model),
'model_id' => $model->getKey(),
'action' => $action,
'user_id' => auth()->id() ?? null,
'changes' => $action === 'updated' ? $model->getDirty() : $model->getAttributes(),
]);
}
}

Then register it for multiple models:

// In a service provider
public function boot(): void
{
$auditableModels = [
\App\Models\User::class,
\App\Models\Post::class,
\App\Models\Comment::class,
];
foreach ($auditableModels as $model) {
$model::observe(AuditObserver::class);
}
}

Handling Complex Logic with Jobs and Listeners

Offloading Observer Logic to Jobs

For observers that might perform time-consuming operations, it’s better to offload the work to queued jobs:

<?php

namespace App\Observers;

use App\Jobs\GenerateThumbnails;
use App\Jobs\UpdateSearchIndex;
use App\Models\Product;

class ProductObserver
{
/**
* Handle the Product "created" event.
*/
public function created(Product $product): void
{
// Dispatch jobs to handle resource-intensive tasks
GenerateThumbnails::dispatch($product);
UpdateSearchIndex::dispatch($product);
}

/**
* Handle the Product "updated" event.
*/
public function updated(Product $product): void
{
if ($product->wasChanged('name', 'description', 'category_id')) {
UpdateSearchIndex::dispatch($product);
}

if ($product->wasChanged('image')) {
GenerateThumbnails::dispatch($product);
}
}
}

The job implementation might look like:

<?php

namespace App\Jobs;

use App\Models\Product;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class UpdateSearchIndex implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $product;
/**
* Create a new job instance.
*/
public function __construct(Product $product)
{
$this->product = $product;
}
/**
* Execute the job.
*/
public function handle(): void
{
// Update search index with product information
app('search')->update($this->product);
}
}

Using Event Listeners

An alternative approach is to use Laravel’s event/listener system by defining dedicated events and listeners:

Get Zulfikar Ditya’s stories in your inbox

Join Medium for free to get updates from this writer.

First, define the event:

<?php

namespace App\Events;

use App\Models\Post;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PostPublished
{
use Dispatchable, SerializesModels;

public $post;
/**
* Create a new event instance.
*/
public function __construct(Post $post)
{
$this->post = $post;
}
}

Then create a listener:

<?php

namespace App\Listeners;

use App\Events\PostPublished;
use Illuminate\Contracts\Queue\ShouldQueue;

class NotifySubscribers implements ShouldQueue
{
/**
* Handle the event.
*/
public function handle(PostPublished $event): void
{
$subscribers = $event->post->author->subscribers;

foreach ($subscribers as $subscriber) {
\Mail::to($subscriber->email)
->send(new \App\Mail\NewPostNotification($event->post));
}
}
}

Register the event and listener in your EventServiceProvider:

<?php

namespace App\Providers;

use App\Events\PostPublished;
use App\Listeners\NotifySubscribers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
PostPublished::class => [
NotifySubscribers::class,
],
];
}

And finally, dispatch the event from your model observer:

<?php

namespace App\Observers;

use App\Events\PostPublished;
use App\Models\Post;

class PostObserver
{
/**
* Handle the Post "updated" event.
*/
public function updated(Post $post): void
{
// If post was just published
if ($post->wasChanged('status') && $post->status === 'published') {
PostPublished::dispatch($post);
}
}
}

Best Practices and When to Use Each Approach

Comparing the Different Approaches

Observer Classes

Advantages:

  • Separate, focused classes for event handling
  • Easy to test in isolation
  • Good for complex logic or handling multiple events
  • Support dependency injection

Best for:

  • Large applications with complex model event handling
  • When logic needs to be reused across several models
  • Teams where clear separation of concerns is important

Model Boot Method

Advantages:

  • Simple to implement
  • Keeps model-related logic in the model itself
  • No need for additional files for simple cases

Best for:

  • Simple event handling logic
  • Smaller applications
  • When the logic is intrinsic to the model itself

Global Event Listeners

Advantages:

  • Can listen for events across multiple models
  • More flexible for advanced patterns
  • Can be used for system-wide concerns

Best for:

  • Cross-cutting concerns like logging or auditing
  • When you need to handle events from multiple models in the same way

Performance Considerations

— Observer Method vs Boot Method:

  • Performance-wise, both approaches are similar as they use the same underlying event system
  • The choice between them should be based on code organization, not performance

— Queued Jobs for Intensive Tasks:

  • Offload resource-intensive tasks (like sending emails, image processing, or API calls) to queued jobs
  • This prevents model operations from being slow and improves user experience

— Event Listener vs. Observer:

  • For simple synchronous operations, observers are fine
  • For complex operations that can be run asynchronously, use event listeners that implement ShouldQueue

Maintainability Guidelines

— Keep Observers Focused:

  • Each observer should have a clear responsibility
  • If an observer grows too large, consider splitting it into multiple observers or using the event/listener approach

— Documentation:

  • Document the purpose of each observer and what it handles
  • This is especially important since the effects of observers might not be immediately obvious when looking at controller code

— Testing:

  • Write tests specifically for your observers
  • Test that the right actions happen when model events are triggered

— Avoid Infinite Loops:

  • Be careful when updating models in their own observers, as this can trigger additional events
  • Use $model->withoutEvents(function () { /* code */ }) when needed to prevent this

Conclusion

Laravel Model Observers provide a powerful way to handle model events in your application. Whether you choose to use dedicated observer classes, the model boot method, or global event listeners depends on your specific requirements and the complexity of your application.

For simple applications or straightforward logic, the model boot method may be sufficient. As your application grows, observer classes offer better organization and maintainability. For complex logic or resource-intensive operations, consider using the event/listener system with queued jobs.

By understanding the strengths and appropriate use cases for each approach, you can create more maintainable Laravel applications with cleaner, more modular code. The key is to choose the right tool for the job and follow best practices regarding code organization and performance considerations.

Remember that the goal is to keep your controllers thin and your models focused on their primary responsibility, with observers and event listeners helping to manage secondary effects and side operations when models change.

--

--

Zulfikar Ditya
Zulfikar Ditya

Professional Backend Developer. Use Laravel mostly but sometimes use node. Available for any opportunities.