Skip to content

Commit 088112e

Browse files
feat(hooks): refactor to use Laravel Pipeline pattern
Following Statamic's Hookable trait pattern for cleaner hook execution: - Create HookPipeline class using Laravel's Pipeline - Refactor HookManager to use Pipeline instead of manual looping - Update MetricsHook interface to accept mixed payloads - Update ClosureHook to work with Pipeline pattern - Integrate hooks in RecordJobStart/Completion/Failure actions Closes the implementation gap identified in the terrible verbose pattern. Pipeline pattern provides: - Cleaner, more Laravel-like code - Better closure binding support - More flexible payload handling - Follows established Laravel conventions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 07abe68 commit 088112e

File tree

10 files changed

+294
-48
lines changed

10 files changed

+294
-48
lines changed

config/queue-metrics.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use PHPeek\LaravelQueueMetrics\Repositories\RedisQueueMetricsRepository;
2323
use PHPeek\LaravelQueueMetrics\Repositories\RedisWorkerHeartbeatRepository;
2424
use PHPeek\LaravelQueueMetrics\Repositories\RedisWorkerRepository;
25+
use PHPeek\LaravelQueueMetrics\Actions\CalculateBaselinesAction;
2526

2627
// config for PHPeek/LaravelQueueMetrics
2728
return [
@@ -135,7 +136,6 @@
135136

136137
'worker_heartbeat' => [
137138
'stale_threshold' => env('QUEUE_METRICS_STALE_THRESHOLD', 60),
138-
'auto_detect_schedule' => env('QUEUE_METRICS_AUTO_DETECT_SCHEDULE', '* * * * *'),
139139
],
140140

141141
'baseline' => [
@@ -168,7 +168,7 @@
168168

169169
/*
170170
|--------------------------------------------------------------------------
171-
| 🛠️ SPATIE-STYLE EXTENSIBILITY
171+
| 🛠️ EXTENSIBILITY
172172
|--------------------------------------------------------------------------
173173
|
174174
| Override repositories and actions to customize package behavior.
@@ -203,7 +203,7 @@
203203
'transition_worker_state' => TransitionWorkerStateAction::class,
204204
'record_queue_depth_history' => RecordQueueDepthHistoryAction::class,
205205
'record_throughput_history' => RecordThroughputHistoryAction::class,
206-
'calculate_baselines' => \PHPeek\LaravelQueueMetrics\Actions\CalculateBaselinesAction::class,
206+
'calculate_baselines' => CalculateBaselinesAction::class,
207207
],
208208

209209
];

src/Actions/RecordJobCompletionAction.php

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Carbon\Carbon;
88
use PHPeek\LaravelQueueMetrics\Repositories\Contracts\JobMetricsRepository;
9+
use PHPeek\LaravelQueueMetrics\Support\HookManager;
910

1011
/**
1112
* Record when a job completes successfully.
@@ -14,6 +15,7 @@
1415
{
1516
public function __construct(
1617
private JobMetricsRepository $repository,
18+
private HookManager $hookManager,
1719
) {}
1820

1921
public function execute(
@@ -30,16 +32,36 @@ public function execute(
3032
return;
3133
}
3234

35+
// Prepare data for hooks
36+
$data = [
37+
'job_id' => $jobId,
38+
'job_class' => $jobClass,
39+
'connection' => $connection,
40+
'queue' => $queue,
41+
'duration_ms' => $durationMs,
42+
'memory_mb' => $memoryMb,
43+
'cpu_time_ms' => $cpuTimeMs,
44+
'hostname' => $hostname,
45+
'completed_at' => Carbon::now(),
46+
];
47+
48+
// Execute before_record hooks
49+
$data = $this->hookManager->execute('before_record', $data);
50+
/** @var array<string, mixed> $data */
51+
3352
$this->repository->recordCompletion(
34-
jobId: $jobId,
35-
jobClass: $jobClass,
36-
connection: $connection,
37-
queue: $queue,
38-
durationMs: $durationMs,
39-
memoryMb: $memoryMb,
40-
cpuTimeMs: $cpuTimeMs,
41-
completedAt: Carbon::now(),
42-
hostname: $hostname,
53+
jobId: $data['job_id'],
54+
jobClass: $data['job_class'],
55+
connection: $data['connection'],
56+
queue: $data['queue'],
57+
durationMs: $data['duration_ms'],
58+
memoryMb: $data['memory_mb'],
59+
cpuTimeMs: $data['cpu_time_ms'],
60+
completedAt: $data['completed_at'],
61+
hostname: $data['hostname'],
4362
);
63+
64+
// Execute after_record hooks
65+
$this->hookManager->execute('after_record', $data);
4466
}
4567
}

src/Actions/RecordJobFailureAction.php

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Carbon\Carbon;
88
use PHPeek\LaravelQueueMetrics\Repositories\Contracts\JobMetricsRepository;
9+
use PHPeek\LaravelQueueMetrics\Support\HookManager;
910
use Throwable;
1011

1112
/**
@@ -15,6 +16,7 @@
1516
{
1617
public function __construct(
1718
private JobMetricsRepository $repository,
19+
private HookManager $hookManager,
1820
) {}
1921

2022
public function execute(
@@ -29,14 +31,33 @@ public function execute(
2931
return;
3032
}
3133

34+
// Prepare data for hooks
35+
$exceptionMessage = $exception->getMessage().' in '.$exception->getFile().':'.$exception->getLine();
36+
$data = [
37+
'job_id' => $jobId,
38+
'job_class' => $jobClass,
39+
'connection' => $connection,
40+
'queue' => $queue,
41+
'exception' => $exceptionMessage,
42+
'hostname' => $hostname,
43+
'failed_at' => Carbon::now(),
44+
];
45+
46+
// Execute before_record hooks
47+
$data = $this->hookManager->execute('before_record', $data);
48+
/** @var array<string, mixed> $data */
49+
3250
$this->repository->recordFailure(
33-
jobId: $jobId,
34-
jobClass: $jobClass,
35-
connection: $connection,
36-
queue: $queue,
37-
exception: $exception->getMessage().' in '.$exception->getFile().':'.$exception->getLine(),
38-
failedAt: Carbon::now(),
39-
hostname: $hostname,
51+
jobId: $data['job_id'],
52+
jobClass: $data['job_class'],
53+
connection: $data['connection'],
54+
queue: $data['queue'],
55+
exception: $data['exception'],
56+
failedAt: $data['failed_at'],
57+
hostname: $data['hostname'],
4058
);
59+
60+
// Execute after_record hooks
61+
$this->hookManager->execute('after_record', $data);
4162
}
4263
}

src/Actions/RecordJobStartAction.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Carbon\Carbon;
88
use PHPeek\LaravelQueueMetrics\Repositories\Contracts\JobMetricsRepository;
99
use PHPeek\LaravelQueueMetrics\Repositories\Contracts\QueueMetricsRepository;
10+
use PHPeek\LaravelQueueMetrics\Support\HookManager;
1011

1112
/**
1213
* Record when a job starts processing.
@@ -16,6 +17,7 @@
1617
public function __construct(
1718
private JobMetricsRepository $repository,
1819
private QueueMetricsRepository $queueMetricsRepository,
20+
private HookManager $hookManager,
1921
) {}
2022

2123
public function execute(
@@ -31,12 +33,28 @@ public function execute(
3133
// Mark queue as discovered for listQueues() to find it
3234
$this->queueMetricsRepository->markQueueDiscovered($connection, $queue);
3335

36+
// Prepare data for hooks
37+
$data = [
38+
'job_id' => $jobId,
39+
'job_class' => $jobClass,
40+
'connection' => $connection,
41+
'queue' => $queue,
42+
'started_at' => Carbon::now(),
43+
];
44+
45+
// Execute before_record hooks
46+
$data = $this->hookManager->execute('before_record', $data);
47+
/** @var array<string, mixed> $data */
48+
3449
$this->repository->recordStart(
35-
jobId: $jobId,
36-
jobClass: $jobClass,
37-
connection: $connection,
38-
queue: $queue,
39-
startedAt: Carbon::now(),
50+
jobId: $data['job_id'],
51+
jobClass: $data['job_class'],
52+
connection: $data['connection'],
53+
queue: $data['queue'],
54+
startedAt: $data['started_at'],
4055
);
56+
57+
// Execute after_record hooks
58+
$this->hookManager->execute('after_record', $data);
4159
}
4260
}

src/Contracts/MetricsHook.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@
77
/**
88
* Hook interface for extending metrics processing pipeline.
99
* Allows users to inject custom logic at key points in metrics collection.
10+
*
11+
* Hooks are executed through Laravel's Pipeline for clean composition.
1012
*/
1113
interface MetricsHook
1214
{
1315
/**
14-
* Execute hook logic.
15-
*
16-
* @param array<string, mixed> $data
17-
* @return array<string, mixed> Modified data
16+
* Process the payload through the hook.
17+
* Can accept and return any type of payload (arrays, DTOs, etc.).
1818
*/
19-
public function handle(array $data): array;
19+
public function handle(mixed $payload): mixed;
2020

2121
/**
22-
* Determine if this hook should run.
22+
* Determine if this hook should run in the given context.
2323
*/
2424
public function shouldRun(string $context): bool;
2525

src/Facades/QueueMetrics.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
namespace PHPeek\LaravelQueueMetrics\Facades;
66

7+
use Closure;
78
use Illuminate\Support\Facades\Facade;
9+
use PHPeek\LaravelQueueMetrics\Contracts\MetricsHook;
810
use PHPeek\LaravelQueueMetrics\Services\JobMetricsQueryService;
911
use PHPeek\LaravelQueueMetrics\Services\OverviewQueryService;
1012
use PHPeek\LaravelQueueMetrics\Services\QueueMetricsQueryService;
1113
use PHPeek\LaravelQueueMetrics\Services\WorkerMetricsQueryService;
14+
use PHPeek\LaravelQueueMetrics\Support\ClosureHook;
15+
use PHPeek\LaravelQueueMetrics\Support\HookManager;
1216

1317
/**
1418
* Facade providing convenient access to queue metrics services.
@@ -35,10 +39,14 @@
3539
* @method static array<string, array<string, mixed>> getAllServersWithMetrics()
3640
* @method static array<string, mixed> getWorkersSummary()
3741
*
42+
* Hook methods:
43+
* @method static void hook(string $context, \Closure|\PHPeek\LaravelQueueMetrics\Contracts\MetricsHook $hook, int $priority = 100)
44+
*
3845
* @see \PHPeek\LaravelQueueMetrics\Services\OverviewQueryService
3946
* @see \PHPeek\LaravelQueueMetrics\Services\JobMetricsQueryService
4047
* @see \PHPeek\LaravelQueueMetrics\Services\QueueMetricsQueryService
4148
* @see \PHPeek\LaravelQueueMetrics\Services\WorkerMetricsQueryService
49+
* @see \PHPeek\LaravelQueueMetrics\Support\HookManager
4250
*/
4351
final class QueueMetrics extends Facade
4452
{
@@ -73,4 +81,23 @@ public static function __callStatic(mixed $method, mixed $args): mixed
7381

7482
return $service->$method(...$args);
7583
}
84+
85+
/**
86+
* Register a hook for a specific context.
87+
*
88+
* @param string $context Hook context (before_record, after_record, etc.)
89+
* @param Closure|MetricsHook $hook Closure or MetricsHook implementation
90+
* @param int $priority Lower priority runs first (default: 100)
91+
*/
92+
public static function hook(string $context, Closure|MetricsHook $hook, int $priority = 100): void
93+
{
94+
$hookManager = app(HookManager::class);
95+
96+
// Wrap closures in ClosureHook
97+
if ($hook instanceof Closure) {
98+
$hook = new ClosureHook($hook, $context, $priority);
99+
}
100+
101+
$hookManager->register($context, $hook);
102+
}
76103
}

src/LaravelQueueMetricsServiceProvider.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
use PHPeek\LaravelQueueMetrics\Services\RedisKeyScannerService;
5656
use PHPeek\LaravelQueueMetrics\Services\ServerMetricsService;
5757
use PHPeek\LaravelQueueMetrics\Services\WorkerMetricsQueryService;
58+
use PHPeek\LaravelQueueMetrics\Contracts\MetricsHook;
59+
use PHPeek\LaravelQueueMetrics\Support\HookManager;
60+
use PHPeek\LaravelQueueMetrics\Support\HookPipeline;
5861
use PHPeek\LaravelQueueMetrics\Support\RedisMetricsStore;
5962
use PHPeek\LaravelQueueMetrics\Utilities\PercentileCalculator;
6063
use Spatie\LaravelPackageTools\Package;
@@ -112,6 +115,10 @@ public function packageRegistered(): void
112115

113116
// Register utilities
114117
$this->app->singleton(PercentileCalculator::class);
118+
119+
// Register hook system
120+
$this->app->singleton(HookPipeline::class);
121+
$this->app->singleton(HookManager::class);
115122
}
116123

117124
/**
@@ -180,25 +187,50 @@ public function packageBooted(): void
180187

181188
// Register scheduled tasks
182189
$this->registerScheduledTasks();
190+
191+
// Load and register hooks from configuration
192+
$this->loadHooksFromConfig();
193+
}
194+
195+
/**
196+
* Load hooks from configuration and register them.
197+
*/
198+
protected function loadHooksFromConfig(): void
199+
{
200+
/** @var HookManager $hookManager */
201+
$hookManager = $this->app->make(HookManager::class);
202+
203+
/** @var array<string, array<class-string<MetricsHook>>> $configHooks */
204+
$configHooks = config('queue-metrics.hooks', []);
205+
206+
foreach ($configHooks as $context => $hookClasses) {
207+
foreach ($hookClasses as $hookClass) {
208+
if (! class_exists($hookClass)) {
209+
continue;
210+
}
211+
212+
/** @var MetricsHook $hook */
213+
$hook = $this->app->make($hookClass);
214+
$hookManager->register($context, $hook);
215+
}
216+
}
183217
}
184218

185219
/**
186220
* Register scheduled tasks for queue metrics maintenance.
187221
*/
188222
protected function registerScheduledTasks(): void
189223
{
190-
/** @var string $schedule */
191-
$schedule = config('queue-metrics.worker_heartbeat.auto_detect_schedule', '* * * * *');
192224
/** @var int $threshold */
193225
$threshold = config('queue-metrics.worker_heartbeat.stale_threshold', 60);
194226

195227
// Schedule stale worker cleanup
196-
$this->app->booted(function () use ($schedule, $threshold) {
228+
$this->app->booted(function () use ($threshold) {
197229
$scheduler = $this->app->make(\Illuminate\Console\Scheduling\Schedule::class);
198230

199231
$scheduler->command('queue-metrics:cleanup-stale-workers', [
200232
'--threshold' => $threshold,
201-
])->cron($schedule);
233+
])->everyMinute();
202234

203235
// Schedule adaptive baseline calculation
204236
$this->scheduleAdaptiveBaselineCalculation($scheduler);

0 commit comments

Comments
 (0)