Skip to content

Conversation

@yceruto
Copy link
Member

@yceruto yceruto commented Nov 29, 2025

Q A
Branch? 8.1
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

Same as with Controllers and Messenger Handlers, this would allow defining multiple commands in the same class:

class MethodBasedCommand
{
    public function __construct(
        // common dependencies ...
    ) {
    }

    #[AsCommand('app:cmd1')]
    public function cmd1(): int
    {
        // ...
    }

    #[AsCommand('app:cmd2')]
    public function cmd2(): int
    {
        // ...
    }
}

Asked by @kbond in #59340 (review)

Component standalone usage:

$instance = new MethodBasedCommand();

$application = new Application();
$application->addCommand($instance->cmd1(...)); // or [$instance, 'cmd1']
$application->run($input, $output);

$application = new Application();
$application->addCommand($instance->cmd2(...));
$application->run($input, $output);

Testing:

$instance = new MethodBasedCommand();

$tester = new CommandTester($instance->cmd1(...)); // or [$instance, 'cmd1']
$tester->execute([]);
$tester->assertCommandIsSuccessful();

// etc.

@yceruto yceruto requested a review from chalasr as a code owner November 29, 2025 11:33
@carsonbot carsonbot added this to the 8.1 milestone Nov 29, 2025
@yceruto yceruto force-pushed the method_based_command branch 2 times, most recently from 9eaaeca to 8e4f9a0 Compare November 30, 2025 23:43
@yceruto
Copy link
Member Author

yceruto commented Nov 30, 2025

better review hiding whitespace https://github.com/symfony/symfony/pull/62567/files?w=1

@yceruto yceruto force-pushed the method_based_command branch from 8e4f9a0 to 74eac25 Compare December 1, 2025 16:37
@stof
Copy link
Member

stof commented Dec 1, 2025

how does this interact with the Prompt attribute added in version 7.4 ?

@stof
Copy link
Member

stof commented Dec 1, 2025

To me, the component standalone usage should show how to register those in an Application actually (which will probably show that those are much more cumbersome to use than the invokable commands by having to do the Command wrapping in userland instead of keeping it internal, unless I missed something)

@yceruto
Copy link
Member Author

yceruto commented Dec 1, 2025

how does this interact with the Prompt attribute added in version 7.4 ?

when the #[Interact] attribute is used, it will need to check $input->getArgument('command') to distinguish the executed command. we can create as many #[Interact] as needed. though this new approach shouldn't be used for complex commands

private function registerCommand(ContainerBuilder $container, \ReflectionClass $reflection, string $id, string $class, array $tags, Definition $definition, array &$serviceIds, array &$lazyCommandMap, array &$lazyCommandRefs): void
{
if (!$reflection->isSubclassOf(Command::class)) {
$method = $tags[0]['method'] ?? '__invoke';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reading $tags[0] is broken when processing the registration for other tags. It will not validate the right method (and won't register the right one either)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the broken escenario. other tags for the same command method will be processed later and interpreted as aliases.

Copy link
Member Author

@yceruto yceruto Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, tags are now split into methods, so there is no way to process other tags with a different method in this same iteration

@stof
Copy link
Member

stof commented Dec 1, 2025

it will need to check $input->getArgument('command') to distinguish the executed command.

This is a bad advice because the Application class supports resolving commands using any non-ambiguous shortened name passed as the command argument. So you could be running the foobar command with $input->getArgument('command') === 'f' if this is non ambiguous.

@yceruto yceruto force-pushed the method_based_command branch 2 times, most recently from b84cd82 to e123736 Compare December 7, 2025 22:32
@yceruto
Copy link
Member Author

yceruto commented Dec 7, 2025

it will need to check $input->getArgument('command') to distinguish the executed command.

This is a bad advice because the Application class supports resolving commands using any non-ambiguous shortened name passed as the command argument. So you could be running the foobar command with $input->getArgument('command') === 'f' if this is non ambiguous.

yes, that's unfortunately not perfect, so I've added support for injecting the Command instance. This way people can check getName() to distinguish them:

#[Interact]
public function prompt(Command $cmd, /* ... */): void
{
    // $cmd->getName()
}

Copy link
Member

@GromNaN GromNaN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this interact with the Prompt attribute added in version 7.4 ?

when the #[Interact] attribute is used, it will need to check $input->getArgument('command') to distinguish the executed command. we can create as many #[Interact] as needed. though this new approach shouldn't be used for complex commands

Calling all the #[Interact] methods of the class for every commands is the most logical behavior. This feature will only be usable when all the command methods have the same interaction needs.

@yceruto yceruto force-pushed the method_based_command branch from e123736 to cb1d853 Compare December 8, 2025 10:39
Comment on lines +43 to +47
if (isset($tag['method'])) {
$commandServices[$id][$tag['method']][] = $tag;
} else {
$commandServices[$id]['__invoke'][] = $tag;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This way, it is more obvious that these two lines do the same thing with a default value.

Suggested change
if (isset($tag['method'])) {
$commandServices[$id][$tag['method']][] = $tag;
} else {
$commandServices[$id]['__invoke'][] = $tag;
}
$commandServices[$id][$tag['method'] ?? '__invoke'][] = $tag;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually had the opposite view 😅 but I'm fine with your suggestion if others prefer it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for your review 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants