Skip to content

Assign AllCommand subcommands in the constructor (fix uninitialized typed property on CakePHP 5.4)#1091

Open
dereuromark wants to merge 2 commits into
3.nextfrom
fix-allcommand-uninitialized-subcommand
Open

Assign AllCommand subcommands in the constructor (fix uninitialized typed property on CakePHP 5.4)#1091
dereuromark wants to merge 2 commits into
3.nextfrom
fix-allcommand-uninitialized-subcommand

Conversation

@dereuromark
Copy link
Copy Markdown
Member

@dereuromark dereuromark commented Jun 5, 2026

Fixes Cluster B of cakephp/cakephp#19491.

quite a heavy change though that most likely still leaves other third party commands affected.

Problem

ControllerAllCommand, ModelAllCommand and TemplateAllCommand declare their wrapped subcommand as a non-nullable typed property and assign it in initialize():

protected ControllerCommand $controllerCommand;

public function initialize(): void
{
    parent::initialize();
    $this->controllerCommand = new ControllerCommand();
}

On CakePHP 5.4, BaseCommand::run() builds the option parser before calling initialize(). buildOptionParser() reads the subcommand, which is still uninitialized at that point:

Typed property Bake\Command\ControllerAllCommand::$controllerCommand
must not be accessed before initialization

This surfaced as errors in ControllerAllCommandTest::testExecute and ModelAllCommandTest::testExecute. TemplateAllCommand never reads its subcommand in buildOptionParser(), so its breakage was latent.

Fix

Assign the subcommand in the constructor, so it is available regardless of the run/initialize ordering:

public function __construct(?CommandFactoryInterface $factory = null)
{
    parent::__construct($factory);

    $this->controllerCommand = new ControllerCommand();
}

The initialize() override is dropped; the inherited BakeCommand::initialize() (table-locator fallback) still runs. All three commands are kept consistent.

Tests

Added a testGetOptionParserBeforeInitialize to each command test that calls getOptionParser() directly on a freshly constructed command - the exact pre-initialize() path the runner takes. These reproduce the reported error on the old code and pass with the fix, independent of the installed CakePHP version.

ControllerAllCommand, ModelAllCommand and TemplateAllCommand declared their
wrapped subcommand as a non-nullable typed property and assigned it in
initialize(). On CakePHP 5.4 BaseCommand::run() builds the option parser
before calling initialize(), so buildOptionParser() reads the property while
it is still uninitialized:

    Typed property Bake\Command\ControllerAllCommand::$controllerCommand
    must not be accessed before initialization

Move the assignment into the constructor so the subcommand is available
regardless of the lifecycle ordering. The override of initialize() is dropped;
the inherited BakeCommand::initialize() (table-locator fallback) still runs.
TemplateAllCommand never read the property in buildOptionParser(), so its
breakage was latent; the change keeps the three commands consistent.
@dereuromark dereuromark marked this pull request as draft June 5, 2026 14:52
The constructor approach broke on cakephp/cakephp 5.1, where BaseCommand has
no constructor yet (Error: Cannot call constructor via parent::__construct()).
Bake supports ^5.1.

Assign the wrapped subcommand lazily in buildOptionParser() with ??= instead.
buildOptionParser() always runs before execute() on every supported version,
and on 5.4 it runs before initialize() too, so the property is always set
before use regardless of lifecycle ordering. ??= is safe on an uninitialized
typed property and idempotent across repeated parser builds.
@dereuromark dereuromark marked this pull request as ready for review June 5, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants