diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 23587f0..876c1b6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -13,16 +13,10 @@ parameters: path: database/migrations/2025_09_23_000000_create_queue_job_runs_table.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_class\.$#' - identifier: property.notFound + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: src/Console/Commands/CleanupStuckJobs.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$started_at\.$#' - identifier: property.notFound - count: 2 - path: src/Console/Commands/CleanupStuckJobs.php + path: src/Console/Commands/BackfillJobTags.php - message: '#^Negated boolean expression is always true\.$#' @@ -31,83 +25,11 @@ parameters: path: src/Console/Commands/PruneOldJobs.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$exception_message\.$#' - identifier: property.notFound - count: 1 - path: src/Console/Commands/RetryFailedJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_class\.$#' - identifier: property.notFound - count: 2 - path: src/Console/Commands/RetryFailedJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_tags\.$#' - identifier: property.notFound - count: 1 - path: src/Console/Commands/RetryFailedJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$payload\.$#' - identifier: property.notFound - count: 1 - path: src/Console/Commands/RetryFailedJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$status\.$#' - identifier: property.notFound + message: '#^Parameter \#1 \$json of function json_decode expects string, array given\.$#' + identifier: argument.type count: 1 path: src/Console/Commands/RetryFailedJob.php - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$duration_ms\.$#' - identifier: property.notFound - count: 1 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_class\.$#' - identifier: property.notFound - count: 1 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_tags\.$#' - identifier: property.notFound - count: 2 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$payload\.$#' - identifier: property.notFound - count: 1 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$status\.$#' - identifier: property.notFound - count: 3 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\|Illuminate\\Database\\Eloquent\\Collection\\:\:\$job_class\.$#' - identifier: property.notFound - count: 1 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\|Illuminate\\Database\\Eloquent\\Collection\\:\:\$retried_from_id\.$#' - identifier: property.notFound - count: 1 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\|Illuminate\\Database\\Eloquent\\Collection\\:\:\$status\.$#' - identifier: property.notFound - count: 1 - path: src/Http/Controllers/QueueMonitorController.php - - message: ''' #^Call to deprecated method get\(\) of class Illuminate\\Http\\Request\: @@ -123,108 +45,6 @@ parameters: count: 1 path: src/Http/Controllers/QueueMonitorController.php - - - message: '#^Parameter \#1 \$callback of method Illuminate\\Support\\Collection\<\(int\|string\),mixed\>\:\:map\(\) contains unresolvable type\.$#' - identifier: argument.unresolvableType - count: 2 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Parameter \#1 \$callback of method Illuminate\\Support\\Collection\\:\:flatMap\(\) contains unresolvable type\.$#' - identifier: argument.unresolvableType - count: 2 - path: src/Http/Controllers/QueueMonitorController.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$cpu_sys_ms\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$cpu_user_ms\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$duration_ms\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$exception_class\.$#' - identifier: property.notFound - count: 2 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$exception_message\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$finished_at\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_class\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_end_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_delta_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_end_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_start_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$stack\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$started_at\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$status\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - - - message: '#^Call to an undefined method Illuminate\\Support\\Carbon\:\:diffInRealMilliseconds\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/Listeners/RecordJobFailure.php - - message: '#^Call to function method_exists\(\) with Illuminate\\Contracts\\Queue\\Job and ''getJobId'' will always evaluate to true\.$#' identifier: function.alreadyNarrowedType @@ -285,72 +105,6 @@ parameters: count: 2 path: src/Listeners/RecordJobStart.php - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$cpu_sys_ms\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$cpu_user_ms\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$duration_ms\.$#' - identifier: property.notFound - count: 2 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$finished_at\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_end_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_delta_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_end_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_start_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$started_at\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$status\.$#' - identifier: property.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - - - message: '#^Call to an undefined method Illuminate\\Support\\Carbon\:\:diffInRealMilliseconds\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/Listeners/RecordJobSuccess.php - - message: '#^Call to function method_exists\(\) with Illuminate\\Contracts\\Queue\\Job and ''getJobId'' will always evaluate to true\.$#' identifier: function.alreadyNarrowedType @@ -388,118 +142,40 @@ parameters: path: src/Listeners/RecordJobSuccess.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$cpu_sys_ms\.$#' - identifier: property.notFound - count: 2 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$cpu_user_ms\.$#' - identifier: property.notFound - count: 2 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$duration_ms\.$#' - identifier: property.notFound - count: 1 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_tags\.$#' - identifier: property.notFound - count: 1 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_end_bytes\.$#' - identifier: property.notFound - count: 2 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_delta_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_end_bytes\.$#' - identifier: property.notFound + message: '#^Parameter \#1 \$json of function json_decode expects string, array given\.$#' + identifier: argument.type count: 1 path: src/Models/VantageJob.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_peak_start_bytes\.$#' - identifier: property.notFound - count: 1 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$memory_start_bytes\.$#' - identifier: property.notFound - count: 2 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$payload\.$#' - identifier: property.notFound + message: '#^Call to method error\(\) on an unknown class Illuminate\\Notifications\\Messages\\SlackMessage\.$#' + identifier: class.notFound count: 1 - path: src/Models/VantageJob.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$attempt\.$#' - identifier: property.notFound - count: 2 path: src/Notifications/JobFailedNotification.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$exception_class\.$#' - identifier: property.notFound - count: 2 - path: src/Notifications/JobFailedNotification.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$exception_message\.$#' - identifier: property.notFound + message: '#^Instantiated class Illuminate\\Notifications\\Messages\\SlackMessage not found\.$#' + identifier: class.notFound count: 1 path: src/Notifications/JobFailedNotification.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_class\.$#' - identifier: property.notFound - count: 3 - path: src/Notifications/JobFailedNotification.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$queue\.$#' - identifier: property.notFound - count: 2 - path: src/Notifications/JobFailedNotification.php - - - - message: '#^Access to protected property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$connection\.$#' - identifier: property.protected - count: 2 - path: src/Notifications/JobFailedNotification.php - - - - message: '#^Call to method error\(\) on an unknown class Illuminate\\Notifications\\Messages\\SlackMessage\.$#' + message: '#^Method Storvia\\Vantage\\Notifications\\JobFailedNotification\:\:toSlack\(\) has invalid return type Illuminate\\Notifications\\Messages\\SlackMessage\.$#' identifier: class.notFound count: 1 path: src/Notifications/JobFailedNotification.php - - message: '#^Instantiated class Illuminate\\Notifications\\Messages\\SlackMessage not found\.$#' - identifier: class.notFound + message: '#^Call to function is_array\(\) with non\-empty\-array\ will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: src/Notifications/JobFailedNotification.php + path: src/Support/JobRestorer.php - - message: '#^Method HoudaSlassi\\Vantage\\Notifications\\JobFailedNotification\:\:toSlack\(\) has invalid return type Illuminate\\Notifications\\Messages\\SlackMessage\.$#' - identifier: class.notFound + message: '#^Ternary operator condition is always true\.$#' + identifier: ternary.alwaysTrue count: 1 - path: src/Notifications/JobFailedNotification.php + path: src/Support/JobRestorer.php - message: '#^Ternary operator condition is always true\.$#' @@ -508,43 +184,25 @@ parameters: path: src/Support/QueueDepthChecker.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$failed\.$#' + message: '#^Access to an undefined property Storvia\\Vantage\\Models\\VantageJob\:\:\$failed\.$#' identifier: property.notFound count: 1 path: src/Vantage.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$job_class\.$#' - identifier: property.notFound - count: 1 - path: src/Vantage.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$payload\.$#' - identifier: property.notFound - count: 2 - path: src/Vantage.php - - - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$processed\.$#' + message: '#^Access to an undefined property Storvia\\Vantage\\Models\\VantageJob\:\:\$processed\.$#' identifier: property.notFound count: 2 path: src/Vantage.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$status\.$#' + message: '#^Access to an undefined property Storvia\\Vantage\\Models\\VantageJob\:\:\$total\.$#' identifier: property.notFound count: 1 path: src/Vantage.php - - message: '#^Access to an undefined property HoudaSlassi\\Vantage\\Models\\VantageJob\:\:\$total\.$#' - identifier: property.notFound - count: 1 - path: src/Vantage.php - - - - message: '#^Call to an undefined method HoudaSlassi\\Vantage\\Support\\QueueDepthChecker\:\:check\(\)\.$#' - identifier: method.notFound + message: '#^Call to function is_string\(\) with non\-falsy\-string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 path: src/Vantage.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index de458d6..f2d83fc 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,4 +10,8 @@ parameters: tmpDir: build/phpstan checkOctaneCompatibility: true checkModelProperties: true + ignoreErrors: + - + message: '#^Parameter \#1 \$view of function view expects view\-string\|null, string given\.$#' + reportUnmatched: false diff --git a/src/Http/Controllers/QueueMonitorController.php b/src/Http/Controllers/QueueMonitorController.php index 98cb9ec..558fca1 100644 --- a/src/Http/Controllers/QueueMonitorController.php +++ b/src/Http/Controllers/QueueMonitorController.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Storvia\Vantage\Models\VantageJob; +use Storvia\Vantage\Support\JobRestorer; use Storvia\Vantage\Support\QueueDepthChecker; use Storvia\Vantage\Support\TagAggregator; use Storvia\Vantage\Support\VantageLogger; @@ -424,7 +425,7 @@ public function retry($id) } // Safely restore job from payload with security checks - $job = $this->restoreJobFromPayload($run, $jobClass); + $job = app(JobRestorer::class)->restore($run, $jobClass); if (! $job) { return back()->with('error', 'Unable to restore job. Payload might be missing or corrupted.'); @@ -450,92 +451,6 @@ public function retry($id) } } - /** - * Safely restore job from payload with security checks. - * - * @param VantageJob $run The job run record - * @param string $expectedJobClass The expected job class name for validation - * @return object|null The restored job object or null on failure - */ - protected function restoreJobFromPayload(VantageJob $run, string $expectedJobClass): ?object - { - if (! $run->payload) { - return null; - } - - // Validate expected class exists and is a valid job class - if (! class_exists($expectedJobClass)) { - VantageLogger::warning('Vantage: Expected job class does not exist', [ - 'run_id' => $run->id, - 'expected_class' => $expectedJobClass, - ]); - - return null; - } - - try { - $payload = is_array($run->payload) ? $run->payload : json_decode($run->payload, true); - - if (! is_array($payload)) { - VantageLogger::warning('Vantage: Invalid payload format', ['run_id' => $run->id]); - - return null; - } - - // Get the serialized command from Laravel's raw payload - $serialized = $payload['raw_payload']['data']['command'] ?? null; - - // Fallback to old format if new format not available - if (! $serialized) { - $serialized = $payload['data']['command'] ?? null; - } - - if (! $serialized || ! is_string($serialized)) { - VantageLogger::warning('Vantage: No serialized command in payload', ['run_id' => $run->id]); - - return null; - } - - // Unserialize with security: only allow the expected job class - $job = @unserialize($serialized, ['allowed_classes' => [$expectedJobClass]]); - - if (! is_object($job)) { - VantageLogger::warning('Vantage: Unserialize did not return object', [ - 'run_id' => $run->id, - 'result_type' => gettype($job), - ]); - - return null; - } - - // Double-check the class matches the expected class (security validation) - if (! $job instanceof $expectedJobClass) { - VantageLogger::warning('Vantage: Unserialized job class does not match expected class', [ - 'run_id' => $run->id, - 'expected_class' => $expectedJobClass, - 'actual_class' => get_class($job), - ]); - - return null; - } - - VantageLogger::info('Vantage: Successfully restored job', [ - 'run_id' => $run->id, - 'job_class' => get_class($job), - ]); - - return $job; - - } catch (\Throwable $e) { - VantageLogger::error('Vantage: Exception while restoring job from payload', [ - 'run_id' => $run->id, - 'error' => $e->getMessage(), - ]); - - return null; - } - } - /** * Get retry chain */ diff --git a/src/Http/Middleware/AuthorizeVantage.php b/src/Http/Middleware/AuthorizeVantage.php index 7229ac7..e35ed9e 100644 --- a/src/Http/Middleware/AuthorizeVantage.php +++ b/src/Http/Middleware/AuthorizeVantage.php @@ -13,7 +13,7 @@ class AuthorizeVantage /** * Handle an incoming request. * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + * @param Closure(Request): (Response) $next */ public function handle(Request $request, Closure $next): Response { diff --git a/src/Listeners/RecordJobSuccess.php b/src/Listeners/RecordJobSuccess.php index 11b11ed..c39fcb8 100644 --- a/src/Listeners/RecordJobSuccess.php +++ b/src/Listeners/RecordJobSuccess.php @@ -3,6 +3,7 @@ namespace Storvia\Vantage\Listeners; use Illuminate\Queue\Events\JobProcessed; +use Illuminate\Support\Str; use Storvia\Vantage\Models\VantageJob; use Storvia\Vantage\Support\JobPerformanceContext; use Storvia\Vantage\Support\PayloadExtractor; diff --git a/src/Models/VantageJob.php b/src/Models/VantageJob.php index bf9695b..d450d0b 100644 --- a/src/Models/VantageJob.php +++ b/src/Models/VantageJob.php @@ -4,8 +4,36 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Carbon; use Storvia\Vantage\Database\Factories\VantageJobFactory; +/** + * @property int $id + * @property string $status + * @property string $job_uuid + * @property string $job_class + * @property string $queue + * @property string|null $connection + * @property int $attempt + * @property int|null $retried_from_id + * @property array|null $payload + * @property array|null $job_tags + * @property string|null $exception_class + * @property string|null $exception_message + * @property string|null $stack + * @property Carbon|null $started_at + * @property Carbon|null $finished_at + * @property int|null $duration_ms + * @property int|null $memory_start_bytes + * @property int|null $memory_end_bytes + * @property int|null $memory_peak_start_bytes + * @property int|null $memory_peak_end_bytes + * @property int|null $memory_peak_delta_bytes + * @property int|null $cpu_user_ms + * @property int|null $cpu_sys_ms + * @property Carbon|null $created_at + * @property Carbon|null $updated_at + */ class VantageJob extends Model { use HasFactory; diff --git a/src/Support/JobRestorer.php b/src/Support/JobRestorer.php new file mode 100644 index 0000000..8c17def --- /dev/null +++ b/src/Support/JobRestorer.php @@ -0,0 +1,91 @@ +payload) { + return null; + } + + if (! class_exists($expectedJobClass)) { + VantageLogger::warning('Vantage: Expected job class does not exist', [ + 'job_id' => $job->id, + 'expected_class' => $expectedJobClass, + ]); + + return null; + } + + try { + $payload = is_array($job->payload) ? $job->payload : json_decode($job->payload, true); + + if (! is_array($payload)) { + VantageLogger::warning('Vantage: Invalid payload format', ['job_id' => $job->id]); + + return null; + } + + // Support both the new format (from PayloadExtractor) and the legacy format. + $serialized = $payload['raw_payload']['data']['command'] + ?? $payload['data']['command'] + ?? null; + + if (! $serialized || ! is_string($serialized)) { + VantageLogger::warning('Vantage: No serialized command in payload', ['job_id' => $job->id]); + + return null; + } + + $command = @unserialize($serialized, ['allowed_classes' => [$expectedJobClass]]); + + if (! is_object($command)) { + VantageLogger::warning('Vantage: Unserialize did not return an object', [ + 'job_id' => $job->id, + 'result_type' => gettype($command), + ]); + + return null; + } + + if (! $command instanceof $expectedJobClass) { + VantageLogger::warning('Vantage: Unserialized job class does not match expected class', [ + 'job_id' => $job->id, + 'expected_class' => $expectedJobClass, + 'actual_class' => get_class($command), + ]); + + return null; + } + + VantageLogger::info('Vantage: Successfully restored job from payload', [ + 'job_id' => $job->id, + 'job_class' => get_class($command), + ]); + + return $command; + + } catch (\Throwable $e) { + VantageLogger::error('Vantage: Exception while restoring job from payload', [ + 'job_id' => $job->id, + 'error' => $e->getMessage(), + ]); + + return null; + } + } +} diff --git a/src/Support/QueueDepthChecker.php b/src/Support/QueueDepthChecker.php index cc62f7b..ef53e2c 100644 --- a/src/Support/QueueDepthChecker.php +++ b/src/Support/QueueDepthChecker.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Redis; +use Storvia\Vantage\Models\VantageJob; class QueueDepthChecker { @@ -153,7 +154,7 @@ protected static function getFallbackQueueDepth(?string $queueName, string $driv // For unsupported drivers, we can still show what we know from job_runs // Count jobs that are processing or recently started (might be queued) try { - $query = \Storvia\Vantage\Models\VantageJob::where('status', 'processing'); + $query = VantageJob::where('status', 'processing'); if ($queueName) { $query->where('queue', $queueName); diff --git a/src/Support/TagAggregator.php b/src/Support/TagAggregator.php index 106c1cb..25acbbd 100644 --- a/src/Support/TagAggregator.php +++ b/src/Support/TagAggregator.php @@ -2,6 +2,8 @@ namespace Storvia\Vantage\Support; +use Carbon\Carbon; +use Illuminate\Database\Connection; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -28,7 +30,7 @@ class TagAggregator protected string $connectionName; - protected \Illuminate\Database\Connection $connection; + protected Connection $connection; protected string $jobsTable; @@ -47,7 +49,7 @@ public function __construct() /** * Get top tags with statistics, using the most efficient method available. * - * @param \Carbon\Carbon $since Filter jobs created after this date + * @param Carbon $since Filter jobs created after this date * @param int $limit Maximum number of tags to return * @return Collection Collection of tag statistics */ @@ -65,7 +67,7 @@ public function getTopTags($since, int $limit = 10): Collection /** * Get detailed tag statistics including duration averages. * - * @param \Carbon\Carbon $since Filter jobs created after this date + * @param Carbon $since Filter jobs created after this date * @return array Associative array of tag => statistics */ public function getTagStats($since): array @@ -602,7 +604,7 @@ protected function formatTagStatsResult($rows): array * * @param int $jobId The job ID * @param array $tags Array of tag strings - * @param \Carbon\Carbon|null $createdAt The job's created_at timestamp + * @param Carbon|null $createdAt The job's created_at timestamp */ public function insertJobTags(int $jobId, array $tags, $createdAt = null): void { @@ -665,7 +667,7 @@ public function deleteJobTags(int $jobId): void * Delete old tags based on job created_at timestamp. * Used by the prune command. * - * @param \Carbon\Carbon $before Delete tags for jobs created before this date + * @param Carbon $before Delete tags for jobs created before this date * @return int Number of tags deleted */ public function pruneOldTags($before): int diff --git a/src/Vantage.php b/src/Vantage.php index 429dc09..1bdce25 100644 --- a/src/Vantage.php +++ b/src/Vantage.php @@ -2,9 +2,12 @@ namespace Storvia\Vantage; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Queue\Jobs\Job; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Storvia\Vantage\Models\VantageJob; +use Storvia\Vantage\Support\JobRestorer; use Storvia\Vantage\Support\QueueDepthChecker; use Storvia\Vantage\Support\VantageLogger; @@ -15,7 +18,7 @@ class Vantage */ public function queueDepth(?string $queue = null): Collection { - return app(QueueDepthChecker::class)->check($queue); + return collect(QueueDepthChecker::getQueueDepth($queue)); } /** @@ -106,13 +109,13 @@ public function retryJob(int $jobId): bool } // Validate it's a valid job class (implements ShouldQueue or extends Job) - if (! is_subclass_of($expectedJobClass, \Illuminate\Contracts\Queue\ShouldQueue::class) && - ! is_subclass_of($expectedJobClass, \Illuminate\Queue\Jobs\Job::class)) { + if (! is_subclass_of($expectedJobClass, ShouldQueue::class) && + ! is_subclass_of($expectedJobClass, Job::class)) { return false; } // Try to restore job from payload with safety checks - $command = $this->restoreJobFromPayload($job, $expectedJobClass); + $command = app(JobRestorer::class)->restore($job, $expectedJobClass); if (! $command) { // Only allow fallback if payload is completely missing (not corrupted/malicious) @@ -141,53 +144,6 @@ public function retryJob(int $jobId): bool return true; } - /** - * Safely restore job from payload with security checks. - */ - protected function restoreJobFromPayload(VantageJob $job, string $expectedJobClass): ?object - { - if (! $job->payload) { - return null; - } - - try { - $payload = is_array($job->payload) ? $job->payload : json_decode($job->payload, true); - - if (! is_array($payload)) { - return null; - } - - // Try new format first (from PayloadExtractor) - $serialized = $payload['raw_payload']['data']['command'] ?? null; - - // Fallback to old format - if (! $serialized) { - $serialized = $payload['data']['command'] ?? null; - } - - if (! $serialized || ! is_string($serialized)) { - return null; - } - - // Unserialize with allowed_classes restriction - only allow the expected class - $command = @unserialize($serialized, ['allowed_classes' => [$expectedJobClass]]); - - // Validate the result - if (! is_object($command)) { - return null; - } - - // Double-check the class matches - if (! $command instanceof $expectedJobClass) { - return null; - } - - return $command; - } catch (\Throwable $e) { - return null; - } - } - /** * Clean up stuck processing jobs. */ diff --git a/tests/Feature/JobListenersTest.php b/tests/Feature/JobListenersTest.php index 0c6bb92..5438f4d 100644 --- a/tests/Feature/JobListenersTest.php +++ b/tests/Feature/JobListenersTest.php @@ -112,7 +112,7 @@ public function resolveName() 'started_at' => now()->subSeconds(5), ]); - $exception = new \Exception('Test exception message'); + $exception = new Exception('Test exception message'); $job = new class { public $queue = 'default'; @@ -234,7 +234,7 @@ public function payload() } }; - $exception = new \Exception('Test error message', 500); + $exception = new Exception('Test error message', 500); $event = new JobFailed('test-connection', $job, $exception); $listener = new RecordJobFailure; diff --git a/tests/Feature/UnserializeSecurityTest.php b/tests/Feature/UnserializeSecurityTest.php index 71bb0ba..85eeaf5 100644 --- a/tests/Feature/UnserializeSecurityTest.php +++ b/tests/Feature/UnserializeSecurityTest.php @@ -2,10 +2,11 @@ namespace Storvia\Vantage\Tests\Feature; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Str; -use Storvia\Vantage\Http\Controllers\QueueMonitorController; use Storvia\Vantage\Models\VantageJob; +use Storvia\Vantage\Support\JobRestorer; use Storvia\Vantage\Vantage; beforeEach(function () { @@ -14,7 +15,7 @@ class TestSecureJob implements ShouldQueue { - use \Illuminate\Bus\Queueable; + use Queueable; public $testProperty; @@ -158,8 +159,8 @@ class NotAJobClass expect($result)->toBeFalse(); }); -it('prevents unserializing malicious classes in QueueMonitorController::restoreJobFromPayload', function () { - $controller = new QueueMonitorController; +it('prevents unserializing malicious classes in JobRestorer::restore', function () { + $restorer = new JobRestorer; $maliciousSerialized = 'O:15:"MaliciousClass":0:{}'; @@ -177,17 +178,13 @@ class NotAJobClass ]), ]); - $reflection = new \ReflectionClass($controller); - $method = $reflection->getMethod('restoreJobFromPayload'); - $method->setAccessible(true); - - $result = $method->invoke($controller, $job, TestSecureJob::class); + $result = $restorer->restore($job, TestSecureJob::class); expect($result)->toBeNull(); }); -it('prevents unserializing wrong class in QueueMonitorController::restoreJobFromPayload', function () { - $controller = new QueueMonitorController; +it('prevents unserializing wrong class in JobRestorer::restore', function () { + $restorer = new JobRestorer; $job = VantageJob::create([ 'uuid' => Str::uuid(), @@ -203,17 +200,13 @@ class NotAJobClass ]), ]); - $reflection = new \ReflectionClass($controller); - $method = $reflection->getMethod('restoreJobFromPayload'); - $method->setAccessible(true); - - $result = $method->invoke($controller, $job, TestSecureJob::class); + $result = $restorer->restore($job, TestSecureJob::class); expect($result)->toBeNull(); }); -it('allows restoring valid job in QueueMonitorController::restoreJobFromPayload', function () { - $controller = new QueueMonitorController; +it('allows restoring valid job in JobRestorer::restore', function () { + $restorer = new JobRestorer; $testJob = new TestSecureJob('test-value'); $job = VantageJob::create([ @@ -230,11 +223,7 @@ class NotAJobClass ]), ]); - $reflection = new \ReflectionClass($controller); - $method = $reflection->getMethod('restoreJobFromPayload'); - $method->setAccessible(true); - - $result = $method->invoke($controller, $job, TestSecureJob::class); + $result = $restorer->restore($job, TestSecureJob::class); expect($result)->not->toBeNull() ->and($result)->toBeInstanceOf(TestSecureJob::class) diff --git a/tests/Unit/JobPerformanceContextTest.php b/tests/Unit/JobPerformanceContextTest.php index f940622..74261ec 100644 --- a/tests/Unit/JobPerformanceContextTest.php +++ b/tests/Unit/JobPerformanceContextTest.php @@ -4,7 +4,7 @@ beforeEach(function () { // Clear baselines before each test - $reflection = new \ReflectionClass(JobPerformanceContext::class); + $reflection = new ReflectionClass(JobPerformanceContext::class); $property = $reflection->getProperty('baselines'); $property->setAccessible(true); $property->setValue(null, []); diff --git a/tests/Unit/VantageJobTest.php b/tests/Unit/VantageJobTest.php index 2aec555..307b5b5 100644 --- a/tests/Unit/VantageJobTest.php +++ b/tests/Unit/VantageJobTest.php @@ -1,5 +1,6 @@ '2024-01-01 10:05:00', ]); - expect($job->started_at)->toBeInstanceOf(\Illuminate\Support\Carbon::class) - ->and($job->finished_at)->toBeInstanceOf(\Illuminate\Support\Carbon::class); + expect($job->started_at)->toBeInstanceOf(Carbon::class) + ->and($job->finished_at)->toBeInstanceOf(Carbon::class); }); it('has retried_from relationship', function () {