Courses

How to Structure Laravel 12 Projects

Events and Listeners: When and How?

Summary of this lesson:
- Setting up Event classes
- Creating Event Listeners
- Implementing event dispatching
- Managing event-driven operations

Another possible option for "tasks in the background" is to call the Event in the Controller and allow different classes (current ones or future ones) to "listen" to that event.

This is the whole logic behind events/listeners: future-first thinking. You are opening the system for other developers to add their listeners in the future easily.

Imagine the scenario: you need to inform the system that the new user is registered. And then, as one of the Listeners, we want to update a Monthly Report. So first, let's make an Event class:

php artisan make:event NewUserRegistered

And a Listener:

php artisan make:listener MonthlyReportNewUserListener --event=NewUserRegistered

Now we can dispatch the Event in the Controller, similar to a job:

use App\Events\NewUserRegistered;
 
// ...
 
public function store(StoreUserRequest $request)
{
$user = (new CreateUserAction())->execute($request->validated());
NewUserDataJob::dispatch($user);
 
NewUserRegistered::dispatch($user);
//
}

Inside the Event, we won't perform any actions, but we need to accept a User, so every Listener class would have access to that parameter:

app/Events/NewUserRegistered.php:

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

Events are registered automatically by scanning app/Listeners directory.

Inside the MonthlyReportNewUserListener listener class, we have an $event parameter in the handle() method. We move the code from the Controller to that method:

use App\Models\MonthlyReport;
 
class MonthlyReportNewUserListener
{
public function handle(NewUserRegistered $event)
{
MonthlyReport::where('month', now()->format('Y-m'))->increment('users_count');
}
}

Example from Laravel Skeleton

Another example of events/listeners comes from Laravel itself.

In the Controller, we don't need to send email verification notifications because it is already handled by Laravel's Registered event and SendEmailVerificationNotification listener.

But we can create another Listener to send more notifications: for example, notify admins about something.

First, create a Listener and register it in the EventServiceProvider:

php artisan make:listener NewUserSendAdminNotifications --event=NewUserRegistered

Now, in the NewUserSendAdminNotifications listener, in the handle() method, we move the code from the Controller. And you can access the User from the $event using $event->user:

class NewUserSendAdminNotifications
{
public function handle(NewUserRegistered $event)
{
$admins = User::where('is_admin', 1)->get();
Notification::send($admins, new AdminNewUserNotification($event->user));
}
}

Our Shortened Controller

So now, as we moved different code parts from the Controller to various classes, the full Controller looks just like this, from 37 to just 10 lines of code:

public function store(StoreUserRequest $request)
{
$user = (new CreateUserAction())->execute($request->validated());
 
NewUserDataJob::dispatch($user);
 
NewUserRegistered::dispatch($user);
 
return response()->json([
'result' => 'success',
'data' => $user,
], 200);
}

This was our goal: to offload the logic from the Controller to different classes inside the app/ folder. But, again, as I have repeated multiple times, it's your personal preference which classes to use in your projects.

Now, let's look at a few more examples of events/listeners.


Open-Source Examples

Example Project 1. laravelio/laravel.io

Let's look at the laravelio/laravel.io open-source project. This project has seven Events, and each of them has Event Listeners.

Here's the ReplyWasCreated event, which has four listeners.

app/Providers/EventServiceProvider.php:

class EventServiceProvider extends ServiceProvider
{
protected $listen = [
ArticleWasSubmittedForApproval::class => [
SendNewArticleNotification::class,
],
ArticleWasApproved::class => [
SendArticleApprovedNotification::class,
],
EmailAddressWasChanged::class => [
RenewEmailVerificationNotification::class,
],
Registered::class => [
SendEmailVerificationNotification::class,
],
ReplyWasCreated::class => [
MarkLastActivity::class,
SendNewReplyNotification::class,
SubscribeUsersMentionedInReply::class,
NotifyUsersMentionedInReply::class,
],
ThreadWasCreated::class => [
SubscribeUsersMentionedInThread::class,
NotifyUsersMentionedInThread::class,
],
SpamWasReported::class => [
SendNewSpamNotification::class,
],
];
 
// ...
}

When a reply to a thread is made, an event is fired.

app/Jobs/CreateReply.php:

final class CreateReply
{
// ...
 
public function handle(): void
{
$reply = new Reply([
'uuid' => $this->uuid->toString(),
'body' => $this->body,
]);
$reply->authoredBy($this->author);
$reply->to($this->replyAble);
$reply->save();
 
event(new ReplyWasCreated($reply));
 
// ...
}
}

app/Events/ReplyWasCreated.php:

final class ReplyWasCreated
{
use SerializesModels;
 
public function __construct(public Reply $reply)
{
}
}

Then, all listeners get triggered: notifications are sent, the last activity time is set, etc.

app/Listeners/MarkLastActivity.php:

final class MarkLastActivity
{
public function handle(ReplyWasCreated $event): void
{
$replyAble = $event->reply->replyAble();
$replyAble->last_activity_at = now();
$replyAble->timestamps = false;
$replyAble->save();
}
}

Example Project 2. tighten/novapackages

Next, let's look at the tighten/novapackages open-source project. This project has six events, and each event has a listener.

app/Providers/EventServiceProvider.php:

class EventServiceProvider extends ServiceProvider
{
protected $listen = [
CollaboratorClaimedEvent::class => [CollaboratorClaimed::class],
NewUserSignedUp::class => [ClaimOrCreateCollaboratorForNewUser::class],
PackageCreated::class => [SendNewPackageNotification::class],
PackageRated::class => [ClearPackageRatingCache::class],
PackageDeleted::class => [SendPackageDeletedNotification::class],
Registered::class => [SendEmailVerificationNotification::class],
];
 
// ...
}

For example, when a new package is added, the PackageCreated is fired from the Controller.

app/Http/Controllers/App/PackageController.php:

namespace App\Http\Controllers\App;
 
use App\Collaborator;
use App\Events\PackageCreated;
use App\Events\PackageDeleted;
use App\Events\PackageUpdated;
use App\Http\Controllers\Controller;
use App\Http\Requests\PackageFormRequest;
use App\Package;
use App\Tag;
use DateTime;
use Facades\App\Repo;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
 
class PackageController extends Controller
{
// ...
 
public function store(PackageFormRequest $request)
{
// Code to create record in the DB...
 
event(new PackageCreated($package));
 
if (request('screenshots')) {
$package->syncScreenshots(request()->input('screenshots', []));
}
 
return redirect()->route('app.packages.index');
}
 
// ...
}

Then, the SendNewPackageNotification listener is triggered, which sends the notification.

app/Listeners/SendNewPackageNotification.php:

class SendNewPackageNotification
{
public function handle(PackageCreated $event)
{
(new Tighten)->notify(new NewPackage($event->package));
}
}

So, we've finished refactoring our Controller. But there are a few more topics of other Laravel app/ folder structure options that we didn't directly use here. The following few lessons will be about them.

Previous: Dispatch Jobs into Background Queue
avatar

There is a small typo in the "Example from Laravel Skeleton" section, at the bottom: "And you can access the User from the $event using $user->event:" it should be $event->user. 🙂

avatar

Thank you, fixed the typo!

avatar

I don't think the typo was addressed because I can still see it just as the user above mentioned it.

avatar
You can use Markdown
avatar

Hello, There is not Event Service provider in Laravel 11, So where should I listen like,

protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
    \Illuminate\Auth\Events\Login::class => [
        SetTenantIdInSession::class,
        RecordLogin::class,
    ],
    Logout::class => [
        ClearTenantIdFromSession::class,
    ]
];

where this will go in laravel 11.

avatar

I got an email notification of your comment, I guess since I also commented here before.

To answer your question, Laravel 11 removed all service provider files except for AppServiceProvider. You can either let your events be auto-discovered or register them manually, but in a different way, using the AppServiceProvider.

I'm sure the LD team will update this article, however, in the meantime, here's the documentation on how to register your listeners: https://laravel.com/docs/11.x/events#registering-events-and-listeners

avatar
You can use Markdown
avatar
You can use Markdown