Advanced Laravel

Advanced Eloquent: Events & Observers

18 min Lesson 2 of 40

Understanding Eloquent Events

Eloquent models fire several events throughout their lifecycle, allowing you to hook into various points in a model's lifecycle to execute custom logic. This is incredibly powerful for tasks like logging, sending notifications, or updating related data.

Available Model Events:
  • retrieved - After a model is retrieved from the database
  • creating - Before a new model is saved to the database
  • created - After a new model is saved to the database
  • updating - Before an existing model is updated
  • updated - After an existing model is updated
  • saving - Before a model is created or updated
  • saved - After a model is created or updated
  • deleting - Before a model is deleted
  • deleted - After a model is deleted
  • restoring - Before a soft-deleted model is restored
  • restored - After a soft-deleted model is restored
  • replicating - Before a model is replicated

Listening to Events in Models

The simplest way to listen to model events is using the boot() or booted() method in your model.

Basic Event Listeners:
// app/Models/Post.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Post extends Model
{
    protected static function booted()
    {
        // Generate slug when creating a new post
        static::creating(function ($post) {
            if (empty($post->slug)) {
                $post->slug = Str::slug($post->title);
            }
        });

        // Update slug when title changes
        static::updating(function ($post) {
            if ($post->isDirty('title')) {
                $post->slug = Str::slug($post->title);
            }
        });

        // Increment view count when post is retrieved
        static::retrieved(function ($post) {
            // Be careful with this - can cause performance issues
            // Better to use a queue or batch update
        });

        // Log when post is deleted
        static::deleted(function ($post) {
            \Log::info("Post deleted: {$post->title}");
        });
    }
}
Using Closures for Complex Logic:
// app/Models/Order.php
namespace App\Models;

use App\Notifications\OrderCreatedNotification;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected static function booted()
    {
        // Send notification when order is created
        static::created(function ($order) {
            $order->user->notify(new OrderCreatedNotification($order));
        });

        // Calculate totals before saving
        static::saving(function ($order) {
            $order->total = $order->items->sum(function ($item) {
                return $item->quantity * $item->price;
            });
            $order->tax = $order->total * 0.1; // 10% tax
            $order->grand_total = $order->total + $order->tax;
        });

        // Update inventory when order is completed
        static::updated(function ($order) {
            if ($order->wasChanged('status') && $order->status === 'completed') {
                foreach ($order->items as $item) {
                    $item->product->decrement('stock', $item->quantity);
                }
            }
        });

        // Prevent deletion of completed orders
        static::deleting(function ($order) {
            if ($order->status === 'completed') {
                return false; // Cancel the delete operation
            }
        });
    }
}
Event Listener Return Values: If an event listener returns false, the operation will be cancelled. This is useful for creating, updating, saving, and deleting events to prevent the action from completing.

Model Observers

For more complex event handling, Laravel provides observers. Observers are classes that contain methods corresponding to model events, keeping your model clean and separating concerns.

Creating an Observer:
// Generate an observer using artisan
php artisan make:observer PostObserver --model=Post

// app/Observers/PostObserver.php
namespace App\Observers;

use App\Models\Post;
use Illuminate\Support\Str;

class PostObserver
{
    /**
     * Handle the Post "creating" event.
     */
    public function creating(Post $post): void
    {
        // Auto-generate slug
        if (empty($post->slug)) {
            $post->slug = Str::slug($post->title);
        }

        // Set default author if not provided
        if (empty($post->author_id)) {
            $post->author_id = auth()->id();
        }
    }

    /**
     * Handle the Post "created" event.
     */
    public function created(Post $post): void
    {
        // Log the creation
        \Log::info("New post created: {$post->title}");

        // Send notification to followers
        $post->author->followers->each(function ($follower) use ($post) {
            $follower->notify(new \App\Notifications\NewPostPublished($post));
        });
    }

    /**
     * Handle the Post "updating" event.
     */
    public function updating(Post $post): void
    {
        // Update slug if title changed
        if ($post->isDirty('title')) {
            $post->slug = Str::slug($post->title);
        }

        // Record who made the update
        $post->last_updated_by = auth()->id();
    }

    /**
     * Handle the Post "updated" event.
     */
    public function updated(Post $post): void
    {
        // Clear cache when post is updated
        \Cache::forget("post.{$post->id}");
        \Cache::forget("posts.recent");

        // Send notification if published status changed
        if ($post->wasChanged('status') && $post->status === 'published') {
            $post->author->notify(new \App\Notifications\PostPublished($post));
        }
    }

    /**
     * Handle the Post "deleting" event.
     */
    public function deleting(Post $post): void
    {
        // Delete associated comments
        $post->comments()->delete();

        // Delete associated media
        $post->media()->each(function ($media) {
            \Storage::delete($media->path);
            $media->delete();
        });
    }

    /**
     * Handle the Post "deleted" event.
     */
    public function deleted(Post $post): void
    {
        // Clear cache
        \Cache::forget("post.{$post->id}");

        // Log deletion
        \Log::warning("Post deleted: {$post->title} by user " . auth()->id());
    }

    /**
     * Handle the Post "restored" event.
     */
    public function restored(Post $post): void
    {
        // Restore associated comments
        $post->comments()->restore();

        \Log::info("Post restored: {$post->title}");
    }
}
Registering an Observer:
// app/Providers/EventServiceProvider.php
namespace App\Providers;

use App\Models\Post;
use App\Observers\PostObserver;
use Illuminate\Support\ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * Register any events for your application.
     */
    public function boot(): void
    {
        // Register observer
        Post::observe(PostObserver::class);
    }
}

// Alternative: Register in AppServiceProvider
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Post::observe(PostObserver::class);
}
Observer Method Naming: Observer methods must exactly match the event names: retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored, replicating.

Advanced Observer Patterns

Observers can do much more than simple event handling. Here are some advanced patterns:

User Observer with Multiple Features:
// app/Observers/UserObserver.php
namespace App\Observers;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class UserObserver
{
    /**
     * Handle the User "creating" event.
     */
    public function creating(User $user): void
    {
        // Hash password if not already hashed
        if ($user->password && !Hash::needsRehash($user->password)) {
            $user->password = Hash::make($user->password);
        }

        // Generate unique username if not provided
        if (empty($user->username)) {
            $base = Str::slug($user->name);
            $username = $base;
            $counter = 1;

            while (User::where('username', $username)->exists()) {
                $username = $base . $counter++;
            }

            $user->username = $username;
        }

        // Generate API token
        $user->api_token = Str::random(80);
    }

    /**
     * Handle the User "created" event.
     */
    public function created(User $user): void
    {
        // Create default profile
        $user->profile()->create([
            'bio' => '',
            'avatar' => 'default-avatar.png',
        ]);

        // Create default settings
        $user->settings()->create([
            'notifications_enabled' => true,
            'email_frequency' => 'daily',
        ]);

        // Send welcome email
        $user->sendEmailVerificationNotification();

        // Log registration
        activity()
            ->performedOn($user)
            ->log('User registered');
    }

    /**
     * Handle the User "updating" event.
     */
    public function updating(User $user): void
    {
        // Re-hash password if changed
        if ($user->isDirty('password')) {
            $user->password = Hash::make($user->password);
        }

        // Verify email is unique if changed
        if ($user->isDirty('email')) {
            $user->email_verified_at = null;
        }
    }

    /**
     * Handle the User "deleting" event.
     */
    public function deleting(User $user): void
    {
        // Delete related data
        $user->posts()->delete();
        $user->comments()->delete();
        $user->profile()->delete();
        $user->settings()->delete();

        // Revoke API tokens
        $user->tokens()->delete();

        // Log deletion
        activity()
            ->performedOn($user)
            ->log('User account deleted');
    }
}

Dispatching Custom Events

Beyond built-in model events, you can dispatch custom events for specific business logic:

Creating Custom Events:
// app/Events/OrderShipped.php
namespace App\Events;

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

class OrderShipped
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Order $order
    ) {}
}

// app/Events/PaymentProcessed.php
namespace App\Events;

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

class PaymentProcessed
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Payment $payment,
        public bool $successful
    ) {}
}
Creating Event Listeners:
// app/Listeners/SendShipmentNotification.php
namespace App\Listeners;

use App\Events\OrderShipped;
use App\Notifications\OrderShippedNotification;

class SendShipmentNotification
{
    public function handle(OrderShipped $event): void
    {
        $event->order->user->notify(
            new OrderShippedNotification($event->order)
        );
    }
}

// app/Listeners/UpdateInventory.php
namespace App\Listeners;

use App\Events\OrderShipped;

class UpdateInventory
{
    public function handle(OrderShipped $event): void
    {
        foreach ($event->order->items as $item) {
            $item->product->decrement('stock', $item->quantity);
        }
    }
}
Registering Custom Events:
// app/Providers/EventServiceProvider.php
namespace App\Providers;

use App\Events\OrderShipped;
use App\Events\PaymentProcessed;
use App\Listeners\SendShipmentNotification;
use App\Listeners\UpdateInventory;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     */
    protected $listen = [
        OrderShipped::class => [
            SendShipmentNotification::class,
            UpdateInventory::class,
        ],
        PaymentProcessed::class => [
            // Add listeners here
        ],
    ];

    /**
     * Register any events for your application.
     */
    public function boot(): void
    {
        //
    }
}
Dispatching Custom Events:
// In your controller or service
use App\Events\OrderShipped;
use App\Models\Order;

public function shipOrder(Order $order)
{
    // Update order status
    $order->update([
        'status' => 'shipped',
        'shipped_at' => now(),
    ]);

    // Dispatch the event
    event(new OrderShipped($order));

    // Alternative syntax
    OrderShipped::dispatch($order);

    return response()->json(['message' => 'Order shipped successfully']);
}

Event Subscribers

Event subscribers allow you to subscribe to multiple events within a single class, perfect for related event handling:

Creating an Event Subscriber:
// app/Listeners/UserEventSubscriber.php
namespace App\Listeners;

use App\Events\UserRegistered;
use App\Events\UserLoggedIn;
use App\Events\UserProfileUpdated;
use Illuminate\Events\Dispatcher;

class UserEventSubscriber
{
    /**
     * Handle user registration events.
     */
    public function handleUserRegistration($event)
    {
        // Send welcome email
        $event->user->sendWelcomeEmail();

        // Create activity log
        activity()
            ->performedOn($event->user)
            ->log('User registered');
    }

    /**
     * Handle user login events.
     */
    public function handleUserLogin($event)
    {
        // Update last login timestamp
        $event->user->update([
            'last_login_at' => now(),
            'last_login_ip' => request()->ip(),
        ]);
    }

    /**
     * Handle user profile update events.
     */
    public function handleProfileUpdate($event)
    {
        // Clear cache
        \Cache::forget("user.{$event->user->id}");

        // Log the update
        activity()
            ->performedOn($event->user)
            ->log('Profile updated');
    }

    /**
     * Register the listeners for the subscriber.
     */
    public function subscribe(Dispatcher $events): void
    {
        $events->listen(
            UserRegistered::class,
            [UserEventSubscriber::class, 'handleUserRegistration']
        );

        $events->listen(
            UserLoggedIn::class,
            [UserEventSubscriber::class, 'handleUserLogin']
        );

        $events->listen(
            UserProfileUpdated::class,
            [UserEventSubscriber::class, 'handleProfileUpdate']
        );
    }
}
Registering Event Subscriber:
// app/Providers/EventServiceProvider.php
namespace App\Providers;

use App\Listeners\UserEventSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The subscriber classes to register.
     */
    protected $subscribe = [
        UserEventSubscriber::class,
    ];
}
Performance Consideration: Be careful with events that fire frequently (like retrieved or saving). Heavy operations in these listeners can significantly impact performance. Consider using queued listeners for time-consuming tasks.
Queued Event Listeners: Implement the ShouldQueue interface on your listener to process it asynchronously:
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(OrderShipped $event): void
    {
        // This will run in a queue
    }
}

Exercise 1: Create a Product Observer

Create a ProductObserver that:

  • Generates a unique SKU when creating a product (if not provided)
  • Automatically generates a slug from the product name
  • Logs when a product is created, updated, or deleted
  • Prevents deletion if the product has any orders
  • Clears relevant cache entries when product is updated

Exercise 2: Custom Event System

Create a custom event system for a blog:

  • Event: PostPublished - dispatched when a post status changes to "published"
  • Listener: NotifySubscribers - sends email to all blog subscribers
  • Listener: UpdateSitemap - regenerates the sitemap
  • Listener: ClearCache - clears post-related cache

Implement the event, listeners, registration, and dispatching logic.

Exercise 3: Event Subscriber

Create an OrderEventSubscriber that handles multiple order-related events:

  • OrderCreated - Send confirmation email, create invoice
  • OrderPaid - Update payment status, trigger fulfillment
  • OrderShipped - Send tracking email, update inventory
  • OrderCancelled - Refund payment, restore inventory

Implement the subscriber with all methods and register it properly.

Key Takeaways:
  • Model events fire automatically during the model lifecycle
  • Observers keep event handling organized and separated from models
  • Custom events allow you to implement domain-specific event systems
  • Event subscribers group related event handlers together
  • Use queued listeners for time-consuming operations
  • Return false from "before" events to cancel the operation