Menü schliessen
Created: December 8th 2025
Categories: IT Development,  Laravel
Author: Milos Jevtic

Laravel Custom Events: Complete Guide to Creating, Dispatching, and Listening

Introduction: Why Laravel Events Matter

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.

What Are Laravel Events?

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:

  • Event - Something happened (ProductCreated)
  • Listener - What to do about it (SendNotificationToAdmin, UpdateInventoryCache)
  • Dispatcher - The mechanism that connects them

The Benefits

  • Decoupling - Your core logic doesn't need to know about all the side effects
  • Single Responsibility - Each listener does one thing well
  • Testability - Easy to test events and listeners independently
  • Flexibility - Add or remove listeners without touching existing code
  • Async Processing - Run time-consuming tasks in the background

When to Use Events

Before diving into code, let's understand when events make sense.

Good Use Cases

  • Multiple actions triggered by one event - User registers → send email, log activity, notify admin
  • Decoupling concerns - Creating a product shouldn't directly handle notifications
  • Cross-cutting concerns - Logging, analytics, notifications
  • Third-party integrations - Syncing with external services
  • Domain events - Business-significant moments (OrderShipped, PaymentReceived)

When NOT to Use Events

  • Simple, direct operations - If you're only doing one thing, just do it
  • Critical immediate responses - Don't use queued events for things that must happen synchronously
  • Over-engineering simple features - Not every action needs an event

Creating Your First Event

Let's build a practical example: a product management system where creating a product triggers several actions.

Step 1: Generate the Event

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;
    }
}

Understanding the Traits

  • Dispatchable - Lets you dispatch the event using ProductCreated::dispatch($product)
  • InteractsWithSockets - Excludes the current user when broadcasting
  • SerializesModels - Serializes Eloquent models gracefully for queued listeners

Step 2: Create Listeners

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

Step 3: Implement the Listeners

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()
        ]);
    }
}

Step 4: Register Event and Listeners

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
    {
        //
    }
}

Dispatching Events

Now that your event and listeners are set up, it's time to dispatch the event when a product is created.

Method 1: Using the Dispatch Method

use App\Events\ProductCreated;

$product = Product::create($request->validated());

ProductCreated::dispatch($product);

Method 2: Using the Event Helper

$product = Product::create($request->validated());

event(new ProductCreated($product));

Method 3: Using the Event Facade

use Illuminate\Support\Facades\Event;

$product = Product::create($request->validated());

Event::dispatch(new ProductCreated($product));

In a Controller

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.

Queued Listeners for Better Performance

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.

Configuring Queue Behavior

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
    }
}

Passing Multiple Data in Events

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
        ]);
    }
}

Event Subscribers: Grouping Related Listeners

If you have multiple related events and want to handle them in one class, use an event subscriber.

Create a 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']
        );
    }
}

Register the Subscriber

In EventServiceProvider.php:

protected $subscribe = [
    ProductEventSubscriber::class,
];

Real-World Example: Complete Product Workflow

Let's see how events work together in a real application.

The Events

// 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) {}
}

The Listeners

// 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));
        }
    }
}

Registration

protected $listen = [
    ProductCreated::class => [
        NotifyAdminsOfNewProduct::class,
        ClearProductCache::class,
    ],
    
    ProductUpdated::class => [
        ClearProductCache::class,
    ],
    
    ProductOutOfStock::class => [
        AlertLowStock::class,
        ClearProductCache::class,
    ],
    
    ProductBackInStock::class => [
        NotifyWaitingCustomers::class,
        ClearProductCache::class,
    ],
];

Using in Models

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);
                }
            }
        });
    }
}

Testing Events and Listeners

Laravel makes testing events straightforward.

Test That an Event is Dispatched

<?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;
        });
    }
}

Test a Listener Directly

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'));
}

Event Discovery

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.

Best Practices

1. Name Events as Past Tense

Events represent something that already happened.

Good:

ProductCreated
OrderShipped
UserRegistered
PaymentProcessed

Bad:

CreateProduct
ShipOrder
RegisterUser
ProcessPayment

2. Keep Events Simple

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();
    }
}

3. One Listener, One Responsibility

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();
    }
}

4. Use Queues for Slow Operations

Don't make users wait for slow operations.

Queue these:

  • Sending emails
  • Calling external APIs
  • Processing images
  • Generating reports

Don't queue these:

  • Updating cache
  • Simple database writes
  • Critical business logic that must complete

5. Handle Failures Gracefully

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()
        ]);
    }
}

Common Patterns

Pattern 1: Domain Events

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

Pattern 2: System Events

Use events for system-wide concerns:

// Monitoring
SystemHealthCheck
DatabaseConnectionFailed
ApiRateLimitExceeded

// Caching
CacheWasCleared
CacheWasWarmed

// Logging
AuditLogCreated
SecurityEventDetected

Pattern 3: Integration Events

Use events to integrate with external systems:

class ProductCreated
{
    public function __construct(public Product $product) {}
}

// Listeners
SyncToShopify
UpdateInventorySystem
NotifyWarehouse
SendToAnalytics

Debugging Events

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

Conclusion

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:

  • Events represent something that happened (past tense)
  • Listeners respond to events with single-purpose actions
  • Use queued listeners for slow operations
  • Dispatch events with EventName::dispatch($data)
  • Register in EventServiceProvider or use auto-discovery
  • Keep events simple—just data carriers
  • One listener, one responsibility

Start 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.