Advanced Eloquent: Events & Observers
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.
retrieved- After a model is retrieved from the databasecreating- Before a new model is saved to the databasecreated- After a new model is saved to the databaseupdating- Before an existing model is updatedupdated- After an existing model is updatedsaving- Before a model is created or updatedsaved- After a model is created or updateddeleting- Before a model is deleteddeleted- After a model is deletedrestoring- Before a soft-deleted model is restoredrestored- After a soft-deleted model is restoredreplicating- 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.
// 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}");
});
}
}
// 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
}
});
}
}
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.
// 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}");
}
}
// 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);
}
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:
// 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:
// 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
) {}
}
// 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);
}
}
}
// 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
{
//
}
}
// 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:
// 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']
);
}
}
// 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,
];
}
retrieved or saving). Heavy operations in these listeners can significantly impact performance. Consider using queued listeners for time-consuming tasks.
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 invoiceOrderPaid- Update payment status, trigger fulfillmentOrderShipped- Send tracking email, update inventoryOrderCancelled- Refund payment, restore inventory
Implement the subscriber with all methods and register it properly.
- 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
falsefrom "before" events to cancel the operation