Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.6k
[Console] Add support for invokable commands and input attributes#59340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This is really great@yceruto!
Not required for this PR but just some ideas:
- Allow
#[AsCommand]
to be added to public methods (I believe this can be added easily). #[AsCommand]
on methods in your kernel - for micro-apps.
🥂 Happy new year!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I love it 💚
- Allow
#[AsCommand]
to be added to public methods (I believe this can be added easily).#[AsCommand]
on methods in your kernel - for micro-apps.
I think we should go all the way with this principle and only look for theAsCommand
attribute on service methods.
This would make it possible to define several commands in a single class, or a command directly on a service class.
It could be so flexible that it could quickly become chaotic on some projects; we might need adebug:console
command to find the callable associated to each command.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
cc@kbond as you are the author of |
We had a chat recently about this topic and that repo was my inspiration actually |
d8a8a6d
toaafa900
CompareI've tested theSymfony one-file demo with this approach for a single-app command, and this is the result: <?phprequire_once__DIR__.'/vendor/autoload_runtime.php';useSymfony\Bundle\FrameworkBundle\Console\Application;useSymfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;useSymfony\Component\Console\Attribute\Argument;useSymfony\Component\Console\Attribute\AsCommand;useSymfony\Component\Console\Style\SymfonyStyle;useSymfony\Component\HttpKernel\Kernel;#[AsCommand('app:start')]class SymfonyOneFileAppextends Kernel{use MicroKernelTrait;publicfunction__invoke(SymfonyStyle$io, #[Argument]string$name ='World'):void {$io->success(sprintf('Hello %s!',$name)); }}returnstaticfunction (array$context) {returnnewApplication(newSymfonyOneFileApp($context['APP_ENV'], (bool)$context['APP_DEBUG']));}; It's missing having services autowired on the method, though. |
I love it! Really nice DX addition 👏 |
yceruto commentedJan 2, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I’ve concerns about supporting
I don't think it's a good idea to support that |
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) &&2 ===\count($self->suggestedValues) && ($instance =$parameter->getDeclaringFunction()->getClosureThis()) &&$instance::class ===$self->suggestedValues[0] &&\is_callable($instance::class,$self->suggestedValues[1])) { | ||
$self->suggestedValues = [$instance,$self->suggestedValues[1]]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
[note for reviewer] this falls back from the "static class method call" syntax to the "object method call" syntax due to the impossibility of passing a\Closure
orcallable
in the attribute constructor. Allowing this suggestion methods to access the instance's dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
For instance, this will allow us to configure#[Argument(suggestedValues: [self::class, 'getPermissions'])]
wheregetPermissions
is not defined as static method, enabling it to access any instance dependencies and dynamically retrieve all available permissions from another service (e.g. entity manager)
Let's just say that these functions are rarely used in final projects, and that if you want to use them, it's worth separating the commands into separate classes.
To quote@stof:
SRP is an implementation principle that can be left up to the developer. I once had 2 commands that were functionally very close to each other, but with different arguments. To share part of the code between the 2, I needed an abstract class. With this new approach, I can create 2 commands in 1 class instead of 3. What I like about it is the ease with which you can create new commands with just a little code. A little RAD, no doubt, but ultimately very useful. |
Yeah, it's an unhappy situation in my opinion. Just imagine you already have two commands in the same class, then you decide to use the I'd prefer to guide developers to happy paths where the code can evolve without problems, even if that implies adding one class per command, which is easier than before now. |
9873de3
to14e8332
CompareWoo hoo! I lovehttps://github.com/zenstruck/console-extra, I recently ported some older code and was reminded how verbose I figured it'd be a Symfony 8 thing, but it'd be great if it could arrive earlier, even if experimental. |
kaluzki commentedJan 3, 2025
Can't the behavior of the #[When(...)]#[AsCommand(...)] ? |
@@ -164,6 +164,9 @@ public function isEnabled(): bool | |||
*/ | |||
protected function configure() | |||
{ | |||
if (!$this->code && \is_callable($this)) { | |||
$this->code = new InvokableCommand($this, $this(...)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
The psalm errors seems incorrect. It can be added to the baseline.
Error: src/Symfony/Component/Console/Command/Command.php:168:13: InvalidPropertyAssignment: $this with non-object type 'never' cannot treated as an object (see https://psalm.dev/010)Error: src/Symfony/Component/Console/Command/Command.php:168:48: NoValue: All possible types for this argument were invalidated - This may be dead code (see https://psalm.dev/179)Error: src/Symfony/Component/Console/Command/Command.php:168:55: InvalidFunctionCall: Cannot treat type never as callable (see https://psalm.dev/064)
Thank you@yceruto. |
8f6560c
intosymfony:7.3Uh oh!
There was an error while loading.Please reload this page.
This PR was squashed before being merged into the 7.3 branch.Discussion----------[Console] Invokable command deprecations| Q | A| ------------- | ---| Branch? | 7.3| Bug fix? | no| New feature? | yes| Deprecations? | yes| Issues | -| License | MITI believe we missed the last commit during the squash and merge of#59340. It has been applied here, along with the UPGRADE entry.Commits-------71d0be1 [Console] Invokable command deprecations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
nice :) here is a late review for minor things
@@ -164,6 +164,9 @@ public function isEnabled(): bool | |||
*/ | |||
protectedfunctionconfigure() | |||
{ | |||
if (!$this->code &&\is_callable($this)) { | |||
$this->code =newInvokableCommand($this,$this(...)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This downside of this is we're now creating a self-referencing class. Might not be an issue in practice since we won't create several instances of the command object, but still worth to have in mind and prevent writing such code in the generic case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Completely agree here! I'm wondering if there is an alternative solution for this case…
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.phpShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
This PR was squashed before being merged into the 7.3 branch.Discussion----------[Console] Invokable command adjustments| Q | A| ------------- | ---| Branch? | 7.3| Bug fix? | no| New feature? | no| Deprecations? | no| Issues | -| License | MITAdjustments based on the latest reviews from#59340Commits-------8ab2f32 [Console] Invokable command adjustments
This PR was squashed before being merged into the 7.3 branch.Discussion----------[Console] Invokable command adjustments| Q | A| ------------- | ---| Branch? | 7.3| Bug fix? | no| New feature? | no| Deprecations? | no| Issues | -| License | MITAdjustments based on the latest reviews fromsymfony/symfony#59340Commits-------8ab2f32ba2c [Console] Invokable command adjustments
…ition (yceruto)This PR was squashed before being merged into the 7.3 branch.Discussion----------[Console] Add broader support for command "help" definition| Q | A| ------------- | ---| Branch? | 7.3| Bug fix? | no| New feature? | yes| Deprecations? | no| Issues | -| License | MITFollow up#59340Invokable and regular commands can now define the command `help` content via the `#[AsCommand]` attribute.This is particularly useful for invokable commands, as it avoids the need to extend the `Command` class.```php#[AsCommand( name: 'user:create', description: 'Create a new user', help: <<<TXTThe <info>%command.name%</info> command generates a new user class for securityand updates your security.yaml file for it. It will also generate a user providerclass if your situation needs a custom class.<info>php %command.full_name% email</info>If the argument is missing, the command will ask for the class name interactively.TXT)]class CreateUserCommand{ public function __invoke(SymfonyStyle $io, #[Argument] string $email): int { // ... }}```Cheers!Commits-------e9a6b0a [Console] Add broader support for command "help" definition
This PR was merged into the 7.3 branch.Discussion----------[Console] Fixed support for Kernel as command| Q | A| ------------- | ---| Branch? | 7.3| Bug fix? | no| New feature? | no| Deprecations? | no| Issues | -| License | MITCurrently, registering the Kernel as a command (see the example here:#59340 (comment)) results in an error:```Undefined array key "kernel"```I added the test case that highlights the issue and the fix (adding the `'container.no_preload'` tag to the invokable service is incorrect, as it is not the command service).Commits-------c9ef38c Fixed support for Kernel as command
…\Closure` function set via `Command::setCode()` (yceruto)This PR was merged into the 7.3 branch.Discussion----------[Console] Deprecate returning a non-int value from a `\Closure` function set via `Command::setCode()`| Q | A| ------------- | ---| Branch? | 7.3| Bug fix? | no| New feature? | no| Deprecations? | yes| Issues | -| License | MITThis adds a missing log entry about a deprecation introduced [here](#59340), and also deprecates returning a `null` value for `\Closure` code (which was allowed before) and throwing a `\TypeError` for the new invokable command, making this consistent with the `Command::execute(): int` method.Commits-------787d60a Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()`
I love this feature, congrats! But it's pretty hard to customize the parameters match in The obvious workaround is to inject How do you see it? Is it something we could improve? |
Please create a new issue, thanks |
chalasr commentedMay 30, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I will resume#59794 for 7.4 - main goal is to provide an extension point for such use cases. |
Uh oh!
There was an error while loading.Please reload this page.
Alternative to:#57225
This PR focuses on enhancing the DX for creating and defining console commands.
Key improvements include:
#[AsCommand]
, extending theCommand
class is no longer required.__invoke()
method, thanks to the new#[Argument]
and#[Option]
attributes.__invoke()
Signature: The__invoke()
method now has a flexible signature, allowing you to define only the helpers you need.Also, this method will fallback toIt's required to return an int value as result, see[Console] Deprecate returning a non-int value from a0
(success) if you return void\Closure
function set viaCommand::setCode()
#60076 .Before
After
However, you can still extend the
Command
class when necessary to use advanced methods, such as theinteract()
method and others.Happy New Year! 🎉