0x55aa
โ† Back to Blog

Laravel Events: Stop Cramming Everything Into One Controller ๐ŸŽฏ

โ€ข8 min read

Laravel Events: Stop Cramming Everything Into One Controller ๐ŸŽฏ

You know that moment when a user registers and you need to: send a welcome email, notify admins, log to analytics, update CRM, create a Slack notification, give them welcome points, and probably solve world hunger too?

Your controller starts looking like this monster:

public function register(Request $request)
{
    $user = User::create($request->validated());

    Mail::to($user)->send(new WelcomeEmail());
    Mail::to('[email protected]')->send(new NewUserNotification());
    Analytics::track('user_registered', $user);
    Slack::send('New user: ' . $user->name);
    CRM::createContact($user);
    $user->givePoints(100);
    Cache::forget('user_stats');

    return response()->json(['message' => 'Welcome!']);
}

Seven different responsibilities in ONE method! If your controller needs therapy, it's time to learn about Events! ๐ŸŽช

What Are Events & Listeners? ๐Ÿค”

Think of it like a party announcement system:

Event: "HEY EVERYONE, A USER JUST REGISTERED!" ๐Ÿ“ฃ

Listeners: Multiple party guests who hear the announcement and do their own thing:

  • DJ starts the welcome music ๐ŸŽต
  • Bouncer logs it in the guest book ๐Ÿ“–
  • Chef prepares a welcome snack ๐Ÿช
  • Photographer takes a photo ๐Ÿ“ธ

Nobody has to tell each person what to do. They just LISTEN for the announcement and act independently!

In Laravel terms:

  • Event = Something happened (UserRegistered)
  • Listeners = Things that respond to it (SendWelcomeEmail, NotifyAdmin, UpdateAnalytics)

The Problem with Fat Controllers ๐Ÿ˜ฑ

Before Events (the nightmare):

public function register(Request $request)
{
    $user = User::create($request->validated());

    // 50 lines of "stuff that needs to happen"
    Mail::to($user)->send(new WelcomeEmail());
    Mail::to('[email protected]')->send(new NewUserAlert($user));
    Analytics::track('user_registered', $user->id);

    // Update external CRM
    $crm = new CRMClient(config('crm.api_key'));
    $crm->contacts->create([
        'email' => $user->email,
        'name' => $user->name
    ]);

    // Send Slack notification
    Http::post(config('slack.webhook'), [
        'text' => "New user: {$user->name}"
    ]);

    // Award welcome points
    $user->points()->create(['amount' => 100, 'reason' => 'Welcome bonus']);

    // Clear caches
    Cache::forget('total_users');
    Cache::forget('recent_signups');

    return response()->json(['message' => 'Welcome!']);
}

Problems:

  • Controller knows too much ๐Ÿง ๐Ÿ’ฅ
  • Can't test parts independently ๐Ÿงช
  • Hard to add/remove features ๐Ÿ”ง
  • Slow (everything runs synchronously) ๐ŸŒ
  • One failure breaks everything ๐Ÿ’€

The Event Solution โœจ

After Events (the dream):

public function register(Request $request)
{
    $user = User::create($request->validated());

    // Fire the event
    event(new UserRegistered($user));

    return response()->json(['message' => 'Welcome!']);
}

THREE LINES! Now that's what I call clean code! ๐Ÿงผ

Creating Your First Event ๐ŸŽฌ

Laravel makes this stupidly easy:

php artisan make:event UserRegistered

This creates app/Events/UserRegistered.php:

<?php

namespace App\Events;

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

class UserRegistered
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public User $user
    ) {}
}

That's it! An event is just a data bag that holds information about what happened.

Creating Listeners ๐Ÿ‘‚

Now create listeners for all the things that should happen:

php artisan make:listener SendWelcomeEmail --event=UserRegistered
php artisan make:listener NotifyAdmin --event=UserRegistered
php artisan make:listener UpdateAnalytics --event=UserRegistered
php artisan make:listener CreateCRMContact --event=UserRegistered

Each listener gets a handle() method:

<?php

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeEmail());
    }
}

Beautiful! Each listener has ONE job. Single Responsibility Principle for the win! ๐Ÿ†

Registering Event Listeners ๐Ÿ“‹

In app/Providers/EventServiceProvider.php:

protected $listen = [
    UserRegistered::class => [
        SendWelcomeEmail::class,
        NotifyAdmin::class,
        UpdateAnalytics::class,
        CreateCRMContact::class,
        AwardWelcomePoints::class,
    ],
];

Pro tip: Laravel can auto-discover listeners! Just follow the naming convention and you don't even need this! ๐ŸŽ‰

The Power Moves ๐Ÿ’ช

1. Queue Your Listeners (Make Them Fast!)

class SendWelcomeEmail implements ShouldQueue
{
    use Queueable;

    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeEmail());
    }
}

That's it! Add implements ShouldQueue and the listener runs in the background. Your response is instant! โšก

2. Conditional Listeners

class SendPremiumWelcome
{
    public function handle(UserRegistered $event): void
    {
        if ($event->user->isPremium()) {
            Mail::to($event->user)->send(new PremiumWelcomeEmail());
        }
    }
}

Translation: "Only run this if the user is premium!" Easy conditional logic! ๐ŸŽ›๏ธ

3. Stop Propagation

class CheckUserIsBanned
{
    public function handle(UserRegistered $event): bool
    {
        if ($event->user->isBanned()) {
            // Stop other listeners from running!
            return false;
        }

        return true;
    }
}

The power: One listener can stop the chain! Like circuit breakers for events! ๐Ÿšฆ

4. Event Subscribers (Multiple Events, One Class)

class UserEventSubscriber
{
    public function handleUserRegistered(UserRegistered $event): void
    {
        // Handle registration
    }

    public function handleUserLoggedIn(UserLoggedIn $event): void
    {
        // Handle login
    }

    public function subscribe($events): void
    {
        $events->listen(
            UserRegistered::class,
            [UserEventSubscriber::class, 'handleUserRegistered']
        );

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

When to use: When you have related events that share logic. Keep all user-related event handling in one place! ๐Ÿ“ฆ

Real-World Example: E-commerce Order ๐Ÿ›’

The event:

class OrderPlaced
{
    public function __construct(
        public Order $order,
        public User $customer
    ) {}
}

The listeners:

// SendOrderConfirmation.php
class SendOrderConfirmation
{
    public function handle(OrderPlaced $event): void
    {
        Mail::to($event->customer)->send(
            new OrderConfirmationEmail($event->order)
        );
    }
}

// NotifyWarehouse.php
class NotifyWarehouse implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        Http::post(config('warehouse.webhook'), [
            'order_id' => $event->order->id,
            'items' => $event->order->items->toArray()
        ]);
    }
}

// UpdateInventory.php
class UpdateInventory
{
    public function handle(OrderPlaced $event): void
    {
        foreach ($event->order->items as $item) {
            $item->product->decrement('stock', $item->quantity);
        }
    }
}

// CreateInvoice.php
class CreateInvoice implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        $invoice = Invoice::generate($event->order);
        Storage::put("invoices/{$invoice->id}.pdf", $invoice->pdf());
    }
}

// AwardLoyaltyPoints.php
class AwardLoyaltyPoints
{
    public function handle(OrderPlaced $event): void
    {
        $points = floor($event->order->total * 0.1); // 10% back
        $event->customer->addPoints($points);
    }
}

The controller:

public function store(Request $request)
{
    $order = Order::create($request->validated());

    event(new OrderPlaced($order, auth()->user()));

    return response()->json(['order' => $order]);
}

CLEAN! Five complex operations, one simple controller! ๐ŸŽฏ

Testing Made Easy ๐Ÿงช

Test that event is fired:

public function test_order_placed_event_fires()
{
    Event::fake([OrderPlaced::class]);

    $this->post('/orders', $orderData);

    Event::assertDispatched(OrderPlaced::class);
}

Test listener in isolation:

public function test_welcome_email_is_sent()
{
    Mail::fake();

    $user = User::factory()->create();
    $listener = new SendWelcomeEmail();

    $listener->handle(new UserRegistered($user));

    Mail::assertSent(WelcomeEmail::class);
}

The beauty: Test each piece independently! No more testing 10 things at once! ๐ŸŽŠ

Bonus: Model Events (Built-in Magic) โœจ

Laravel models fire events automatically!

class User extends Model
{
    protected static function booted()
    {
        // Runs AFTER user is created
        static::created(function ($user) {
            event(new UserRegistered($user));
        });

        // Runs BEFORE user is deleted
        static::deleting(function ($user) {
            // Clean up related data
            $user->posts()->delete();
        });

        // Runs AFTER user is updated
        static::updated(function ($user) {
            if ($user->wasChanged('email')) {
                // Email changed! Send verification
            }
        });
    }
}

Available model events:

  • retrieved, creating, created
  • updating, updated
  • saving, saved
  • deleting, deleted
  • restoring, restored

Pro tip: Use creating for defaults, created for notifications, deleting for cleanup!

When to Use Events ๐ŸŽจ

Use events when:

  • โœ… Multiple things need to happen after an action
  • โœ… You want decoupled code
  • โœ… Features might be added/removed later
  • โœ… You need to notify external services
  • โœ… Testing needs to be isolated

Don't use events when:

  • โŒ Only ONE thing needs to happen
  • โŒ The logic is critical to the operation (use synchronous code)
  • โŒ You're making things complex for no reason

Real talk: Events are for "side effects," not core business logic. Creating an order? That's NOT an event. Sending confirmation email after order? THAT's an event! ๐Ÿ“ง

Common Gotchas ๐Ÿชค

Gotcha #1: Event listeners not running

Did you register them in EventServiceProvider?

protected $listen = [
    MyEvent::class => [MyListener::class],
];

Or run: php artisan event:cache after changes!

Gotcha #2: Queued listener not working

Did you start your queue worker?

php artisan queue:work

Gotcha #3: Circular events

// DON'T DO THIS:
UserCreated -> fires -> UpdateStats -> fires -> UserCreated -> ๐Ÿ’€

Be careful not to create event loops! Your app will explode! ๐Ÿ’ฅ

The Event Checklist โœ…

Before you ship:

  • Events named clearly (UserRegistered, OrderPlaced)
  • Listeners registered in EventServiceProvider
  • Slow listeners marked as ShouldQueue
  • Tests for event dispatch and listener logic
  • No circular event dependencies
  • Queue worker running in production

Real Talk ๐Ÿ’ฌ

Q: "Should I use events for EVERYTHING?"

A: No! Use them for side effects and cross-cutting concerns. Core business logic should be explicit in your code.

Q: "Events vs Jobs - what's the difference?"

A: Events = "Something happened, anyone who cares can respond." Jobs = "Do this specific task." Events broadcast, jobs execute!

Q: "Are events slower?"

A: Synchronous events? Barely! Queued listeners? Faster! They run in the background!

Q: "Can I pass multiple parameters to an event?"

A: Yes! Events are just classes. Put whatever you need in the constructor!

The Bottom Line

Events are like having a town crier for your app:

  1. Something happens (Event fires)
  2. Announce it (event())
  3. Let interested parties respond (Listeners)
  4. Keep your controllers clean (Single responsibility!)

Stop writing controllers that do 47 things. Stop coupling every feature together. Fire events and let listeners handle their own business!

Think of it like ordering pizza: You don't tell the restaurant how to make the pizza, prep the ingredients, drive the car, and ring your doorbell. You just order (fire event) and they handle the rest (listeners)! ๐Ÿ•

Your controllers will be clean, your code will be testable, and when someone asks you to add "just one more thing" after user registration, you'll just create another listener instead of crying into your coffee! โ˜•


Want to discuss Laravel architecture patterns? Hit me up on LinkedIn - Let's talk decoupled code!

Found this helpful? Star this blog on GitHub for more Laravel magic!

Now go decouple that spaghetti code! ๐Ÿโžก๏ธโœจ