Problem
Tasks cannot use constructor-based dependency injection. The only DI mechanism is ServicesTrait::getService() inside run(), since setContainer() is called after construction. This prevents readonly promoted properties and doesn't match the pattern CakePHP uses for Components and Commands.
How Components do it
CakePHP Components accept DI dependencies alongside the framework-required ComponentRegistry in their constructor:
public function __construct(
ComponentRegistry $registry,
private UserService $users,
array $config = [],
) {
parent::__construct($registry, $config);
}
The DI container resolves all arguments. The component just passes the framework args to the parent.
Why tasks can't do this today
Processor::loadTask() hard-codes new $className($this->io, $this->logger). If a task declares additional constructor parameters, instantiation fails because the Processor only passes two arguments.
The Processor already has access to the DI container ($this->container) but doesn't use it for task resolution.
Proposed change
In Processor::loadTask(), try the container first for explicitly registered tasks, fall through to direct instantiation for everything else:
protected function loadTask(string $taskName): TaskInterface {
$className = $this->getTaskClass($taskName);
if ($this->container && $this->container->has($className)) {
$task = $this->container->get($className);
} else {
$task = new $className($this->io, $this->logger);
}
if (!$task instanceof TaskInterface) {
throw new RuntimeException('Task must implement ' . TaskInterface::class);
}
return $task;
}
Remaining issue: Io and LoggerInterface
The Processor creates runtime-specific Io and LoggerInterface instances (based on CLI flags like --quiet, --logger). These aren't in the app's DI container.
For container-resolved tasks, the base Task::__construct() defaults Io to new Io(new ConsoleIo()) when null. This means container-resolved tasks would get a different Io than the Processor's, causing output capture and logging to misbehave.
A few ways to solve this:
A) Add setIo() / setLogger() to the base Task, called by the Processor after loadTask() regardless of instantiation path. Simple, explicit.
B) Have the Processor register its Io and LoggerInterface in the container before resolving tasks.
C) Accept that container-resolved tasks include Io and LoggerInterface in their constructor (like Components include ComponentRegistry), registered as arguments in the service provider. The Processor would need to ensure these are available in the container.
Backward compatibility
Fully backward-compatible. Tasks not registered in the container are instantiated exactly as today. The ServicesTrait / setContainer() pattern continues to work.
Desired end state
// Service provider registration
$container->add(EndEventTask::class)
->addArgument(Io::class)
->addArgument(LoggerInterface::class)
->addArgument(MyService::class);
// Task class
class EndEventTask extends Task
{
public function __construct(
?Io $io = null,
?LoggerInterface $logger = null,
protected readonly MyService $myService,
) {
parent::__construct($io, $logger);
}
public function run(array $data, int $jobId): void
{
$this->myService->doWork($data);
}
}
Happy to contribute a PR if there's a preferred direction for the Io/LoggerInterface resolution.
Problem
Tasks cannot use constructor-based dependency injection. The only DI mechanism is
ServicesTrait::getService()insiderun(), sincesetContainer()is called after construction. This preventsreadonlypromoted properties and doesn't match the pattern CakePHP uses for Components and Commands.How Components do it
CakePHP Components accept DI dependencies alongside the framework-required
ComponentRegistryin their constructor:The DI container resolves all arguments. The component just passes the framework args to the parent.
Why tasks can't do this today
Processor::loadTask()hard-codesnew $className($this->io, $this->logger). If a task declares additional constructor parameters, instantiation fails because the Processor only passes two arguments.The Processor already has access to the DI container (
$this->container) but doesn't use it for task resolution.Proposed change
In
Processor::loadTask(), try the container first for explicitly registered tasks, fall through to direct instantiation for everything else:Remaining issue: Io and LoggerInterface
The Processor creates runtime-specific
IoandLoggerInterfaceinstances (based on CLI flags like--quiet,--logger). These aren't in the app's DI container.For container-resolved tasks, the base
Task::__construct()defaultsIotonew Io(new ConsoleIo())when null. This means container-resolved tasks would get a different Io than the Processor's, causing output capture and logging to misbehave.A few ways to solve this:
A) Add
setIo()/setLogger()to the baseTask, called by the Processor afterloadTask()regardless of instantiation path. Simple, explicit.B) Have the Processor register its
IoandLoggerInterfacein the container before resolving tasks.C) Accept that container-resolved tasks include
IoandLoggerInterfacein their constructor (like Components includeComponentRegistry), registered as arguments in the service provider. The Processor would need to ensure these are available in the container.Backward compatibility
Fully backward-compatible. Tasks not registered in the container are instantiated exactly as today. The
ServicesTrait/setContainer()pattern continues to work.Desired end state
Happy to contribute a PR if there's a preferred direction for the Io/LoggerInterface resolution.