From 349c696bb515415cc6cd64c8dedcb8d64e3f3a17 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Fri, 28 Nov 2025 19:05:29 +0100 Subject: [PATCH 01/12] Extract generate implementation into generateInheritanceCommand method --- src/Maker/MakeCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index 52d331c53..e23779118 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -63,6 +63,11 @@ public function configureCommand(Command $command, InputConfiguration $inputConf } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $this->generateInheritanceCommand($input, $io, $generator); + } + + private function generateInheritanceCommand(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $commandName = trim($input->getArgument('name')); $commandNameHasAppPrefix = str_starts_with($commandName, 'app:'); From 8fd8bc983145f229b32b4756fbe51d8d9bfde38b Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 11:53:07 +0100 Subject: [PATCH 02/12] Allow to create an InvokableCommand using the make:command command Move the "Old" Inheritance based command template to a separate template file --- src/Maker/MakeCommand.php | 58 ++++++++++++++++++-- templates/command/Command.tpl.php | 27 +++------ templates/command/InheritanceCommand.tpl.php | 45 +++++++++++++++ 3 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 templates/command/InheritanceCommand.tpl.php diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index e23779118..be65e9e4f 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -16,9 +16,12 @@ use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; +use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; +use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\Input\InputArgument; @@ -26,6 +29,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\HttpKernel\Kernel; /** * @author Javier Eguiluz @@ -60,15 +64,19 @@ public function configureCommand(Command $command, InputConfiguration $inputConf ->addArgument('name', InputArgument::OPTIONAL, \sprintf('Choose a command name (e.g. app:%s)', Str::asCommand(Str::getRandomTerm()))) ->setHelp($this->getHelpFileContents('MakeCommand.txt')) ; + + if ($this->supportsInvokableCommand()) { + $command->addOption('invokable', 'i', InputOption::VALUE_NONE, 'Use this option to create an invokable command'); + } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { - $this->generateInheritanceCommand($input, $io, $generator); - } + if (true !== $input->getOption('invokable') && $this->supportsInvokableCommand()) { + $wantsInvokable = $io->confirm('Would you like this command to be inokvable?', false); + $input->setOption('invokable', $wantsInvokable); + } - private function generateInheritanceCommand(InputInterface $input, ConsoleStyle $io, Generator $generator): void - { $commandName = trim($input->getArgument('name')); $commandNameHasAppPrefix = str_starts_with($commandName, 'app:'); @@ -79,6 +87,13 @@ private function generateInheritanceCommand(InputInterface $input, ConsoleStyle \sprintf('The "%s" command name is not valid because it would be implemented by "%s" class, which is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores).', $commandName, Str::asClassName($commandName, 'Command')) ); + $input->getOption('invokable') ? + $this->generateInvokableCommand($commandName, $commandClassNameDetails, $io, $generator) : + $this->generateInheritanceCommand($commandName, $commandClassNameDetails, $io, $generator); + } + + private function generateInheritanceCommand(string $commandName, ClassNameDetails $commandClassNameDetails, ConsoleStyle $io, Generator $generator): void + { $useStatements = new UseStatementGenerator([ Command::class, InputArgument::class, @@ -91,7 +106,7 @@ private function generateInheritanceCommand(InputInterface $input, ConsoleStyle $generator->generateClass( $commandClassNameDetails->getFullName(), - 'command/Command.tpl.php', + 'command/InheritanceCommand.tpl.php', [ 'use_statements' => $useStatements, 'command_name' => $commandName, @@ -108,6 +123,34 @@ private function generateInheritanceCommand(InputInterface $input, ConsoleStyle ]); } + private function generateInvokableCommand(string $commandName, ClassNameDetails $commandClassNameDetails, ConsoleStyle $io, Generator $generator): void + { + $useStatements = new UseStatementGenerator([ + Argument::class, + AsCommand::class, + Command::class, + Option::class, + SymfonyStyle::class, + ]); + + $generator->generateClass( + $commandClassNameDetails->getFullName(), + 'command/Command.tpl.php', + [ + 'use_statements' => $useStatements, + 'command_name' => $commandName, + ] + ); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + $io->text([ + 'Next: open your new command class and customize it!', + 'Find the documentation at https://symfony.com/doc/current/console.html', + ]); + } + public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( @@ -115,4 +158,9 @@ public function configureDependencies(DependencyBuilder $dependencies): void 'console' ); } + + private function supportsInvokableCommand(): bool + { + return Kernel::VERSION_ID >= 70300; + } } diff --git a/templates/command/Command.tpl.php b/templates/command/Command.tpl.php index 3050e1baa..60c5ceb6d 100644 --- a/templates/command/Command.tpl.php +++ b/templates/command/Command.tpl.php @@ -8,32 +8,19 @@ name: '', description: 'Add a short description for your command', )] -class extends Command +class { - public function __construct() + public function __invoke( + SymfonyStyle $io, + #[Argument] string $arg1 = null, + #[Option] bool $option1 = false, + ): int { - parent::__construct(); - } - - protected function configure(): void - { - $this -setDescription(self::\$defaultDescription)\n" : '' ?> - ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description') - ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $arg1 = $input->getArgument('arg1'); - if ($arg1) { $io->note(sprintf('You passed an argument: %s', $arg1)); } - if ($input->getOption('option1')) { + if ($option1) { // ... } diff --git a/templates/command/InheritanceCommand.tpl.php b/templates/command/InheritanceCommand.tpl.php new file mode 100644 index 000000000..dde939b0f --- /dev/null +++ b/templates/command/InheritanceCommand.tpl.php @@ -0,0 +1,45 @@ + + +namespace ; + + + +#[AsCommand( + name: '', + description: 'Add a short description for your command', +)] +class extends Command +{ + public function __construct() + { + parent::__construct(); + } + + protected function configure(): void + { + $this +setDescription(self::\$defaultDescription)\n" : '' ?> + ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description') + ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $arg1 = $input->getArgument('arg1'); + + if ($arg1) { + $io->note(sprintf('You passed an argument: %s', $arg1)); + } + + if ($input->getOption('option1')) { + // ... + } + + $io->success('You have a new command! Now make it your own! Pass --help to see your options.'); + + return Command::SUCCESS; + } +} + From 2f6ca92beeb9dafca97291866aa725c3e72f5014 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 11:53:08 +0100 Subject: [PATCH 03/12] Prompt the user to fill in a description for their Invokable command --- src/Maker/MakeCommand.php | 28 ++++++++++++---------------- templates/command/Command.tpl.php | 2 +- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index be65e9e4f..8f086d109 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -28,6 +28,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpKernel\Kernel; @@ -90,6 +91,14 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $input->getOption('invokable') ? $this->generateInvokableCommand($commandName, $commandClassNameDetails, $io, $generator) : $this->generateInheritanceCommand($commandName, $commandClassNameDetails, $io, $generator); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + $io->text([ + 'Next: open your new command class and customize it!', + 'Find the documentation at https://symfony.com/doc/current/console.html', + ]); } private function generateInheritanceCommand(string $commandName, ClassNameDetails $commandClassNameDetails, ConsoleStyle $io, Generator $generator): void @@ -113,18 +122,12 @@ private function generateInheritanceCommand(string $commandName, ClassNameDetail 'set_description' => !class_exists(LazyCommand::class), ] ); - - $generator->writeChanges(); - - $this->writeSuccessMessage($io); - $io->text([ - 'Next: open your new command class and customize it!', - 'Find the documentation at https://symfony.com/doc/current/console.html', - ]); } private function generateInvokableCommand(string $commandName, ClassNameDetails $commandClassNameDetails, ConsoleStyle $io, Generator $generator): void { + $description = $io->ask('Enter a short description for your command'); + $useStatements = new UseStatementGenerator([ Argument::class, AsCommand::class, @@ -139,16 +142,9 @@ private function generateInvokableCommand(string $commandName, ClassNameDetails [ 'use_statements' => $useStatements, 'command_name' => $commandName, + 'command_description' => $description, ] ); - - $generator->writeChanges(); - - $this->writeSuccessMessage($io); - $io->text([ - 'Next: open your new command class and customize it!', - 'Find the documentation at https://symfony.com/doc/current/console.html', - ]); } public function configureDependencies(DependencyBuilder $dependencies): void diff --git a/templates/command/Command.tpl.php b/templates/command/Command.tpl.php index 60c5ceb6d..6943bc7d5 100644 --- a/templates/command/Command.tpl.php +++ b/templates/command/Command.tpl.php @@ -6,7 +6,7 @@ #[AsCommand( name: '', - description: 'Add a short description for your command', + description: '', )] class { From 39c7205e4755fe58cf6a41f0a54fe7bc2492098d Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 11:53:08 +0100 Subject: [PATCH 04/12] Remove the incorrectly copied over use statement --- src/Maker/MakeCommand.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index 8f086d109..34a34e4b2 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -129,9 +129,8 @@ private function generateInvokableCommand(string $commandName, ClassNameDetails $description = $io->ask('Enter a short description for your command'); $useStatements = new UseStatementGenerator([ - Argument::class, AsCommand::class, - Command::class, + Argument::class, Option::class, SymfonyStyle::class, ]); From 215dca8decd13b27de016fa84e25d0ee6fa74c80 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 11:53:08 +0100 Subject: [PATCH 05/12] Add the functionality to interactively generate command inputs --- src/Maker/MakeCommand.php | 269 ++++++++++++++++++++++++++++-- templates/command/Command.tpl.php | 90 +++++++++- 2 files changed, 334 insertions(+), 25 deletions(-) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index 34a34e4b2..b499c05d5 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -28,9 +28,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpKernel\Kernel; +use function is_string; +use function sprintf; /** * @author Javier Eguiluz @@ -44,7 +45,7 @@ public function __construct(private ?PhpCompatUtil $phpCompatUtil = null) @trigger_deprecation( 'symfony/maker-bundle', '1.55.0', - \sprintf('Initializing MakeCommand while providing an instance of "%s" is deprecated. The $phpCompatUtil param will be removed in a future version.', PhpCompatUtil::class), + sprintf('Initializing MakeCommand while providing an instance of "%s" is deprecated. The $phpCompatUtil param will be removed in a future version.', PhpCompatUtil::class), ); } } @@ -62,9 +63,8 @@ public static function getCommandDescription(): string public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command - ->addArgument('name', InputArgument::OPTIONAL, \sprintf('Choose a command name (e.g. app:%s)', Str::asCommand(Str::getRandomTerm()))) - ->setHelp($this->getHelpFileContents('MakeCommand.txt')) - ; + ->addArgument('name', InputArgument::OPTIONAL, sprintf('Choose a command name (e.g. app:%s)', Str::asCommand(Str::getRandomTerm()))) + ->setHelp($this->getHelpFileContents('MakeCommand.txt')); if ($this->supportsInvokableCommand()) { $command->addOption('invokable', 'i', InputOption::VALUE_NONE, 'Use this option to create an invokable command'); @@ -85,20 +85,12 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $commandNameHasAppPrefix ? substr($commandName, 4) : $commandName, 'Command\\', 'Command', - \sprintf('The "%s" command name is not valid because it would be implemented by "%s" class, which is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores).', $commandName, Str::asClassName($commandName, 'Command')) + sprintf('The "%s" command name is not valid because it would be implemented by "%s" class, which is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores).', $commandName, Str::asClassName($commandName, 'Command')) ); - $input->getOption('invokable') ? - $this->generateInvokableCommand($commandName, $commandClassNameDetails, $io, $generator) : + $input->getOption('invokable') ? + $this->generateInvokableCommand($commandName, $commandClassNameDetails, $io, $generator) : $this->generateInheritanceCommand($commandName, $commandClassNameDetails, $io, $generator); - - $generator->writeChanges(); - - $this->writeSuccessMessage($io); - $io->text([ - 'Next: open your new command class and customize it!', - 'Find the documentation at https://symfony.com/doc/current/console.html', - ]); } private function generateInheritanceCommand(string $commandName, ClassNameDetails $commandClassNameDetails, ConsoleStyle $io, Generator $generator): void @@ -122,15 +114,36 @@ private function generateInheritanceCommand(string $commandName, ClassNameDetail 'set_description' => !class_exists(LazyCommand::class), ] ); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + $io->text([ + 'Next: open your new command class and customize it!', + 'Find the documentation at https://symfony.com/doc/current/console.html', + ]); } private function generateInvokableCommand(string $commandName, ClassNameDetails $commandClassNameDetails, ConsoleStyle $io, Generator $generator): void { - $description = $io->ask('Enter a short description for your command'); + if (class_exists($commandClassNameDetails->getFullName())) { + $io->error('This command already exists.'); + + return; + } + + $description = $io->ask('What is the command description?'); + if (false === is_string($description)) { + $description = (string)$description; + } + + $arguments = $this->askForArguments($io); + $options = $this->askForOptions($io); $useStatements = new UseStatementGenerator([ AsCommand::class, Argument::class, + Command::class, Option::class, SymfonyStyle::class, ]); @@ -142,8 +155,228 @@ private function generateInvokableCommand(string $commandName, ClassNameDetails 'use_statements' => $useStatements, 'command_name' => $commandName, 'command_description' => $description, + 'command_parameters' => $this->mergeAndSortParameters($arguments, $options), ] ); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + $io->text([ + 'Next: open your new command class and customize it!', + 'Find the documentation at https://symfony.com/doc/current/console.html', + ]); + } + + /** + * @param array $arguments + * @param array $options + * @return array + */ + private function mergeAndSortParameters(array $arguments, array $options): array + { + // Merge arguments and options, marking each with its type + $parameters = []; + foreach ($arguments as $arg) { + $parameters[] = array_merge($arg, ['param_type' => 'argument']); + } + foreach ($options as $opt) { + $parameters[] = array_merge($opt, ['param_type' => 'option']); + } + + // Sort parameters: required parameters (no defaults) must come before optional ones (with defaults) + usort($parameters, function ($a, $b) { + $aHasDefault = $this->parameterHasDefault($a); + $bHasDefault = $this->parameterHasDefault($b); + + if ($aHasDefault === $bHasDefault) { + return 0; // Keep original order within same group (required with required, optional with optional) + } + + // Required (no default) comes before optional (has default) + return $aHasDefault ? 1 : -1; + }); + + return $parameters; + } + + private function parameterHasDefault(array $param): bool + { + if ('argument' === $param['param_type']) { + // Arguments have defaults if explicitly set or if nullable + return null !== $param['default'] || ($param['nullable'] ?? false); + } + + // Options always have defaults + return true; + } + + /** + * @return array + */ + private function askForArguments(ConsoleStyle $io): array + { + $arguments = []; + $isFirst = true; + + while (true) { + $io->writeln(''); + + if ($isFirst) { + $questionText = 'Argument name? (press to stop adding arguments)'; + } else { + $questionText = 'Add another argument? Enter the argument name (or press to stop adding arguments)'; + } + + $name = $io->ask($questionText, null, function ($name) use ($arguments) { + // allow it to be empty + if (!$name) { + return $name; + } + + foreach ($arguments as $arg) { + if ($arg['name'] === $name) { + throw new \InvalidArgumentException(sprintf('The "%s" argument already exists.', $name)); + } + } + + return $name; + }); + + if (!$name) { + break; + } + + $isFirst = false; + + $type = $io->choice( + 'What is the argument type?', + ['string', 'int', 'float', 'bool', 'array'], + 'string' + ); + + $nullable = $io->confirm('Is this argument nullable?', false); + + $description = $io->ask('What is the argument description?', null); + if (!is_string($description) && null !== $description) { + $description = (string)$description; + } + + $hasDefault = $io->confirm('Does this argument have a default value?', false); + $default = null; + if ($hasDefault) { + if ('bool' === $type) { + $default = $io->confirm('What is the default value?', false); + } elseif ('int' === $type) { + $default = (int)$io->ask('What is the default value?', '0'); + } elseif ('float' === $type) { + $default = (float)$io->ask('What is the default value?', '0.0'); + } elseif ('array' === $type) { + $defaultValue = $io->ask('What is the default value?', '[]'); + $default = '[]' === $defaultValue ? [] : $defaultValue; + } else { + $default = $io->ask('What is the default value?', ''); + if (!is_string($default)) { + $default = (string)$default; + } + } + } elseif ($nullable) { + $default = null; + } + + $arguments[] = [ + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'default' => $default, + 'nullable' => $nullable, + ]; + } + + return $arguments; + } + + /** + * @return array + */ + private function askForOptions(ConsoleStyle $io): array + { + $options = []; + $isFirst = true; + + while (true) { + $io->writeln(''); + + if ($isFirst) { + $questionText = 'What is the option name?'; + } else { + $questionText = 'What is the next option name?'; + } + + $name = $io->ask($questionText, null, function ($name) use ($options) { + // allow it to be empty + if (!$name) { + return $name; + } + + foreach ($options as $opt) { + if ($opt['name'] === $name) { + throw new \InvalidArgumentException(sprintf('The "%s" option already exists.', $name)); + } + } + + return $name; + }); + + if (!$name) { + break; + } + + $isFirst = false; + + $shortcut = $io->ask('What is the option shortcut?', null); + if (!is_string($shortcut) && null !== $shortcut) { + $shortcut = (string)$shortcut; + } + + $type = $io->choice( + 'What is the option type?', + ['bool', 'string', 'int', 'float', 'array'], + 'bool' + ); + + $description = $io->ask('What is the option description?', null); + if (!is_string($description) && null !== $description) { + $description = (string)$description; + } + + $default = null; + if ('bool' === $type) { + $default = $io->confirm('What is the default value?', false); + } elseif ('int' === $type) { + $default = (int)$io->ask('What is the default value?', '0'); + } elseif ('float' === $type) { + $default = (float)$io->ask('What is the default value?', '0.0'); + } elseif ('array' === $type) { + $defaultValue = $io->ask('What is the default value?', '[]'); + $default = '[]' === $defaultValue ? [] : $defaultValue; + } else { + $default = $io->ask('What is the default value?', ''); + if (!is_string($default)) { + $default = (string)$default; + } + } + + $options[] = [ + 'name' => $name, + 'shortcut' => $shortcut, + 'type' => $type, + 'description' => $description, + 'default' => $default, + ]; + } + + return $options; } public function configureDependencies(DependencyBuilder $dependencies): void @@ -153,7 +386,7 @@ public function configureDependencies(DependencyBuilder $dependencies): void 'console' ); } - + private function supportsInvokableCommand(): bool { return Kernel::VERSION_ID >= 70300; diff --git a/templates/command/Command.tpl.php b/templates/command/Command.tpl.php index 6943bc7d5..7274642eb 100644 --- a/templates/command/Command.tpl.php +++ b/templates/command/Command.tpl.php @@ -11,19 +11,95 @@ class { public function __invoke( - SymfonyStyle $io, - #[Argument] string $arg1 = null, - #[Option] bool $option1 = false, + SymfonyStyle $io, ): int { - if ($arg1) { - $io->note(sprintf('You passed an argument: %s', $arg1)); + + + if ($) { + $io->note(sprintf('You passed an argument: %s', $)); } - if ($option1) { - // ... + + if ($) { + $io->note(sprintf('You passed option: %s', $)); } + + $io->success('You have a new command! Now make it your own! Pass --help to see your options.'); return Command::SUCCESS; From 67e9e619bb7c9704abff13a23742754a6e863e52 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 11:53:08 +0100 Subject: [PATCH 06/12] Remove a small amount of duplicate code --- src/Maker/MakeCommand.php | 136 ++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 73 deletions(-) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index b499c05d5..327f1ca31 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -30,8 +30,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpKernel\Kernel; -use function is_string; -use function sprintf; /** * @author Javier Eguiluz @@ -45,7 +43,7 @@ public function __construct(private ?PhpCompatUtil $phpCompatUtil = null) @trigger_deprecation( 'symfony/maker-bundle', '1.55.0', - sprintf('Initializing MakeCommand while providing an instance of "%s" is deprecated. The $phpCompatUtil param will be removed in a future version.', PhpCompatUtil::class), + \sprintf('Initializing MakeCommand while providing an instance of "%s" is deprecated. The $phpCompatUtil param will be removed in a future version.', PhpCompatUtil::class), ); } } @@ -63,7 +61,7 @@ public static function getCommandDescription(): string public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command - ->addArgument('name', InputArgument::OPTIONAL, sprintf('Choose a command name (e.g. app:%s)', Str::asCommand(Str::getRandomTerm()))) + ->addArgument('name', InputArgument::OPTIONAL, \sprintf('Choose a command name (e.g. app:%s)', Str::asCommand(Str::getRandomTerm()))) ->setHelp($this->getHelpFileContents('MakeCommand.txt')); if ($this->supportsInvokableCommand()) { @@ -73,11 +71,6 @@ public function configureCommand(Command $command, InputConfiguration $inputConf public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { - if (true !== $input->getOption('invokable') && $this->supportsInvokableCommand()) { - $wantsInvokable = $io->confirm('Would you like this command to be inokvable?', false); - $input->setOption('invokable', $wantsInvokable); - } - $commandName = trim($input->getArgument('name')); $commandNameHasAppPrefix = str_starts_with($commandName, 'app:'); @@ -85,7 +78,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $commandNameHasAppPrefix ? substr($commandName, 4) : $commandName, 'Command\\', 'Command', - sprintf('The "%s" command name is not valid because it would be implemented by "%s" class, which is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores).', $commandName, Str::asClassName($commandName, 'Command')) + \sprintf('The "%s" command name is not valid because it would be implemented by "%s" class, which is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores).', $commandName, Str::asClassName($commandName, 'Command')) ); $input->getOption('invokable') ? @@ -133,8 +126,8 @@ private function generateInvokableCommand(string $commandName, ClassNameDetails } $description = $io->ask('What is the command description?'); - if (false === is_string($description)) { - $description = (string)$description; + if (false === \is_string($description)) { + $description = (string) $description; } $arguments = $this->askForArguments($io); @@ -169,8 +162,9 @@ private function generateInvokableCommand(string $commandName, ClassNameDetails } /** - * @param array $arguments + * @param array $arguments * @param array $options + * * @return array */ private function mergeAndSortParameters(array $arguments, array $options): array @@ -200,6 +194,7 @@ private function mergeAndSortParameters(array $arguments, array $options): array return $parameters; } + /** @param array{default: mixed, param_type: string, nullable?: bool} $param */ private function parameterHasDefault(array $param): bool { if ('argument' === $param['param_type']) { @@ -216,6 +211,8 @@ private function parameterHasDefault(array $param): bool */ private function askForArguments(ConsoleStyle $io): array { + $io->writeln('Now, let\'s add some arguments.'); + $arguments = []; $isFirst = true; @@ -236,7 +233,7 @@ private function askForArguments(ConsoleStyle $io): array foreach ($arguments as $arg) { if ($arg['name'] === $name) { - throw new \InvalidArgumentException(sprintf('The "%s" argument already exists.', $name)); + throw new \InvalidArgumentException(\sprintf('The "%s" argument already exists.', $name)); } } @@ -255,41 +252,12 @@ private function askForArguments(ConsoleStyle $io): array 'string' ); - $nullable = $io->confirm('Is this argument nullable?', false); - - $description = $io->ask('What is the argument description?', null); - if (!is_string($description) && null !== $description) { - $description = (string)$description; - } - - $hasDefault = $io->confirm('Does this argument have a default value?', false); - $default = null; - if ($hasDefault) { - if ('bool' === $type) { - $default = $io->confirm('What is the default value?', false); - } elseif ('int' === $type) { - $default = (int)$io->ask('What is the default value?', '0'); - } elseif ('float' === $type) { - $default = (float)$io->ask('What is the default value?', '0.0'); - } elseif ('array' === $type) { - $defaultValue = $io->ask('What is the default value?', '[]'); - $default = '[]' === $defaultValue ? [] : $defaultValue; - } else { - $default = $io->ask('What is the default value?', ''); - if (!is_string($default)) { - $default = (string)$default; - } - } - } elseif ($nullable) { - $default = null; - } - $arguments[] = [ 'name' => $name, 'type' => $type, - 'description' => $description, - 'default' => $default, - 'nullable' => $nullable, + 'description' => $this->askForParameterDescription($io), + 'default' => $this->askForDefaults($io, $type), + 'nullable' => $io->confirm('Is this argument nullable?', false), ]; } @@ -301,6 +269,8 @@ private function askForArguments(ConsoleStyle $io): array */ private function askForOptions(ConsoleStyle $io): array { + $io->writeln('Now, let\'s add some options.'); + $options = []; $isFirst = true; @@ -321,7 +291,7 @@ private function askForOptions(ConsoleStyle $io): array foreach ($options as $opt) { if ($opt['name'] === $name) { - throw new \InvalidArgumentException(sprintf('The "%s" option already exists.', $name)); + throw new \InvalidArgumentException(\sprintf('The "%s" option already exists.', $name)); } } @@ -335,8 +305,8 @@ private function askForOptions(ConsoleStyle $io): array $isFirst = false; $shortcut = $io->ask('What is the option shortcut?', null); - if (!is_string($shortcut) && null !== $shortcut) { - $shortcut = (string)$shortcut; + if (!\is_string($shortcut) && null !== $shortcut) { + $shortcut = (string) $shortcut; } $type = $io->choice( @@ -345,40 +315,60 @@ private function askForOptions(ConsoleStyle $io): array 'bool' ); - $description = $io->ask('What is the option description?', null); - if (!is_string($description) && null !== $description) { - $description = (string)$description; - } - - $default = null; - if ('bool' === $type) { - $default = $io->confirm('What is the default value?', false); - } elseif ('int' === $type) { - $default = (int)$io->ask('What is the default value?', '0'); - } elseif ('float' === $type) { - $default = (float)$io->ask('What is the default value?', '0.0'); - } elseif ('array' === $type) { - $defaultValue = $io->ask('What is the default value?', '[]'); - $default = '[]' === $defaultValue ? [] : $defaultValue; - } else { - $default = $io->ask('What is the default value?', ''); - if (!is_string($default)) { - $default = (string)$default; - } - } - $options[] = [ 'name' => $name, 'shortcut' => $shortcut, 'type' => $type, - 'description' => $description, - 'default' => $default, + 'description' => $this->askForParameterDescription($io), + 'default' => $this->askForDefaults($io, $type), ]; } return $options; } + private function askForParameterDescription(ConsoleStyle $io): ?string + { + $description = $io->ask('What is the description?', null); + if (null !== $description && !\is_string($description)) { + $description = (string) $description; + } + + if (false === \is_scalar($description)) { + $description = null; + } + + return $description; + } + + private function askForDefaults(ConsoleStyle $io, string $type): mixed + { + $hasDefault = $io->confirm('Does it have a default value?', false); + + if (false === $hasDefault) { + return null; + } + + if ('bool' === $type) { + return $io->confirm('What is the default value?', false); + } elseif ('int' === $type) { + return (int) $io->ask('What is the default value?', '0'); + } elseif ('float' === $type) { + return (float) $io->ask('What is the default value?', '0.0'); + } elseif ('array' === $type) { + $defaultValue = $io->ask('What is the default value?', '[]'); + + return '[]' === $defaultValue ? [] : $defaultValue; + } else { + $default = $io->ask('What is the default value?', ''); + if (true === \is_scalar($default)) { + return (string) $default; + } + + return null; + } + } + public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( From c9a1842bf96772b5876d40879cdf7328de67c0c4 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 11:53:09 +0100 Subject: [PATCH 07/12] Fixes an issue where options would not have defaults and dashes in parameters would break generation --- src/Maker/MakeCommand.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index 327f1ca31..fa56ac76f 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -65,7 +65,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf ->setHelp($this->getHelpFileContents('MakeCommand.txt')); if ($this->supportsInvokableCommand()) { - $command->addOption('invokable', 'i', InputOption::VALUE_NONE, 'Use this option to create an invokable command'); + $command->addOption('invokable', 'i', InputOption::VALUE_NEGATABLE, 'Use this option to create an invokable command', default: $this->supportsInvokableCommand()); } } @@ -237,7 +237,7 @@ private function askForArguments(ConsoleStyle $io): array } } - return $name; + return Str::asLowerCamelCase(strtr($name, ['-' => ' '])); }); if (!$name) { @@ -295,7 +295,7 @@ private function askForOptions(ConsoleStyle $io): array } } - return $name; + return Str::asLowerCamelCase(strtr($name, ['-' => ' '])); }); if (!$name) { @@ -304,23 +304,23 @@ private function askForOptions(ConsoleStyle $io): array $isFirst = false; - $shortcut = $io->ask('What is the option shortcut?', null); + $shortcut = $io->ask('What is the option shortcut?'); if (!\is_string($shortcut) && null !== $shortcut) { $shortcut = (string) $shortcut; } $type = $io->choice( 'What is the option type?', - ['bool', 'string', 'int', 'float', 'array'], + ['bool', 'string', 'int', 'float'], 'bool' ); $options[] = [ - 'name' => $name, + 'name' => Str::asLowerCamelCase($name), 'shortcut' => $shortcut, 'type' => $type, 'description' => $this->askForParameterDescription($io), - 'default' => $this->askForDefaults($io, $type), + 'default' => $this->askForDefaults($io, $type, true), ]; } @@ -341,12 +341,14 @@ private function askForParameterDescription(ConsoleStyle $io): ?string return $description; } - private function askForDefaults(ConsoleStyle $io, string $type): mixed + private function askForDefaults(ConsoleStyle $io, string $type, bool $force = false): mixed { - $hasDefault = $io->confirm('Does it have a default value?', false); + if (false === $force) { + $hasDefault = $io->confirm('Does it have a default value?', false); - if (false === $hasDefault) { - return null; + if (false === $hasDefault) { + return null; + } } if ('bool' === $type) { From 9d6985b84e418c371f2cdc79b29c3cde14d92363 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 12:24:26 +0100 Subject: [PATCH 08/12] Add and repair tests for invokable command generation --- tests/Maker/MakeCommandTest.php | 72 ++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/tests/Maker/MakeCommandTest.php b/tests/Maker/MakeCommandTest.php index 1ff279c19..3b81a3b23 100644 --- a/tests/Maker/MakeCommandTest.php +++ b/tests/Maker/MakeCommandTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\MakerBundle\Maker\MakeCommand; use Symfony\Bundle\MakerBundle\Test\MakerTestCase; use Symfony\Bundle\MakerBundle\Test\MakerTestRunner; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Yaml\Yaml; class MakeCommandTest extends MakerTestCase @@ -25,23 +26,25 @@ protected function getMakerClass(): string public function getTestDetails(): \Generator { + $supportsInvokable = Kernel::VERSION_ID >= 70300; + yield 'it_makes_a_command_no_attributes' => [$this->createMakerTest() - ->run(function (MakerTestRunner $runner) { + ->run(function (MakerTestRunner $runner) use ($supportsInvokable) { $runner->runMaker([ // command name 'app:foo', - ]); + ], $supportsInvokable ? '--no-invokable' : ''); $this->runCommandTest($runner, 'it_makes_a_command.php'); }), ]; yield 'it_makes_a_command_with_attributes' => [$this->createMakerTest() - ->run(function (MakerTestRunner $runner) { + ->run(function (MakerTestRunner $runner) use ($supportsInvokable) { $runner->runMaker([ // command name 'app:foo', - ]); + ], $supportsInvokable ? '--no-invokable' : ''); $this->runCommandTest($runner, 'it_makes_a_command.php'); @@ -54,7 +57,7 @@ public function getTestDetails(): \Generator yield 'it_makes_a_command_in_custom_namespace' => [$this->createMakerTest() ->changeRootNamespace('Custom') - ->run(function (MakerTestRunner $runner) { + ->run(function (MakerTestRunner $runner) use ($supportsInvokable) { $runner->writeFile( 'config/packages/dev/maker.yaml', Yaml::dump(['maker' => ['root_namespace' => 'Custom']]) @@ -63,11 +66,68 @@ public function getTestDetails(): \Generator $runner->runMaker([ // command name 'app:foo', - ]); + ], $supportsInvokable ? '--no-invokable' : ''); $this->runCommandTest($runner, 'it_makes_a_command_in_custom_namespace.php'); }), ]; + + if ($supportsInvokable) { + yield 'it_makes_an_invokable_command_by_default' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $runner->runMaker([ + // command name + 'app:foo', + 'foo', + '', + '', + ]); + + $this->runCommandTest($runner, 'it_makes_a_command.php'); + + $commandFileContents = file_get_contents($runner->getPath('src/Command/FooCommand.php')); + + self::assertStringContainsString('use Symfony\Component\Console\Attribute\AsCommand;', $commandFileContents); + self::assertStringContainsString('#[AsCommand(', $commandFileContents); + self::assertStringNotContainsString('extends Command', $commandFileContents); + self::assertStringContainsString('__invoke(', $commandFileContents); + }), + ]; + + yield 'it_makes_an_invokable_and_configures_parameters' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $runner->runMaker([ + // command name + 'app:foo', + 'foo', + 'bar', // Argument name + 0, // Argument type (string) + 'Adds a bar argument to your command', // Argument description + 'no', // Has no default value + 'yes', // Is nullable + 'baz', // Second argument, will be required + 1, // Second type (int) + 'How many bazzes do you need?', // Second argument description + 'no', // Second argument no default + 'no', // Second argument not nullable + '', // Stop Arguments + 'dry-run', // Option name + 'd', // Option shortcut + 0, // Option type (boolean) + 'Perform a dry run?', + 'no', // Default value (false) + '', // Stop option insertion + ]); + + $commandFileContents = file_get_contents($runner->getPath('src/Command/FooCommand.php')); + + self::assertStringContainsString('__invoke(', $commandFileContents); + self::assertStringContainsString('#[Argument(description: \'How many bazzes do you need?\')] int $baz,', $commandFileContents); + self::assertStringContainsString('#[Argument(description: \'Adds a bar argument to your command\')] ?string $bar = null', $commandFileContents); + self::assertStringContainsString('#[Option(description: \'Perform a dry run?\', shortcut: \'d\')] bool $dryRun = false', $commandFileContents); + }), + ]; + } } private function runCommandTest(MakerTestRunner $runner, string $filename): void From 108abf818ebbfbdbf49cb78b7b5faa4019031c38 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 12:45:23 +0100 Subject: [PATCH 09/12] Add a failsafe on option retrieval for invokable --- src/Maker/MakeCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index fa56ac76f..e5150684f 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -81,7 +81,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen \sprintf('The "%s" command name is not valid because it would be implemented by "%s" class, which is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores).', $commandName, Str::asClassName($commandName, 'Command')) ); - $input->getOption('invokable') ? + $this->supportsInvokableCommand() && $input->getOption('invokable') ? $this->generateInvokableCommand($commandName, $commandClassNameDetails, $io, $generator) : $this->generateInheritanceCommand($commandName, $commandClassNameDetails, $io, $generator); } From a0b8e4ff88a7cd0f1bd1b6cea3e1e6578213cd31 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 13:09:15 +0100 Subject: [PATCH 10/12] Always show the invoke option but make it no-op in unsupported versions --- src/Maker/MakeCommand.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index e5150684f..c01094ca5 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -64,9 +64,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf ->addArgument('name', InputArgument::OPTIONAL, \sprintf('Choose a command name (e.g. app:%s)', Str::asCommand(Str::getRandomTerm()))) ->setHelp($this->getHelpFileContents('MakeCommand.txt')); - if ($this->supportsInvokableCommand()) { $command->addOption('invokable', 'i', InputOption::VALUE_NEGATABLE, 'Use this option to create an invokable command', default: $this->supportsInvokableCommand()); - } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void From c569ff3fe677c8fb458cb103a7e984005feadd5c Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Sat, 29 Nov 2025 13:29:12 +0100 Subject: [PATCH 11/12] Check the Test Runner Symfony Environment to see if Invokable Commands are supported --- tests/Maker/MakeCommandTest.php | 133 +++++++++++++++++--------------- 1 file changed, 69 insertions(+), 64 deletions(-) diff --git a/tests/Maker/MakeCommandTest.php b/tests/Maker/MakeCommandTest.php index 3b81a3b23..2f2b46bb2 100644 --- a/tests/Maker/MakeCommandTest.php +++ b/tests/Maker/MakeCommandTest.php @@ -26,25 +26,24 @@ protected function getMakerClass(): string public function getTestDetails(): \Generator { - $supportsInvokable = Kernel::VERSION_ID >= 70300; - yield 'it_makes_a_command_no_attributes' => [$this->createMakerTest() - ->run(function (MakerTestRunner $runner) use ($supportsInvokable) { + ->run(function (MakerTestRunner $runner) { + $runner->runMaker([ // command name 'app:foo', - ], $supportsInvokable ? '--no-invokable' : ''); + ], $runner->getSymfonyVersion() >= 70300 ? '--no-invokable' : ''); $this->runCommandTest($runner, 'it_makes_a_command.php'); }), ]; yield 'it_makes_a_command_with_attributes' => [$this->createMakerTest() - ->run(function (MakerTestRunner $runner) use ($supportsInvokable) { + ->run(function (MakerTestRunner $runner) { $runner->runMaker([ // command name 'app:foo', - ], $supportsInvokable ? '--no-invokable' : ''); + ], $runner->getSymfonyVersion() >= 70300 ? '--no-invokable' : ''); $this->runCommandTest($runner, 'it_makes_a_command.php'); @@ -57,7 +56,7 @@ public function getTestDetails(): \Generator yield 'it_makes_a_command_in_custom_namespace' => [$this->createMakerTest() ->changeRootNamespace('Custom') - ->run(function (MakerTestRunner $runner) use ($supportsInvokable) { + ->run(function (MakerTestRunner $runner) { $runner->writeFile( 'config/packages/dev/maker.yaml', Yaml::dump(['maker' => ['root_namespace' => 'Custom']]) @@ -66,68 +65,74 @@ public function getTestDetails(): \Generator $runner->runMaker([ // command name 'app:foo', - ], $supportsInvokable ? '--no-invokable' : ''); + ], $runner->getSymfonyVersion() >= 70300 ? '--no-invokable' : ''); $this->runCommandTest($runner, 'it_makes_a_command_in_custom_namespace.php'); }), ]; - if ($supportsInvokable) { - yield 'it_makes_an_invokable_command_by_default' => [$this->createMakerTest() - ->run(function (MakerTestRunner $runner) { - $runner->runMaker([ - // command name - 'app:foo', - 'foo', - '', - '', - ]); - - $this->runCommandTest($runner, 'it_makes_a_command.php'); - - $commandFileContents = file_get_contents($runner->getPath('src/Command/FooCommand.php')); - - self::assertStringContainsString('use Symfony\Component\Console\Attribute\AsCommand;', $commandFileContents); - self::assertStringContainsString('#[AsCommand(', $commandFileContents); - self::assertStringNotContainsString('extends Command', $commandFileContents); - self::assertStringContainsString('__invoke(', $commandFileContents); - }), - ]; - - yield 'it_makes_an_invokable_and_configures_parameters' => [$this->createMakerTest() - ->run(function (MakerTestRunner $runner) { - $runner->runMaker([ - // command name - 'app:foo', - 'foo', - 'bar', // Argument name - 0, // Argument type (string) - 'Adds a bar argument to your command', // Argument description - 'no', // Has no default value - 'yes', // Is nullable - 'baz', // Second argument, will be required - 1, // Second type (int) - 'How many bazzes do you need?', // Second argument description - 'no', // Second argument no default - 'no', // Second argument not nullable - '', // Stop Arguments - 'dry-run', // Option name - 'd', // Option shortcut - 0, // Option type (boolean) - 'Perform a dry run?', - 'no', // Default value (false) - '', // Stop option insertion - ]); - - $commandFileContents = file_get_contents($runner->getPath('src/Command/FooCommand.php')); - - self::assertStringContainsString('__invoke(', $commandFileContents); - self::assertStringContainsString('#[Argument(description: \'How many bazzes do you need?\')] int $baz,', $commandFileContents); - self::assertStringContainsString('#[Argument(description: \'Adds a bar argument to your command\')] ?string $bar = null', $commandFileContents); - self::assertStringContainsString('#[Option(description: \'Perform a dry run?\', shortcut: \'d\')] bool $dryRun = false', $commandFileContents); - }), - ]; - } + yield 'it_makes_an_invokable_command_by_default' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + if ($runner->getSymfonyVersion() < 70300) { + $this->markTestSkipped('Symfony version does not support Invokable Commands'); + } + + $runner->runMaker([ + // command name + 'app:foo', + 'foo', + '', + '', + ]); + + $this->runCommandTest($runner, 'it_makes_a_command.php'); + + $commandFileContents = file_get_contents($runner->getPath('src/Command/FooCommand.php')); + + self::assertStringContainsString('use Symfony\Component\Console\Attribute\AsCommand;', $commandFileContents); + self::assertStringContainsString('#[AsCommand(', $commandFileContents); + self::assertStringNotContainsString('extends Command', $commandFileContents); + self::assertStringContainsString('__invoke(', $commandFileContents); + }), + ]; + + yield 'it_makes_an_invokable_and_configures_parameters' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + if ($runner->getSymfonyVersion() < 70300) { + $this->markTestSkipped('Symfony version does not support Invokable Commands'); + } + + $runner->runMaker([ + // command name + 'app:foo', + 'foo', + 'bar', // Argument name + 0, // Argument type (string) + 'Adds a bar argument to your command', // Argument description + 'no', // Has no default value + 'yes', // Is nullable + 'baz', // Second argument, will be required + 1, // Second type (int) + 'How many bazzes do you need?', // Second argument description + 'no', // Second argument no default + 'no', // Second argument not nullable + '', // Stop Arguments + 'dry-run', // Option name + 'd', // Option shortcut + 0, // Option type (boolean) + 'Perform a dry run?', + 'no', // Default value (false) + '', // Stop option insertion + ]); + + $commandFileContents = file_get_contents($runner->getPath('src/Command/FooCommand.php')); + + self::assertStringContainsString('__invoke(', $commandFileContents); + self::assertStringContainsString('#[Argument(description: \'How many bazzes do you need?\')] int $baz,', $commandFileContents); + self::assertStringContainsString('#[Argument(description: \'Adds a bar argument to your command\')] ?string $bar = null', $commandFileContents); + self::assertStringContainsString('#[Option(description: \'Perform a dry run?\', shortcut: \'d\')] bool $dryRun = false', $commandFileContents); + }), + ]; } private function runCommandTest(MakerTestRunner $runner, string $filename): void From 9a4544194a21f3e09705e3d1b9c05569fccba68c Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Mon, 1 Dec 2025 22:09:06 +0100 Subject: [PATCH 12/12] Rename Command.tpl to InvokableCommand.tpl to beter indicate it's intention --- src/Maker/MakeCommand.php | 2 +- templates/command/{Command.tpl.php => InvokableCommand.tpl.php} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename templates/command/{Command.tpl.php => InvokableCommand.tpl.php} (100%) diff --git a/src/Maker/MakeCommand.php b/src/Maker/MakeCommand.php index c01094ca5..8d010ac2d 100644 --- a/src/Maker/MakeCommand.php +++ b/src/Maker/MakeCommand.php @@ -141,7 +141,7 @@ private function generateInvokableCommand(string $commandName, ClassNameDetails $generator->generateClass( $commandClassNameDetails->getFullName(), - 'command/Command.tpl.php', + 'command/InvokableCommand.tpl.php', [ 'use_statements' => $useStatements, 'command_name' => $commandName, diff --git a/templates/command/Command.tpl.php b/templates/command/InvokableCommand.tpl.php similarity index 100% rename from templates/command/Command.tpl.php rename to templates/command/InvokableCommand.tpl.php