diff --git a/composer.json b/composer.json index 352018c4..d08c73ef 100644 --- a/composer.json +++ b/composer.json @@ -22,16 +22,16 @@ "source": "https://github.com/cakephp/bake" }, "require": { - "php": ">=8.1", - "brick/varexporter": "^0.6.0 || ^0.7.0", - "cakephp/cakephp": "^5.1", - "cakephp/twig-view": "^2.0.2", + "php": ">=8.2", + "brick/varexporter": "^0.7.0", + "cakephp/cakephp": "^5.4", + "cakephp/twig-view": "^2.1", "nikic/php-parser": "^5.0.0" }, "require-dev": { "cakephp/cakephp-codesniffer": "^5.0.0", "cakephp/debug_kit": "^5.0.0", - "phpunit/phpunit": "^10.5.40 || ^11.5.20 || ^12.2.4 || ^13.0" + "phpunit/phpunit": "^11.5.20 || ^12.2.4 || ^13.0" }, "autoload": { "psr-4": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e45840cf..de4a29d8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,11 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Method Bake\\BakePlugin\:\:bootstrap\(\) has parameter \$app with generic interface Cake\\Core\\PluginApplicationInterface but does not specify its types\: TSubject$#' - identifier: missingType.generics - count: 1 - path: src/BakePlugin.php - - message: '#^Instanceof between mixed and Cake\\Chronos\\Chronos will always evaluate to false\.$#' identifier: instanceof.alwaysFalse diff --git a/src/BakePlugin.php b/src/BakePlugin.php index e2ce6b65..ed8cc035 100644 --- a/src/BakePlugin.php +++ b/src/BakePlugin.php @@ -109,8 +109,7 @@ protected function discoverCommands(CommandCollection $commands): CommandCollect * * @param string $namespace The namespace classes are expected to be in. * @param string $path The path to look in. - * @return array - * @phpstan-return array> + * @return array> */ protected function findInPath(string $namespace, string $path): array { diff --git a/src/Command/CellCommand.php b/src/Command/CellCommand.php index c0fb6dc5..739b9991 100644 --- a/src/Command/CellCommand.php +++ b/src/Command/CellCommand.php @@ -59,8 +59,7 @@ public function template(): string * Get template data. * * @param \Cake\Console\Arguments $arguments Arguments object. - * @return array - * @phpstan-return array + * @return array */ public function templateData(Arguments $arguments): array { diff --git a/src/Command/CommandCommand.php b/src/Command/CommandCommand.php index 8612f330..00c4f49b 100644 --- a/src/Command/CommandCommand.php +++ b/src/Command/CommandCommand.php @@ -57,8 +57,7 @@ public function template(): string * Get template data. * * @param \Cake\Console\Arguments $arguments Arguments object. - * @return array - * @phpstan-return array + * @return array */ public function templateData(Arguments $arguments): array { diff --git a/src/Command/ControllerAllCommand.php b/src/Command/ControllerAllCommand.php index 43ffb3ca..c1bc2009 100644 --- a/src/Command/ControllerAllCommand.php +++ b/src/Command/ControllerAllCommand.php @@ -40,17 +40,6 @@ public static function defaultName(): string return 'bake controller all'; } - /** - * initialize - * - * @return void - */ - public function initialize(): void - { - parent::initialize(); - $this->controllerCommand = new ControllerCommand(); - } - /** * Execute the command. * @@ -82,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/EnumCommand.php b/src/Command/EnumCommand.php index 5eee0d0e..52e2bf9b 100644 --- a/src/Command/EnumCommand.php +++ b/src/Command/EnumCommand.php @@ -61,8 +61,7 @@ public function template(): string * Get template data. * * @param \Cake\Console\Arguments $arguments The arguments for the command - * @return array - * @phpstan-return array + * @return array */ public function templateData(Arguments $arguments): array { diff --git a/src/Command/ModelAllCommand.php b/src/Command/ModelAllCommand.php index 2bbae1a7..8730e99d 100644 --- a/src/Command/ModelAllCommand.php +++ b/src/Command/ModelAllCommand.php @@ -40,17 +40,6 @@ public static function defaultName(): string return 'bake model all'; } - /** - * initialize - * - * @return void - */ - public function initialize(): void - { - parent::initialize(); - $this->modelCommand = new ModelCommand(); - } - /** * Gets the option parser instance and configures it. * @@ -59,6 +48,11 @@ public function initialize(): void */ 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/SimpleBakeCommand.php b/src/Command/SimpleBakeCommand.php index a9667a22..59dcbc5c 100644 --- a/src/Command/SimpleBakeCommand.php +++ b/src/Command/SimpleBakeCommand.php @@ -53,8 +53,7 @@ abstract public function template(): string; * Get template data. * * @param \Cake\Console\Arguments $arguments The arguments for the command - * @return array - * @phpstan-return array + * @return array */ public function templateData(Arguments $arguments): array { diff --git a/src/Command/TemplateAllCommand.php b/src/Command/TemplateAllCommand.php index 0a1c72e5..2c7ff454 100644 --- a/src/Command/TemplateAllCommand.php +++ b/src/Command/TemplateAllCommand.php @@ -37,17 +37,6 @@ public static function defaultName(): string return 'bake template all'; } - /** - * initialize - * - * @return void - */ - public function initialize(): void - { - parent::initialize(); - $this->templateCommand = new TemplateCommand(); - } - /** * Execute the command. * @@ -85,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.') diff --git a/src/View/BakeView.php b/src/View/BakeView.php index 99a77010..b6a6abb4 100644 --- a/src/View/BakeView.php +++ b/src/View/BakeView.php @@ -116,7 +116,6 @@ public function dispatchEvent(string $name, array $data = [], ?object $subject = { $name = (string)preg_replace('/^View\./', 'Bake.', $name); - /** @phpstan-ignore-next-line missingType.generics */ return parent::dispatchEvent($name, $data, $subject); } diff --git a/templates/bake/Model/enum.twig b/templates/bake/Model/enum.twig index 83f48243..2ef141ee 100644 --- a/templates/bake/Model/enum.twig +++ b/templates/bake/Model/enum.twig @@ -17,22 +17,16 @@ namespace: "#{namespace}\\Model\\Enum", classImports: [ 'Cake\\Database\\Type\\EnumLabelInterface', - 'Cake\\Utility\\Inflector', + 'Cake\\Database\\Type\\EnumLabelTrait', ], }) }} {{ DocBlock.classDescription(name, 'Enum', [])|raw }} enum {{ name }}: {{ backingType }} implements EnumLabelInterface { + use EnumLabelTrait; {% if cases %} - {{ Bake.concat('\n ', cases) }} + {{ Bake.concat('\n ', cases) }} {% endif %} - /** - * @return string - */ - public function label(): string - { - return Inflector::humanize(Inflector::underscore($this->name)); - } } 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()); + } } diff --git a/tests/comparisons/Model/testBakeEnum.php b/tests/comparisons/Model/testBakeEnum.php index 41c6faea..cdab77f6 100644 --- a/tests/comparisons/Model/testBakeEnum.php +++ b/tests/comparisons/Model/testBakeEnum.php @@ -4,18 +4,12 @@ namespace Bake\Test\App\Model\Enum; use Cake\Database\Type\EnumLabelInterface; -use Cake\Utility\Inflector; +use Cake\Database\Type\EnumLabelTrait; /** * FooBar Enum */ enum FooBar: string implements EnumLabelInterface { - /** - * @return string - */ - public function label(): string - { - return Inflector::humanize(Inflector::underscore($this->name)); - } + use EnumLabelTrait; } diff --git a/tests/comparisons/Model/testBakeEnumBackedInt.php b/tests/comparisons/Model/testBakeEnumBackedInt.php index c0c0648c..0fbe2b07 100644 --- a/tests/comparisons/Model/testBakeEnumBackedInt.php +++ b/tests/comparisons/Model/testBakeEnumBackedInt.php @@ -4,18 +4,12 @@ namespace Bake\Test\App\Model\Enum; use Cake\Database\Type\EnumLabelInterface; -use Cake\Utility\Inflector; +use Cake\Database\Type\EnumLabelTrait; /** * FooBar Enum */ enum FooBar: int implements EnumLabelInterface { - /** - * @return string - */ - public function label(): string - { - return Inflector::humanize(Inflector::underscore($this->name)); - } + use EnumLabelTrait; } diff --git a/tests/comparisons/Model/testBakeEnumBackedIntWithCases.php b/tests/comparisons/Model/testBakeEnumBackedIntWithCases.php index fcd88e07..d5beba07 100644 --- a/tests/comparisons/Model/testBakeEnumBackedIntWithCases.php +++ b/tests/comparisons/Model/testBakeEnumBackedIntWithCases.php @@ -4,22 +4,16 @@ namespace Bake\Test\App\Model\Enum; use Cake\Database\Type\EnumLabelInterface; -use Cake\Utility\Inflector; +use Cake\Database\Type\EnumLabelTrait; /** * FooBar Enum */ enum FooBar: int implements EnumLabelInterface { + use EnumLabelTrait; + case Foo = 0; case Bar = 1; case BarBaz = 9; - - /** - * @return string - */ - public function label(): string - { - return Inflector::humanize(Inflector::underscore($this->name)); - } } diff --git a/tests/comparisons/Model/testBakeEnumBackedWithCases.php b/tests/comparisons/Model/testBakeEnumBackedWithCases.php index 403e8fda..a13c1378 100644 --- a/tests/comparisons/Model/testBakeEnumBackedWithCases.php +++ b/tests/comparisons/Model/testBakeEnumBackedWithCases.php @@ -4,22 +4,16 @@ namespace Bake\Test\App\Model\Enum; use Cake\Database\Type\EnumLabelInterface; -use Cake\Utility\Inflector; +use Cake\Database\Type\EnumLabelTrait; /** * FooBar Enum */ enum FooBar: string implements EnumLabelInterface { + use EnumLabelTrait; + case Foo = 'foo'; case Bar = 'b'; case BarBaz = 'bar_baz'; - - /** - * @return string - */ - public function label(): string - { - return Inflector::humanize(Inflector::underscore($this->name)); - } } diff --git a/tests/comparisons/Model/testBakeTableWithEnumConfig.php b/tests/comparisons/Model/testBakeTableWithEnumConfig.php index 1287d1ac..be913fa3 100644 --- a/tests/comparisons/Model/testBakeTableWithEnumConfig.php +++ b/tests/comparisons/Model/testBakeTableWithEnumConfig.php @@ -4,22 +4,16 @@ namespace Bake\Test\App\Model\Enum; use Cake\Database\Type\EnumLabelInterface; -use Cake\Utility\Inflector; +use Cake\Database\Type\EnumLabelTrait; /** * BakeUserNullableGender Enum */ enum BakeUserNullableGender: string implements EnumLabelInterface { + use EnumLabelTrait; + case Male = 'male'; case Female = 'female'; case Diverse = 'diverse'; - - /** - * @return string - */ - public function label(): string - { - return Inflector::humanize(Inflector::underscore($this->name)); - } } diff --git a/tests/test_app/App/Model/Enum/BakeUserNullableGender.php b/tests/test_app/App/Model/Enum/BakeUserNullableGender.php index 1287d1ac..be913fa3 100644 --- a/tests/test_app/App/Model/Enum/BakeUserNullableGender.php +++ b/tests/test_app/App/Model/Enum/BakeUserNullableGender.php @@ -4,22 +4,16 @@ namespace Bake\Test\App\Model\Enum; use Cake\Database\Type\EnumLabelInterface; -use Cake\Utility\Inflector; +use Cake\Database\Type\EnumLabelTrait; /** * BakeUserNullableGender Enum */ enum BakeUserNullableGender: string implements EnumLabelInterface { + use EnumLabelTrait; + case Male = 'male'; case Female = 'female'; case Diverse = 'diverse'; - - /** - * @return string - */ - public function label(): string - { - return Inflector::humanize(Inflector::underscore($this->name)); - } }