Building Robust Laravel Applications with Lifecycle Hooks: A Complete Guide

Scaibu
4 min readNov 5, 2024

--

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.

--

--

Scaibu
Scaibu

Written by Scaibu

Revolutionize Education with Scaibu: Improving Tech Education and Building Networks with Investors for a Better Future

No responses yet