Could we help you? Please click the banners. We are young and desperately need the money
If you're building a Laravel application, you've probably written code like this: a user registers, and suddenly your controller is sending emails, logging activities, updating analytics, notifying admins, and creating audit trails—all in one method.
That controller becomes a tangled mess. It's doing too much, it's hard to test, and every time you need to add another action, you're editing the same file.
Laravel events solve this problem elegantly. They let you decouple your application logic by separating "what happened" from "what to do about it."
In this guide, I'll show you how to create custom events, dispatch them, listen to them, and—most importantly—when to use them to keep your codebase clean and maintainable.
Events are a way to implement the Observer pattern in Laravel. When something significant happens in your application (a product is created, an order is placed, a user logs in), you dispatch an event. Various parts of your application can listen for that event and respond accordingly.
Think of it like a notification system within your code:
Before diving into code, let's understand when events make sense.
Let's build a practical example: a product management system where creating a product triggers several actions.
php artisan make:event ProductCreated
This creates app/Events/ProductCreated.php:
<?php
namespace App\Events;
use App\Models\Product;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProductCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public Product $product;
public function __construct(Product $product)
{
$this->product = $product;
}
}
ProductCreated::dispatch($product)Now create listeners for the actions you want to take when a product is created.
# Send notification to admin
php artisan make:listener SendProductCreatedNotification --event=ProductCreated
# Update the product cache
php artisan make:listener UpdateProductCache --event=ProductCreated
# Log the activity
php artisan make:listener LogProductActivity --event=ProductCreated
File: app/Listeners/SendProductCreatedNotification.php
<?php
namespace App\Listeners;
use App\Events\ProductCreated;
use App\Notifications\NewProductCreated;
use App\Models\User;
class SendProductCreatedNotification
{
public function handle(ProductCreated $event): void
{
$product = $event->product;
// Notify all admin users
$admins = User::where('role', 'admin')->get();
foreach ($admins as $admin) {
$admin->notify(new NewProductCreated($product));
}
}
}
File: app/Listeners/UpdateProductCache.php
<?php
namespace App\Listeners;
use App\Events\ProductCreated;
use Illuminate\Support\Facades\Cache;
class UpdateProductCache
{
public function handle(ProductCreated $event): void
{
// Clear the products cache so fresh data is fetched
Cache::forget('products.all');
Cache::forget('products.featured');
Cache::forget('products.category.' . $event->product->category_id);
}
}
File: app/Listeners/LogProductActivity.php
<?php
namespace App\Listeners;
use App\Events\ProductCreated;
use Illuminate\Support\Facades\Log;
class LogProductActivity
{
public function handle(ProductCreated $event): void
{
Log::info('Product created', [
'product_id' => $event->product->id,
'product_name' => $event->product->name,
'user_id' => auth()->id(),
'timestamp' => now()
]);
}
}
In app/Providers/EventServiceProvider.php:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use App\Events\ProductCreated;
use App\Listeners\SendProductCreatedNotification;
use App\Listeners\UpdateProductCache;
use App\Listeners\LogProductActivity;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
ProductCreated::class => [
SendProductCreatedNotification::class,
UpdateProductCache::class,
LogProductActivity::class,
],
];
public function boot(): void
{
//
}
}
Now that your event and listeners are set up, it's time to dispatch the event when a product is created.
use App\Events\ProductCreated;
$product = Product::create($request->validated());
ProductCreated::dispatch($product);
$product = Product::create($request->validated());
event(new ProductCreated($product));
use Illuminate\Support\Facades\Event;
$product = Product::create($request->validated());
Event::dispatch(new ProductCreated($product));
Here's a complete example in a controller:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Events\ProductCreated;
use App\Http\Requests\StoreProductRequest;
class ProductController extends Controller
{
public function store(StoreProductRequest $request)
{
$product = Product::create([
'name' => $request->name,
'description' => $request->description,
'price' => $request->price,
'category_id' => $request->category_id,
]);
// Dispatch the event - all listeners will be notified
ProductCreated::dispatch($product);
return redirect()
->route('products.show', $product)
->with('success', 'Product created successfully!');
}
}
Notice how clean this is. The controller doesn't care about notifications, caching, or logging—it just creates the product and dispatches the event. All the side effects are handled by listeners.
Some actions don't need to happen immediately. Sending emails or updating external APIs can be slow—you don't want users waiting for these to complete.
Make a listener queued by implementing the ShouldQueue interface:
<?php
namespace App\Listeners;
use App\Events\ProductCreated;
use App\Notifications\NewProductCreated;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendProductCreatedNotification implements ShouldQueue
{
public function handle(ProductCreated $event): void
{
$product = $event->product;
$admins = User::where('role', 'admin')->get();
foreach ($admins as $admin) {
$admin->notify(new NewProductCreated($product));
}
}
}
Now this listener will run in the background via your queue system. The user gets an immediate response, and the notification is sent asynchronously.
You can customize how queued listeners behave:
class SendProductCreatedNotification implements ShouldQueue
{
// Specify which queue to use
public $queue = 'notifications';
// Number of times to retry
public $tries = 3;
// Seconds to wait before retrying
public $backoff = 60;
public function handle(ProductCreated $event): void
{
// Your code here
}
}
Events can carry any data you need. Here's a more complex example:
<?php
namespace App\Events;
use App\Models\Product;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProductUpdated
{
use Dispatchable, SerializesModels;
public function __construct(
public Product $product,
public array $changes,
public User $updatedBy,
public string $reason
) {}
}
Dispatching it:
ProductUpdated::dispatch(
product: $product,
changes: $product->getChanges(),
updatedBy: auth()->user(),
reason: $request->reason
);
Accessing data in the listener:
class LogProductUpdate
{
public function handle(ProductUpdated $event): void
{
Log::info('Product updated', [
'product_id' => $event->product->id,
'changes' => $event->changes,
'updated_by' => $event->updatedBy->name,
'reason' => $event->reason
]);
}
}
If you have multiple related events and want to handle them in one class, use an event subscriber.
php artisan make:listener ProductEventSubscriber
<?php
namespace App\Listeners;
use App\Events\ProductCreated;
use App\Events\ProductUpdated;
use App\Events\ProductDeleted;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
class ProductEventSubscriber
{
public function handleProductCreated(ProductCreated $event): void
{
Cache::forget('products.all');
}
public function handleProductUpdated(ProductUpdated $event): void
{
Cache::forget('products.all');
Cache::forget('product.' . $event->product->id);
}
public function handleProductDeleted(ProductDeleted $event): void
{
Cache::forget('products.all');
Cache::forget('product.' . $event->product->id);
}
public function subscribe(Dispatcher $events): void
{
$events->listen(
ProductCreated::class,
[ProductEventSubscriber::class, 'handleProductCreated']
);
$events->listen(
ProductUpdated::class,
[ProductEventSubscriber::class, 'handleProductUpdated']
);
$events->listen(
ProductDeleted::class,
[ProductEventSubscriber::class, 'handleProductDeleted']
);
}
}
In EventServiceProvider.php:
protected $subscribe = [
ProductEventSubscriber::class,
];
Let's see how events work together in a real application.
// When a product is created
class ProductCreated
{
public function __construct(public Product $product) {}
}
// When a product is updated
class ProductUpdated
{
public function __construct(
public Product $product,
public array $changes
) {}
}
// When a product goes out of stock
class ProductOutOfStock
{
public function __construct(public Product $product) {}
}
// When a product is back in stock
class ProductBackInStock
{
public function __construct(public Product $product) {}
}
// Notify admins when product is created
class NotifyAdminsOfNewProduct implements ShouldQueue
{
public function handle(ProductCreated $event): void
{
// Send notification
}
}
// Clear cache when product changes
class ClearProductCache
{
public function handle($event): void
{
Cache::forget('products.all');
}
}
// Alert admins when stock runs low
class AlertLowStock implements ShouldQueue
{
public function handle(ProductOutOfStock $event): void
{
// Send urgent notification
}
}
// Notify subscribed users when back in stock
class NotifyWaitingCustomers implements ShouldQueue
{
public function handle(ProductBackInStock $event): void
{
$product = $event->product;
// Get users waiting for this product
$waitingUsers = $product->waitingUsers;
foreach ($waitingUsers as $user) {
$user->notify(new ProductAvailableNotification($product));
}
}
}
protected $listen = [
ProductCreated::class => [
NotifyAdminsOfNewProduct::class,
ClearProductCache::class,
],
ProductUpdated::class => [
ClearProductCache::class,
],
ProductOutOfStock::class => [
AlertLowStock::class,
ClearProductCache::class,
],
ProductBackInStock::class => [
NotifyWaitingCustomers::class,
ClearProductCache::class,
],
];
You can even dispatch events from model events:
<?php
namespace App\Models;
use App\Events\ProductOutOfStock;
use App\Events\ProductBackInStock;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected static function booted(): void
{
static::updated(function (Product $product) {
// Check if quantity changed
if ($product->wasChanged('quantity')) {
$oldQuantity = $product->getOriginal('quantity');
$newQuantity = $product->quantity;
// Just went out of stock
if ($oldQuantity > 0 && $newQuantity == 0) {
ProductOutOfStock::dispatch($product);
}
// Just came back in stock
if ($oldQuantity == 0 && $newQuantity > 0) {
ProductBackInStock::dispatch($product);
}
}
});
}
}
Laravel makes testing events straightforward.
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Product;
use App\Events\ProductCreated;
use Illuminate\Support\Facades\Event;
class ProductTest extends TestCase
{
public function test_product_created_event_is_dispatched(): void
{
Event::fake([ProductCreated::class]);
$product = Product::create([
'name' => 'Test Product',
'price' => 99.99,
]);
// Dispatch the event
ProductCreated::dispatch($product);
// Assert the event was dispatched
Event::assertDispatched(ProductCreated::class, function ($event) use ($product) {
return $event->product->id === $product->id;
});
}
}
public function test_product_cache_is_cleared(): void
{
Cache::put('products.all', ['data'], 60);
$product = Product::factory()->create();
$event = new ProductCreated($product);
$listener = new UpdateProductCache();
$listener->handle($event);
$this->assertFalse(Cache::has('products.all'));
}
Laravel can automatically discover events and listeners without manual registration. Just follow the naming convention:
app/
├── Events/
│ └── ProductCreated.php
└── Listeners/
└── SendProductCreatedNotification.php
Enable discovery in EventServiceProvider.php:
public function shouldDiscoverEvents(): bool
{
return true;
}
Laravel will automatically map ProductCreated to any listeners in the Listeners directory that type-hint it.
Events represent something that already happened.
Good:
ProductCreated
OrderShipped
UserRegistered
PaymentProcessed
Bad:
CreateProduct
ShipOrder
RegisterUser
ProcessPayment
Events should just carry data, not business logic.
Good:
class ProductCreated
{
public function __construct(public Product $product) {}
}
Bad:
class ProductCreated
{
public function __construct(public Product $product)
{
// Don't do business logic here
$this->sendNotifications();
$this->updateCache();
}
}
Each listener should do one thing well.
Good:
SendProductCreatedNotification
UpdateProductCache
LogProductActivity
Bad:
class HandleProductCreated
{
public function handle($event): void
{
// Doing too much in one listener
$this->sendNotification();
$this->updateCache();
$this->logActivity();
$this->syncToThirdParty();
}
}
Don't make users wait for slow operations.
Queue these:
Don't queue these:
Add error handling to listeners:
class SendProductCreatedNotification implements ShouldQueue
{
public $tries = 3;
public function handle(ProductCreated $event): void
{
try {
// Send notification
} catch (\Exception $e) {
Log::error('Failed to send product notification', [
'product_id' => $event->product->id,
'error' => $e->getMessage()
]);
throw $e; // Re-throw to trigger retry
}
}
public function failed(ProductCreated $event, \Throwable $exception): void
{
// This runs after all retries fail
Log::critical('Product notification permanently failed', [
'product_id' => $event->product->id,
'error' => $exception->getMessage()
]);
}
}
Use events to represent significant business moments:
// E-commerce
OrderPlaced
OrderShipped
OrderDelivered
PaymentReceived
RefundIssued
// User Management
UserRegistered
UserVerified
PasswordReset
AccountSuspended
// Content Management
ArticlePublished
CommentPosted
ContentModerated
Use events for system-wide concerns:
// Monitoring
SystemHealthCheck
DatabaseConnectionFailed
ApiRateLimitExceeded
// Caching
CacheWasCleared
CacheWasWarmed
// Logging
AuditLogCreated
SecurityEventDetected
Use events to integrate with external systems:
class ProductCreated
{
public function __construct(public Product $product) {}
}
// Listeners
SyncToShopify
UpdateInventorySystem
NotifyWarehouse
SendToAnalytics
To see all registered events and listeners:
php artisan event:list
To clear cached events:
php artisan event:clear
To generate event cache:
php artisan event:cache
Laravel events transform messy, tightly-coupled code into clean, maintainable applications. Instead of cramming everything into controllers, you dispatch events and let specialized listeners handle the responses.
Key takeaways:
EventName::dispatch($data)EventServiceProvider or use auto-discoveryStart by identifying actions in your application that trigger multiple side effects. Those are perfect candidates for events. Create the event, create listeners for each action, dispatch the event, and watch your code become cleaner and more maintainable.
Your application's architecture isn't just about writing working code—it's about writing code that's easy to understand, test, and change. Events help you achieve all three.