Lifecycle hooks in Laravel - How to build them, and why you'd want to
Published on by Len Woodward
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.
(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.