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