Building Robust Laravel Applications with Lifecycle Hooks: A Complete Guide
Introduction
In enterprise-level Laravel applications, complex processes may need to run across various contexts — CLI commands, queued jobs, or web interfaces — each potentially needing unique feedback mechanisms. How can we effectively handle these scenarios without duplicating code or mingling concerns? Enter lifecycle hooks: a pattern that provides flexibility for multiple contexts while keeping our code clean and maintainable.
Problem We’re Solving
Imagine building a system to handle long-running tasks — data migration, API syncs, or complex calculations. Each task takes time, and you may need to:
- Display progress for CLI commands
- Send Slack updates when queued
- Maintain DRY, extensible code for future needs
This guide walks through implementing a lifecycle hook pattern to address these requirements in a Laravel project.
Project Structure
Organizing code properly is essential for maintainability. Here’s the project structure we’ll use:
app/
├── Actions/ # Action classes for individual tasks
│ ├── FirstStepOfComplexThing.php
│ ├── SecondStepOfComplexThing.php
│ └── ...
├── Console/
│ └── Commands/ # Artisan commands
│ └── RunActionsCommand.php
├── Jobs/ # Queue jobs
│ └── RunActionsJob.php
├── Traits/ # Reusable traits
│ └── UsesOnProgressHook.php
└── ActionRunner.php # Core runner class for coordinating actions
Step 1: Building the Progress Hook Trait
The UsesOnProgressHook
trait will manage our progress callbacks, allowing any class to handle progress tracking independently of the notification method.
<?php
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);
}
}
}
This trait:
- Defines
onProgress()
to accept a callback function for progress updates - Calls
callOnProgressHook()
to invoke the callback - Utilizes nullable types to ensure better type safety with PHP 8
Step 2: Creating Action Classes
Following the Single Responsibility Principle, each task in our process will be represented by a separate, invokable class.
<?php
namespace App\Actions;
class FirstStepOfComplexThing
{
public function __invoke(): void
{
// Simulate some work
sleep(1); // This would be replaced with actual task logic
}
}
By using invokable classes, each task is encapsulated, making it modular, testable, and easy to manage.
Step 3: Implementing the Action Runner
The ActionRunner
class will coordinate our actions, using the progress hook trait to manage progress updates.
<?php
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,
];
public function run(): void
{
foreach ($this->actions as $action) {
$actionClass = class_basename($action);
$this->callOnProgressHook("Starting {$actionClass}...");
app($action)();
$this->callOnProgressHook("Completed {$actionClass}!");
}
}
}
Key aspects:
- Uses Laravel’s service container for dependency injection
- Calls progress updates before and after each action
- Keeps a declarative list of actions, making it easy to manage
Step 4: Setting Up Context-Specific Handlers
Command Line Interface (CLI)
For CLI operations, we want progress to display directly in the terminal.
<?php
namespace App\Console\Commands;
use App\ActionRunner;
use Illuminate\Console\Command;
class RunActionsCommand extends Command
{
protected $signature = 'actions:run';
protected $description = 'Run complex actions with progress feedback';
public function handle(): int
{
$this->info('Starting complex actions...');
app(ActionRunner::class)
->onProgress(function (string $progress) {
$this->line($progress);
})
->run();
$this->info('All actions completed successfully!');
return Command::SUCCESS;
}
}
Queue Jobs with Slack Notifications
When the actions run as a background job, we’ll send updates to Slack using Laravel’s logging channels.
<?php
namespace App\Jobs;
use App\ActionRunner;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class RunActionsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function handle(): void
{
app(ActionRunner::class)
->onProgress(function (string $progress) {
Log::channel('slack')->info($progress);
Log::info($progress); // Also logs to the default channel
})
->run();
}
}
Step 5: Configuring Slack Logging
To set up Slack logging, add this to config/logging.php
:
'channels' => [
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log Bot',
'emoji' => ':rocket:',
'level' => env('LOG_LEVEL', 'critical'),
],
],
Then, in your .env
file, add:
LOG_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/your/webhook/url
Using the Implementation
Here are the main ways to execute this process:
Via Artisan Command:
php artisan actions:run
Through a Queued Job:
RunActionsJob::dispatch();
Directly with Custom Progress Handling:
app(ActionRunner::class)
->onProgress(function (string $progress) {
echo $progress . PHP_EOL;
})
->run();
Advantages of This Pattern
Separation of Concerns
- Action logic is isolated in individual classes, making it modular.
- Progress tracking is decoupled from the actions, allowing different contexts to handle it independently.
Maintainability
- Each class has a single responsibility, making changes easier and less risky.
Flexibility
- Different contexts (CLI, jobs, etc.) can handle progress differently.
- New notification methods can be added with minimal code changes.
Testability
- Actions are testable in isolation.
- Progress tracking can be mocked or verified.
Extending the Pattern
This pattern is highly extensible. Here are a few ideas:
Additional Hooks
- Create error-handling hooks.
- Add before/after hooks for each action.
Enhanced Progress Information
- Include timing or memory usage.
- Display percentages for task completion.
New Notification Channels
- Add email or SMS updates.
- Use WebSocket broadcasts for real-time feedback.
Conclusion
Lifecycle hooks offer a clean solution for handling complex processes in various contexts. By implementing this pattern, you achieve a robust, maintainable structure that keeps your code clean, flexible, and easy to extend.
Thank you for reading! Follow me for more Laravel insights.