Laravel Observers provide an elegant way to listen to Eloquent model events and execute code automatically when specific actions occur on your models. Think of observers as specialized event listeners that watch your models and react to changes like creation, updates, or deletions. This pattern helps you keep your models clean while centralizing related business logic in dedicated classes.
What Are Laravel Observers and Why Should You Use Them?
Imagine you have an e-commerce application where every time a user places an order, you need to send a confirmation email, update inventory levels, and log the transaction. Without observers, you might scatter this logic across your controllers, making your code harder to maintain and test. Observers solve this problem by providing a centralized location for model-related side effects.
Observers are particularly powerful because they automatically trigger based on Eloquent model events. Laravel fires these events during the model lifecycle: creating
, created
, updating
, updated
, saving
, saved
, deleting
, deleted
, restoring
, and restored
. Each event represents a different moment in your model's journey through the application.
Creating Your First Observer
Let's start with a practical example. Suppose you're building a blog application where you want to automatically generate slugs for posts and send notifications when articles are published. Here's how you would create an observer for your Post
model:
<?php
namespace App\Observers;
use App\Models\Post;
use App\Notifications\PostPublishedNotification;
use Illuminate\Support\Str;
class PostObserver
{
public function creating(Post $post): void
{
if (empty($post->slug)) {
$post->slug = Str::slug($post->title);
// Ensure slug uniqueness
$originalSlug = $post->slug;
$counter = 1;
while (Post::where('slug', $post->slug)->exists()) {
$post->slug = $originalSlug . '-' . $counter;
$counter++;
}
}
}
/**
* Handle the Post "created" event.
* This runs after the model has been successfully saved
*/
public function created(Post $post): void
{
// Log the creation for analytics
logger('New post created', [
'post_id' => $post->id,
'title' => $post->title,
'author_id' => $post->user_id
]);
}
/**
* Handle the Post "updated" event.
*/
public function updated(Post $post): void
{
// Check if the post was just published
if ($post->wasChanged('published_at') && $post->published_at !== null) {
// Send notification to subscribers
$post->author->notify(new PostPublishedNotification($post));
// Update search index or cache
$this->updateSearchIndex($post);
}
}
/**
* Handle the Post "deleting" event.
* This runs before the model is deleted
*/
public function deleting(Post $post): void
{
// Clean up related data before deletion
$post->comments()->delete();
$post->tags()->detach();
// Remove from search index
$this->removeFromSearchIndex($post);
}
/**
* Handle the Post "deleted" event.
*/
public function deleted(Post $post): void
{
// Log the deletion for audit purposes
logger('Post deleted', [
'post_id' => $post->id,
'title' => $post->title,
'deleted_by' => auth()->id()
]);
}
/**
* Helper method to update search index
*/
private function updateSearchIndex(Post $post): void
{
// Implementation would depend on your search solution
// This could be Elasticsearch, Algolia, etc.
}
/**
* Helper method to remove from search index
*/
private function removeFromSearchIndex(Post $post): void
{
// Remove from search index implementation
}
}
To generate this observer using Artisan, you would run:
php artisan make:observer PostObserver --model=Post
This command creates the observer class with method stubs for all the common Eloquent events.
Registering Observers
Once you've created your observer, you need to register it with Laravel. The most common approach is to register observers in your AppServiceProvider
or create a dedicated service provider. Here's how to do it in your AppServiceProvider
:
<?php
namespace App\Providers;
use App\Models\Post;
use App\Models\User;
use App\Models\Order;
use App\Observers\PostObserver;
use App\Observers\UserObserver;
use App\Observers\OrderObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Register model observers
Post::observe(PostObserver::class);
User::observe(UserObserver::class);
Order::observe(OrderObserver::class);
}
}
For larger applications, you might want to create a dedicated ObserverServiceProvider
:
<?php
namespace App\Providers;
use App\Models\Post;
use App\Models\User;
use App\Models\Order;
use App\Observers\PostObserver;
use App\Observers\UserObserver;
use App\Observers\OrderObserver;
use Illuminate\Support\ServiceProvider;
class ObserverServiceProvider extends ServiceProvider
{
/**
* The model observers for your application.
*
* @var array
*/
protected $observers = [
Post::class => PostObserver::class,
User::class => UserObserver::class,
Order::class => OrderObserver::class,
];
/**
* Register the observers.
*/
public function boot(): void
{
foreach ($this->observers as $model => $observer) {
$model::observe($observer);
}
}
}
Don't forget to register this service provider in your config/app.php
file:
'providers' => [
// Other providers...
App\Providers\ObserverServiceProvider::class,
],
Real-World Example: E-commerce Order Processing
Let's explore a more complex example that demonstrates the power of observers in an e-commerce context. This OrderObserver
handles the entire lifecycle of an order:
<?php
namespace App\Observers;
use App\Models\Order;
use App\Services\InventoryService;
use App\Services\EmailService;
use App\Services\PaymentService;
use App\Notifications\OrderConfirmationNotification;
use App\Notifications\OrderShippedNotification;
use App\Jobs\ProcessRefund;
class OrderObserver
{
protected $inventoryService;
protected $emailService;
protected $paymentService;
public function __construct(
InventoryService $inventoryService,
EmailService $emailService,
PaymentService $paymentService
) {
$this->inventoryService = $inventoryService;
$this->emailService = $emailService;
$this->paymentService = $paymentService;
}
/**
* Handle the Order "created" event.
* This fires after a new order is successfully created
*/
public function created(Order $order): void
{
// Reserve inventory for the order items
foreach ($order->items as $item) {
$this->inventoryService->reserve($item->product_id, $item->quantity);
}
// Send order confirmation email
$order->customer->notify(new OrderConfirmationNotification($order));
// Create audit log
activity()
->performedOn($order)
->log('Order created with total: ' . $order->total_amount);
}
/**
* Handle the Order "updated" event.
* This allows us to react to status changes
*/
public function updated(Order $order): void
{
// Check if order status changed to 'shipped'
if ($order->wasChanged('status') && $order->status === 'shipped') {
$this->handleOrderShipped($order);
}
// Check if order was cancelled
if ($order->wasChanged('status') && $order->status === 'cancelled') {
$this->handleOrderCancelled($order);
}
// Check if payment status changed to 'failed'
if ($order->wasChanged('payment_status') && $order->payment_status === 'failed') {
$this->handlePaymentFailed($order);
}
}
/**
* Handle the Order "deleting" event.
* Clean up before order deletion
*/
public function deleting(Order $order): void
{
// Release reserved inventory
foreach ($order->items as $item) {
$this->inventoryService->release($item->product_id, $item->quantity);
}
// Process refund if payment was successful
if ($order->payment_status === 'completed') {
ProcessRefund::dispatch($order);
}
}
/**
* Handle order shipped logic
*/
private function handleOrderShipped(Order $order): void
{
// Send shipping notification
$order->customer->notify(new OrderShippedNotification($order));
// Update inventory (convert reservation to actual sale)
foreach ($order->items as $item) {
$this->inventoryService->confirmSale($item->product_id, $item->quantity);
}
// Log shipping event
activity()
->performedOn($order)
->log('Order shipped with tracking number: ' . $order->tracking_number);
}
/**
* Handle order cancellation logic
*/
private function handleOrderCancelled(Order $order): void
{
// Release inventory reservations
foreach ($order->items as $item) {
$this->inventoryService->release($item->product_id, $item->quantity);
}
// Process refund if needed
if (in_array($order->payment_status, ['completed', 'processing'])) {
ProcessRefund::dispatch($order);
}
// Send cancellation email
$this->emailService->sendOrderCancellationEmail($order);
}
/**
* Handle payment failure
*/
private function handlePaymentFailed(Order $order): void
{
// Release inventory since payment failed
foreach ($order->items as $item) {
$this->inventoryService->release($item->product_id, $item->quantity);
}
// Update order status
$order->update(['status' => 'payment_failed']);
// Notify customer
$this->emailService->sendPaymentFailedEmail($order);
}
}
Advanced Observer Techniques
Conditional Observer Logic
Sometimes you want observer methods to run only under certain conditions. Here's how you can implement conditional logic:
<?php
namespace App\Observers;
use App\Models\User;
use App\Services\CacheService;
use App\Jobs\SendWelcomeEmail;
class UserObserver
{
/**
* Handle the User "created" event.
*/
public function created(User $user): void
{
// Only send welcome email for verified users
if ($user->email_verified_at !== null) {
SendWelcomeEmail::dispatch($user);
}
// Update user statistics cache
CacheService::increment('total_users');
}
/**
* Handle the User "updated" event.
*/
public function updated(User $user): void
{
// Clear user cache when profile is updated
if ($user->wasChanged(['name', 'email', 'avatar'])) {
cache()->forget("user.{$user->id}");
}
// Handle email verification
if ($user->wasChanged('email_verified_at') && $user->email_verified_at !== null) {
$this->handleEmailVerified($user);
}
// Handle subscription changes
if ($user->wasChanged('subscription_tier')) {
$this->handleSubscriptionChange($user);
}
}
/**
* Handle email verification completion
*/
private function handleEmailVerified(User $user): void
{
// Send welcome email now that email is verified
SendWelcomeEmail::dispatch($user);
// Unlock premium features trial
$user->update([
'trial_ends_at' => now()->addDays(14)
]);
// Log verification for analytics
activity()
->performedOn($user)
->log('Email verified');
}
/**
* Handle subscription tier changes
*/
private function handleSubscriptionChange(User $user): void
{
$oldTier = $user->getOriginal('subscription_tier');
$newTier = $user->subscription_tier;
// Log the change
activity()
->performedOn($user)
->log("Subscription changed from {$oldTier} to {$newTier}");
// Update user permissions based on new tier
$user->syncPermissions($this->getPermissionsForTier($newTier));
// Clear cached user data
cache()->tags(['user', $user->id])->flush();
}
/**
* Get permissions for subscription tier
*/
private function getPermissionsForTier(string $tier): array
{
return match ($tier) {
'basic' => ['read_posts', 'create_comments'],
'premium' => ['read_posts', 'create_comments', 'create_posts'],
'enterprise' => ['read_posts', 'create_comments', 'create_posts', 'manage_users'],
default => ['read_posts'],
};
}
}
Working with Observer Dependencies
Observers can use dependency injection just like controllers and other Laravel classes. This makes them highly testable and flexible:
<?php
namespace App\Observers;
use App\Models\Product;
use App\Services\SearchService;
use App\Services\CacheService;
use App\Services\ImageOptimizationService;
use Illuminate\Contracts\Queue\ShouldQueue;
class ProductObserver implements ShouldQueue
{
protected $searchService;
protected $cacheService;
protected $imageService;
public function __construct(
SearchService $searchService,
CacheService $cacheService,
ImageOptimizationService $imageService
) {
$this->searchService = $searchService;
$this->cacheService = $cacheService;
$this->imageService = $imageService;
}
/**
* Handle the Product "created" event.
*/
public function created(Product $product): void
{
// Add to search index
$this->searchService->index($product);
// Optimize product images
if ($product->images->isNotEmpty()) {
foreach ($product->images as $image) {
$this->imageService->optimize($image->path);
}
}
// Clear category cache
$this->cacheService->clearCategoryCache($product->category_id);
}
/**
* Handle the Product "updated" event.
*/
public function updated(Product $product): void
{
// Update search index if searchable fields changed
if ($product->wasChanged(['name', 'description', 'tags'])) {
$this->searchService->update($product);
}
// Clear related caches
if ($product->wasChanged(['price', 'stock_quantity', 'is_active'])) {
$this->cacheService->clearProductCache($product->id);
$this->cacheService->clearCategoryCache($product->category_id);
}
}
/**
* Handle the Product "deleted" event.
*/
public function deleted(Product $product): void
{
// Remove from search index
$this->searchService->remove($product->id);
// Clear all related caches
$this->cacheService->clearProductCache($product->id);
$this->cacheService->clearCategoryCache($product->category_id);
}
}
Best Practices and Common Pitfalls
Performance Considerations
Observers run synchronously by default, which means they can slow down your application if they perform heavy operations. For time-consuming tasks, consider queuing them:
<?php
namespace App\Observers;
use App\Models\Order;
use App\Jobs\ProcessOrderAnalytics;
use App\Jobs\UpdateRecommendations;
use Illuminate\Contracts\Queue\ShouldQueue;
class OrderObserver implements ShouldQueue
{
/**
* Handle the Order "created" event.
*/
public function created(Order $order): void
{
// Quick operations can run immediately
$order->update(['order_number' => $this->generateOrderNumber()]);
// Heavy operations should be queued
ProcessOrderAnalytics::dispatch($order);
UpdateRecommendations::dispatch($order->customer);
}
private function generateOrderNumber(): string
{
return 'ORD-' . now()->format('Ymd') . '-' . str_pad(Order::count() + 1, 4, '0', STR_PAD_LEFT);
}
}
Avoiding Infinite Loops
Be careful when updating models within observers, as this can trigger additional observer events and create infinite loops:
<?php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
/**
* Handle the User "updated" event.
*/
public function updated(User $user): void
{
// BAD: This could create an infinite loop
// $user->update(['last_activity' => now()]);
// GOOD: Use saveQuietly or update database directly
$user->saveQuietly();
// OR use query builder to avoid triggering events
User::where('id', $user->id)->update(['last_activity' => now()]);
}
}
If you need to update the model within an observer, use the saveQuietly()
method or direct database queries to prevent triggering additional events.
Error Handling
Always implement proper error handling in your observers to prevent them from breaking the main application flow:
<?php
namespace App\Observers;
use App\Models\Post;
use Exception;
use Illuminate\Support\Facades\Log;
class PostObserver
{
/**
* Handle the Post "created" event.
*/
public function created(Post $post): void
{
try {
// Attempt to perform observer logic
$this->updateSearchIndex($post);
$this->sendNotifications($post);
} catch (Exception $e) {
// Log the error but don't break the main flow
Log::error('Post observer failed', [
'post_id' => $post->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Optionally, queue a retry job
// RetryPostProcessing::dispatch($post)->delay(now()->addMinutes(5));
}
}
}
Conclusion
Laravel Observers provide a powerful and elegant way to respond to model events in your application. They help you maintain clean, organized code by centralizing model-related side effects and business logic. By understanding the different event types, implementing proper error handling, and following best practices around performance and testing, you can leverage observers to build more maintainable and robust Laravel applications.
Remember that observers are just one tool in your Laravel toolkit. Use them when you need to respond to model events consistently across your application, but don't feel obligated to use them for every piece of model-related logic. Sometimes a simple method call in your controller or service class might be more appropriate, especially for one-off operations or complex business logic that doesn't directly relate to the model lifecycle.
As you continue building Laravel applications, you'll find that observers become an invaluable pattern for keeping your code organized and your models focused on their primary responsibilities while handling the side effects of data changes in a clean, testable way.
Top comments (0)