Lifecycle hooks in Laravel - How to build them, and why you'd want to

Published on by

Lifecycle hooks in Laravel - How to build them, and why you'd want to image

We, as programmers, have to be particularly adept at breaking large, complicated problems down into smaller, easier-to-manage chunks. Sometimes, however, it turns out that some of those smaller, repeated bits of code that we've extracted to reduce duplication (or some other need) have to be interacted with differently based on some external context.

Let's explore one of these scenarios, and I'll guide you along the process with me.

To set the scene, we have a list of semi-complex actions strung together to perform an overall gargantuan task. We take this list and make them into a series of invokable classes. The contents aren't important. We'll list these out in our ActionRunner class. Then we loop over them and execute each in order from our run() method.

namespace App;
 
use App\Actions;
 
class ActionRunner
{
public array $actions = [
Actions\FirstStepOfComplexThing::class,
Actions\SecondStepOfComplexThing::class,
Actions\ThirdStepOfComplexThing::class,
Actions\FourthStepOfComplexThing::class,
Actions\FifthStepOfComplexThing::class,
Actions\SixthStepOfComplexThing::class,
];
 
public function run(): void
{
foreach ($this->actions as $action) {
// Resolving the action out of the
// container will help with testing
app($action)();
}
}
}

This is good. This is clean and easy to read. And we can use this one ActionRunner in both a queued job and an artisan command.

namespace App\Jobs;
 
use App\ActionRunner;
 
class RunActionsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
 
public function handle(): void
{
app(ActionRunner::class)->run();
}
}
namespace App\Commands;
 
use App\ActionRunner;
 
class RunActionsCommand extends Command
{
protected $signature = 'complicated-thing:run';
 
protected $description = 'Runs the list of complicated actions.';
 
public function handle(): int
{
app(ActionRunner::class)->run();
 
$this->line('The list of complicated things was run successfully!');
 
return Command::SUCCESS;
}
}

This is where we might run into an issue, though. Let's say that these are some particularly heavy actions, and each one takes about a minute to run. This workflow is going to take 5 minutes with zero feedback. Now let's say that when these actions are being run in the queue, we'd like to send a message to our Slack channel after each action is completed. But when running the command, we want to skip that and output it to the CLI.

Your instinct might be to say, "We'll just copy the run() method into the job and the command and do the loop there with whatever we need to do." And yeah, that'd work. But now, not only are we duplicating code (which isn't always bad), but these classes are taking on responsibility that they shouldn't need to. They're required to have knowledge they really shouldn't have, and if we change how this feature works, we've got to change it in multiple places.

I'm going to show you an approach that I absolutely love. We're going to implement an onProgress() hook. Since this hook can likely be used in other places of this fake app, we will go ahead and just make it into a trait.

namespace App\Traits;
 
use Closure;
 
trait UsesOnProgressHook
{
public ?Closure $onProgressFn = null;
 
public function onProgress(Closure $fn): self
{
$this->onProgressFn = $fn;
 
return $this;
}
 
public function callOnProgressHook(...$args): void
{
if ($this->onProgressFn) {
($this->onProgressFn)(...$args);
}
}
}

In this trait, we start out by defining a nullable Closure. This allows us to simply call the callOnProgressHook() method and let the trait worry about whether or not we're going to do anything with it. If we haven't set a function with the fluent onProgress() method, it becomes a no-op. Now our ActionRunner becomes something like this:

namespace App
 
use App\Actions;
use App\Traits\UsesOnProgressHook;
 
class ActionRunner
{
use UsesOnProgressHook;
 
public array $actions = [
Actions\FirstStepOfComplexThing::class,
Actions\SecondStepOfComplexThing::class,
Actions\ThirdStepOfComplexThing::class,
Actions\FourthStepOfComplexThing::class,
Actions\FifthStepOfComplexThing::class,
Actions\SixthStepOfComplexThing::class,
];
 
public function run(): void
{
foreach ($this->actions as $action) {
app($action)();
$this->callOnProgressHook("Ran {$action}.");
}
}
}

Now on its own, this won't do anything since our $onProgressFn is set to null. But this is where the unlimited cosmic power of this hook comes in.

We can change our queued job's handle() method slightly and allow it to send this message to our Slack channel:

public function handle(): void
{
app(ActionRunner::class)
->onProgress(fn (string $progress) => Log::channel('slack')->info($progress))
->run();
}

Similarly, we can change our command's handle() method and let it echo to the console instead:

public function handle(): int
{
app(ActionRunner::class)
->onProgress(fn (string $progress) => $this->info($progress))
->run();
 
$this->line('The list of complicated things was run successfully!');
 
return Command::SUCCESS;
}

At this point, we could do the same exact thing in a scheduled job that doesn't have any hook defined at all, or we could add more notification channels to the queued job. We could instead build an ActionContext class that would store tons of data and pass that back, allowing us to really pick and choose what data we'd like to work with, like execution times, model data, other meta info, etc... We could implement multiple different hooks that are called at different times in the execution cycle, like onBeforeExternalApiCalls(), or onCompleted().

This is such a powerful pattern that I've had some really great success using, and I'm glad I've been able to share it with you.

Len  Woodward photo

(he/him/his) I live with my wife Ashlyn, and daughter Allison, about 40 minutes south of Vancouver, Canada, in Langley City. I acknowledge that where I work, live, and play, is on the unceded territory of the Matsqui, Kwantlen, and Katzie communities.

I've been working with code in one form or another since about 2003 when I was writing Visual Basic in high school. I got my first paid programming gig in 2005. Since that very first program I wrote in VB over 19 years ago, I've been constantly writing code to help my family run our businesses, for personal projects, and for freelance projects as ProjektGopher Multimedia.

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project.

Visit No Compromises
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
DirectoryTree Authorization is a Native Role and Permission Management Package for Laravel image

DirectoryTree Authorization is a Native Role and Permission Management Package for Laravel

Read article
Sort Elements with the Alpine.js Sort Plugin image

Sort Elements with the Alpine.js Sort Plugin

Read article
Anonymous Event Broadcasting in Laravel 11.5 image

Anonymous Event Broadcasting in Laravel 11.5

Read article
Microsoft Clarity Integration for Laravel image

Microsoft Clarity Integration for Laravel

Read article
Apply Dynamic Filters to Eloquent Models with the Filterable Package image

Apply Dynamic Filters to Eloquent Models with the Filterable Package

Read article
Property Hooks Get Closer to Becoming a Reality in PHP 8.4 image

Property Hooks Get Closer to Becoming a Reality in PHP 8.4

Read article