From 90fbd0b84ad8abd978eaaa369cd87ada8088d24b Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Fri, 5 Jun 2026 16:34:12 +0200 Subject: [PATCH 1/2] Assign AllCommand subcommands in the constructor 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. --- src/Command/ControllerAllCommand.php | 11 +++++++---- src/Command/ModelAllCommand.php | 11 +++++++---- src/Command/TemplateAllCommand.php | 11 +++++++---- .../Command/ControllerAllCommandTest.php | 16 ++++++++++++++++ tests/TestCase/Command/ModelAllCommandTest.php | 16 ++++++++++++++++ .../TestCase/Command/TemplateAllCommandTest.php | 16 ++++++++++++++++ 6 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/Command/ControllerAllCommand.php b/src/Command/ControllerAllCommand.php index 43ffb3ca..73179725 100644 --- a/src/Command/ControllerAllCommand.php +++ b/src/Command/ControllerAllCommand.php @@ -18,6 +18,7 @@ use Bake\Utility\TableScanner; use Cake\Console\Arguments; +use Cake\Console\CommandFactoryInterface; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Datasource\ConnectionManager; @@ -41,13 +42,15 @@ public static function defaultName(): string } /** - * initialize + * The subcommand is assigned in the constructor (not initialize()) because + * buildOptionParser() runs before initialize() and reads it. * - * @return void + * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance. */ - public function initialize(): void + public function __construct(?CommandFactoryInterface $factory = null) { - parent::initialize(); + parent::__construct($factory); + $this->controllerCommand = new ControllerCommand(); } diff --git a/src/Command/ModelAllCommand.php b/src/Command/ModelAllCommand.php index 2bbae1a7..51faf8f1 100644 --- a/src/Command/ModelAllCommand.php +++ b/src/Command/ModelAllCommand.php @@ -18,6 +18,7 @@ use Bake\Utility\TableScanner; use Cake\Console\Arguments; +use Cake\Console\CommandFactoryInterface; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Datasource\ConnectionManager; @@ -41,13 +42,15 @@ public static function defaultName(): string } /** - * initialize + * The subcommand is assigned in the constructor (not initialize()) because + * buildOptionParser() runs before initialize() and reads it. * - * @return void + * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance. */ - public function initialize(): void + public function __construct(?CommandFactoryInterface $factory = null) { - parent::initialize(); + parent::__construct($factory); + $this->modelCommand = new ModelCommand(); } diff --git a/src/Command/TemplateAllCommand.php b/src/Command/TemplateAllCommand.php index 0a1c72e5..953d3c33 100644 --- a/src/Command/TemplateAllCommand.php +++ b/src/Command/TemplateAllCommand.php @@ -18,6 +18,7 @@ use Bake\Utility\TableScanner; use Cake\Console\Arguments; +use Cake\Console\CommandFactoryInterface; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Datasource\ConnectionManager; @@ -38,13 +39,15 @@ public static function defaultName(): string } /** - * initialize + * The subcommand is assigned in the constructor (not initialize()) because + * buildOptionParser() runs before initialize() and reads it. * - * @return void + * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance. */ - public function initialize(): void + public function __construct(?CommandFactoryInterface $factory = null) { - parent::initialize(); + parent::__construct($factory); + $this->templateCommand = new TemplateCommand(); } diff --git a/tests/TestCase/Command/ControllerAllCommandTest.php b/tests/TestCase/Command/ControllerAllCommandTest.php index 4b70b5d1..e5882a74 100644 --- a/tests/TestCase/Command/ControllerAllCommandTest.php +++ b/tests/TestCase/Command/ControllerAllCommandTest.php @@ -16,9 +16,11 @@ */ namespace Bake\Test\TestCase\Command; +use Bake\Command\ControllerAllCommand; use Bake\Test\App\Model\Table\BakeArticlesTable; use Bake\Test\TestCase\TestCase; use Cake\Console\CommandInterface; +use Cake\Console\ConsoleOptionParser; use Cake\Core\Plugin; use Cake\Utility\Inflector; @@ -91,4 +93,18 @@ public function testExecute(): void 'Test should not be created as options should be forwarded', ); } + + /** + * The option parser is built before initialize() runs, so the wrapped subcommand + * must already be available at that point. Regression test for the subcommand being + * assigned in initialize() instead of the constructor. + * + * @return void + */ + public function testGetOptionParserBeforeInitialize(): void + { + $command = new ControllerAllCommand(); + + $this->assertInstanceOf(ConsoleOptionParser::class, $command->getOptionParser()); + } } diff --git a/tests/TestCase/Command/ModelAllCommandTest.php b/tests/TestCase/Command/ModelAllCommandTest.php index 59bb054f..ae92a2db 100644 --- a/tests/TestCase/Command/ModelAllCommandTest.php +++ b/tests/TestCase/Command/ModelAllCommandTest.php @@ -16,9 +16,11 @@ */ namespace Bake\Test\TestCase\Command; +use Bake\Command\ModelAllCommand; use Bake\Test\TestCase\TestCase; use Bake\Utility\SubsetSchemaCollection; use Cake\Console\CommandInterface; +use Cake\Console\ConsoleOptionParser; use Cake\Datasource\ConnectionManager; use Cake\Utility\Inflector; @@ -99,4 +101,18 @@ public function testExecute(): void 'Table test should not be created as options should be forwarded', ); } + + /** + * The option parser is built before initialize() runs, so the wrapped subcommand + * must already be available at that point. Regression test for the subcommand being + * assigned in initialize() instead of the constructor. + * + * @return void + */ + public function testGetOptionParserBeforeInitialize(): void + { + $command = new ModelAllCommand(); + + $this->assertInstanceOf(ConsoleOptionParser::class, $command->getOptionParser()); + } } diff --git a/tests/TestCase/Command/TemplateAllCommandTest.php b/tests/TestCase/Command/TemplateAllCommandTest.php index c420da3f..1f2d8bc5 100644 --- a/tests/TestCase/Command/TemplateAllCommandTest.php +++ b/tests/TestCase/Command/TemplateAllCommandTest.php @@ -16,9 +16,11 @@ */ namespace Bake\Test\TestCase\Command; +use Bake\Command\TemplateAllCommand; use Bake\Test\TestCase\TestCase; use Bake\Utility\SubsetSchemaCollection; use Cake\Console\CommandInterface; +use Cake\Console\ConsoleOptionParser; use Cake\Core\Plugin; use Cake\Datasource\ConnectionManager; @@ -122,4 +124,18 @@ public function testExecuteOptionForwarding(): void $this->assertFileContains('title', $this->generatedFiles[0]); $this->assertFileNotContains('published', $this->generatedFiles[0]); } + + /** + * The option parser is built before initialize() runs, so the wrapped subcommand + * must already be available at that point. Regression test for the subcommand being + * assigned in initialize() instead of the constructor. + * + * @return void + */ + public function testGetOptionParserBeforeInitialize(): void + { + $command = new TemplateAllCommand(); + + $this->assertInstanceOf(ConsoleOptionParser::class, $command->getOptionParser()); + } } From f73d8485de461fc9c41e840e578b1c7298c28eba Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Fri, 5 Jun 2026 16:55:59 +0200 Subject: [PATCH 2/2] Lazy-assign subcommand in buildOptionParser for 5.1+ compatibility 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. --- src/Command/ControllerAllCommand.php | 19 +++++-------------- src/Command/ModelAllCommand.php | 19 +++++-------------- src/Command/TemplateAllCommand.php | 19 +++++-------------- 3 files changed, 15 insertions(+), 42 deletions(-) diff --git a/src/Command/ControllerAllCommand.php b/src/Command/ControllerAllCommand.php index 73179725..c1bc2009 100644 --- a/src/Command/ControllerAllCommand.php +++ b/src/Command/ControllerAllCommand.php @@ -18,7 +18,6 @@ use Bake\Utility\TableScanner; use Cake\Console\Arguments; -use Cake\Console\CommandFactoryInterface; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Datasource\ConnectionManager; @@ -41,19 +40,6 @@ public static function defaultName(): string return 'bake controller all'; } - /** - * The subcommand is assigned in the constructor (not initialize()) because - * buildOptionParser() runs before initialize() and reads it. - * - * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance. - */ - public function __construct(?CommandFactoryInterface $factory = null) - { - parent::__construct($factory); - - $this->controllerCommand = new ControllerCommand(); - } - /** * Execute the command. * @@ -85,6 +71,11 @@ public function execute(Arguments $args, ConsoleIo $io): ?int */ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { + // Assigned here (not initialize()) because on CakePHP 5.4+ the parser is built + // before initialize() runs, while older versions build it after. ??= keeps it + // safe under either ordering and idempotent across repeated calls. + $this->controllerCommand ??= new ControllerCommand(); + $parser = $this->controllerCommand->buildOptionParser($parser); $parser ->setDescription('Bake all controller files with tests.') diff --git a/src/Command/ModelAllCommand.php b/src/Command/ModelAllCommand.php index 51faf8f1..8730e99d 100644 --- a/src/Command/ModelAllCommand.php +++ b/src/Command/ModelAllCommand.php @@ -18,7 +18,6 @@ use Bake\Utility\TableScanner; use Cake\Console\Arguments; -use Cake\Console\CommandFactoryInterface; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Datasource\ConnectionManager; @@ -41,19 +40,6 @@ public static function defaultName(): string return 'bake model all'; } - /** - * The subcommand is assigned in the constructor (not initialize()) because - * buildOptionParser() runs before initialize() and reads it. - * - * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance. - */ - public function __construct(?CommandFactoryInterface $factory = null) - { - parent::__construct($factory); - - $this->modelCommand = new ModelCommand(); - } - /** * Gets the option parser instance and configures it. * @@ -62,6 +48,11 @@ public function __construct(?CommandFactoryInterface $factory = null) */ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { + // Assigned here (not initialize()) because on CakePHP 5.4+ the parser is built + // before initialize() runs, while older versions build it after. ??= keeps it + // safe under either ordering and idempotent across repeated calls. + $this->modelCommand ??= new ModelCommand(); + $parser = $this->modelCommand->buildOptionParser($parser); $parser ->setDescription('Bake all model files with associations and validation.') diff --git a/src/Command/TemplateAllCommand.php b/src/Command/TemplateAllCommand.php index 953d3c33..2c7ff454 100644 --- a/src/Command/TemplateAllCommand.php +++ b/src/Command/TemplateAllCommand.php @@ -18,7 +18,6 @@ use Bake\Utility\TableScanner; use Cake\Console\Arguments; -use Cake\Console\CommandFactoryInterface; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Datasource\ConnectionManager; @@ -38,19 +37,6 @@ public static function defaultName(): string return 'bake template all'; } - /** - * The subcommand is assigned in the constructor (not initialize()) because - * buildOptionParser() runs before initialize() and reads it. - * - * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance. - */ - public function __construct(?CommandFactoryInterface $factory = null) - { - parent::__construct($factory); - - $this->templateCommand = new TemplateCommand(); - } - /** * Execute the command. * @@ -88,6 +74,11 @@ public function execute(Arguments $args, ConsoleIo $io): int */ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { + // Assigned here (not initialize()) because on CakePHP 5.4+ the parser is built + // before initialize() runs, while older versions build it after. buildOptionParser() + // always runs before execute(), so this guarantees the subcommand is available there. + $this->templateCommand ??= new TemplateCommand(); + $parser = $this->_setCommonOptions($parser); $parser ->setDescription('Bake all view template files.')