Laravel Cloud is here! Zero-config managed infrastructure for Laravel apps. Deploy now.

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
Laravel Code Review

Get expert guidance in a few days with a Laravel code review

Visit Laravel Code Review
Curotec logo

Curotec

World class Laravel experts with GenAI dev skills. LATAM-based, embedded engineers that ship fast, communicate clearly, and elevate your product. No bloat, no BS.

Curotec
Bacancy logo

Bacancy

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

Bacancy
Tinkerwell logo

Tinkerwell

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

Tinkerwell
Cut PHP Code Review Time & Bugs into Half with CodeRabbit logo

Cut PHP Code Review Time & Bugs into Half with CodeRabbit

CodeRabbit is an AI-powered code review tool that specializes in PHP and Laravel, running PHPStan and offering automated PR analysis, security checks, and custom review features while remaining free for open-source projects.

Cut PHP Code Review Time & Bugs into Half with CodeRabbit
Get expert guidance in a few days with a Laravel code review logo

Get expert guidance in a few days with a Laravel code review

Expert code review! Get clear, practical feedback from two Laravel devs with 10+ years of experience helping teams build better apps.

Get expert guidance in a few days with a Laravel code review
PhpStorm logo

PhpStorm

The go-to PHP IDE with extensive out-of-the-box support for Laravel and its ecosystem.

PhpStorm
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
Harpoon: Next generation time tracking and invoicing logo

Harpoon: Next generation time tracking and invoicing

The next generation time-tracking and billing software that helps your agency plan and forecast a profitable future.

Harpoon: Next generation time tracking and invoicing
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

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
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant 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

The latest

View all →
Laravel Gets a Claude Code Simplifier Plugin image

Laravel Gets a Claude Code Simplifier Plugin

Read article
Laravel Boost Update Adds Support for the New MCP Protocol image

Laravel Boost Update Adds Support for the New MCP Protocol

Read article
Pest Adds withHost for Browser Testing Subdomains in Laravel image

Pest Adds withHost for Browser Testing Subdomains in Laravel

Read article
Run Artisan Make Commands in Laravel VS Code Extension image

Run Artisan Make Commands in Laravel VS Code Extension

Read article
Livewire 4 Is Dropping Next Week, and wire:transition Makes Animations Effortless image

Livewire 4 Is Dropping Next Week, and wire:transition Makes Animations Effortless

Read article
Laravel 12.45.1, 12.45.2, and 12.46.0 Released image

Laravel 12.45.1, 12.45.2, and 12.46.0 Released

Read article