From 029303642fbf1a2b2a1680f66b8751104f0a01bb Mon Sep 17 00:00:00 2001 From: Jamison Bryant Date: Tue, 31 Mar 2026 10:46:07 -0400 Subject: [PATCH 1/2] Add TaskMetadata for instantiation-free task introspection Read task property defaults (timeout, retries, rate, costs, unique) and description via reflection instead of instantiating the class. This supports tasks with required DI constructor parameters where new $className() would fail with ArgumentCountError. --- src/Queue/TaskMetadata.php | 78 +++++++++++++++++++ tests/TestCase/Queue/TaskMetadataTest.php | 57 ++++++++++++++ .../test_app/src/Queue/Task/MetadataTask.php | 33 ++++++++ 3 files changed, 168 insertions(+) create mode 100644 src/Queue/TaskMetadata.php create mode 100644 tests/TestCase/Queue/TaskMetadataTest.php create mode 100644 tests/test_app/src/Queue/Task/MetadataTask.php diff --git a/src/Queue/TaskMetadata.php b/src/Queue/TaskMetadata.php new file mode 100644 index 00000000..d0d87267 --- /dev/null +++ b/src/Queue/TaskMetadata.php @@ -0,0 +1,78 @@ + $className + * + * @param string $className + * + * @return self + */ + public static function fromClass(string $className): self { + $reflection = new ReflectionClass($className); + + $timeout = static::propertyDefault($reflection, 'timeout'); + $retries = static::propertyDefault($reflection, 'retries'); + $rate = static::propertyDefault($reflection, 'rate') ?? 0; + $costs = static::propertyDefault($reflection, 'costs') ?? 0; + $unique = static::propertyDefault($reflection, 'unique') ?? false; + + $description = null; + try { + /** @var \Queue\Queue\Task $instance */ + $instance = $reflection->newInstanceWithoutConstructor(); + $description = $instance->description(); + } catch (Throwable) { + } + + return new self($className, $timeout, $retries, $rate, $costs, $unique, $description); + } + + /** + * @param \ReflectionClass<\Queue\Queue\Task> $reflection + * @param string $property + * + * @return mixed + */ + protected static function propertyDefault(ReflectionClass $reflection, string $property): mixed { + if (!$reflection->hasProperty($property)) { + return null; + } + + $prop = $reflection->getProperty($property); + if (!$prop->hasDefaultValue()) { + return null; + } + + return $prop->getDefaultValue(); + } + +} diff --git a/tests/TestCase/Queue/TaskMetadataTest.php b/tests/TestCase/Queue/TaskMetadataTest.php new file mode 100644 index 00000000..551456e1 --- /dev/null +++ b/tests/TestCase/Queue/TaskMetadataTest.php @@ -0,0 +1,57 @@ +assertSame(MetadataTask::class, $meta->class); + $this->assertSame(42, $meta->timeout); + $this->assertSame(3, $meta->retries); + $this->assertSame(5, $meta->rate); + $this->assertSame(75, $meta->costs); + $this->assertTrue($meta->unique); + } + + /** + * Test that fromClass reads description() from tasks that override it. + * + * @return void + */ + public function testFromClassReadsDescription(): void { + $meta = TaskMetadata::fromClass(MetadataTask::class); + + $this->assertSame('A task for testing metadata introspection', $meta->description); + } + + /** + * Test that fromClass works for tasks with required constructor DI params + * that cannot be instantiated with new $className(). + * + * @return void + */ + public function testFromClassWithDiConstructor(): void { + $meta = TaskMetadata::fromClass(InjectedTask::class); + + $this->assertSame(InjectedTask::class, $meta->class); + $this->assertSame(10, $meta->timeout); + $this->assertNull($meta->retries); + $this->assertSame(0, $meta->rate); + $this->assertSame(0, $meta->costs); + $this->assertFalse($meta->unique); + } + +} diff --git a/tests/test_app/src/Queue/Task/MetadataTask.php b/tests/test_app/src/Queue/Task/MetadataTask.php new file mode 100644 index 00000000..76afd5e4 --- /dev/null +++ b/tests/test_app/src/Queue/Task/MetadataTask.php @@ -0,0 +1,33 @@ + Date: Tue, 31 Mar 2026 10:46:26 -0400 Subject: [PATCH 2/2] Replace direct task instantiation with TaskMetadata Update Config::taskConfig() and QueueController::index() to use TaskMetadata::fromClass() instead of new $className() for reading task properties and descriptions. Also use is_subclass_of() for interface checks in addJob() to avoid instantiating DI tasks. --- src/Controller/Admin/QueueController.php | 9 ++++----- src/Queue/Config.php | 17 +++++++---------- tests/TestCase/Command/InfoCommandTest.php | 2 +- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Controller/Admin/QueueController.php b/src/Controller/Admin/QueueController.php index fd7bb282..10c201cf 100644 --- a/src/Controller/Admin/QueueController.php +++ b/src/Controller/Admin/QueueController.php @@ -9,6 +9,7 @@ use Queue\Queue\AddFromBackendInterface; use Queue\Queue\AddInterface; use Queue\Queue\TaskFinder; +use Queue\Queue\TaskMetadata; /** * @property \Queue\Model\Table\QueuedJobsTable $QueuedJobs @@ -66,9 +67,7 @@ public function index() { $taskDescriptions = []; foreach ($tasks as $task => $className) { - /** @var \Queue\Queue\Task $taskObject */ - $taskObject = new $className(); - $taskDescriptions[$task] = $taskObject->description(); + $taskDescriptions[$task] = TaskMetadata::fromClass($className)->description; } $servers = $QueueProcesses->serverList(); @@ -132,8 +131,8 @@ public function addJob() { throw new NotFoundException('Class not found for job `' . $job . '`'); } - $object = new $className(); - if ($object instanceof AddInterface) { + if (is_subclass_of($className, AddInterface::class)) { + $object = new $className(); $object->add(null); } else { $this->QueuedJobs->createJob($job); diff --git a/src/Queue/Config.php b/src/Queue/Config.php index 3bc3d32c..e72ec457 100644 --- a/src/Queue/Config.php +++ b/src/Queue/Config.php @@ -120,7 +120,7 @@ public static function ignoredTasks(): array { } /** - * @param array $tasks + * @param array> $tasks * * @throws \RuntimeException * @@ -134,13 +134,12 @@ public static function taskConfig(array $tasks): array { foreach ($tasks as $task => $className) { [$pluginName, $taskName] = pluginSplit($task); - /** @var \Queue\Queue\Task $taskObject */ - $taskObject = new $className(); + $taskMeta = TaskMetadata::fromClass($className); // Get task-specific config overrides from Configure $taskConfig = $taskOverrides[$task] ?? []; - $taskTimeout = $taskConfig['timeout'] ?? $taskObject->timeout ?? $defaultTimeout; + $taskTimeout = $taskConfig['timeout'] ?? $taskMeta->timeout ?? $defaultTimeout; // Auto-cap task timeout to defaultRequeueTimeout to prevent duplicate execution if ($taskTimeout > $defaultTimeout) { @@ -151,12 +150,10 @@ public static function taskConfig(array $tasks): array { $config[$task]['name'] = $taskName; $config[$task]['plugin'] = $pluginName; $config[$task]['timeout'] = $taskTimeout; - $config[$task]['retries'] = $taskConfig['retries'] ?? $taskObject->retries ?? static::defaultworkerretries(); - $config[$task]['rate'] = $taskConfig['rate'] ?? $taskObject->rate; - $config[$task]['costs'] = $taskConfig['costs'] ?? $taskObject->costs; - $config[$task]['unique'] = $taskConfig['unique'] ?? $taskObject->unique; - - unset($taskObject); + $config[$task]['retries'] = $taskConfig['retries'] ?? $taskMeta->retries ?? static::defaultworkerretries(); + $config[$task]['rate'] = $taskConfig['rate'] ?? $taskMeta->rate; + $config[$task]['costs'] = $taskConfig['costs'] ?? $taskMeta->costs; + $config[$task]['unique'] = $taskConfig['unique'] ?? $taskMeta->unique; } return $config; diff --git a/tests/TestCase/Command/InfoCommandTest.php b/tests/TestCase/Command/InfoCommandTest.php index cf512562..b0e1bd0d 100644 --- a/tests/TestCase/Command/InfoCommandTest.php +++ b/tests/TestCase/Command/InfoCommandTest.php @@ -38,7 +38,7 @@ public function testExecute(): void { $this->exec('queue info'); $output = $this->_out->output(); - $this->assertStringContainsString('16 tasks available:', $output); + $this->assertStringContainsString('17 tasks available:', $output); $this->assertExitCode(0); }