-
-
Notifications
You must be signed in to change notification settings - Fork 9.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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
5f2d7d5
to
7f3dd4b
Compare
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 the AsCommand
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 a debug:console
command to find the callable associated to each command.
cc @kbond as you are the author of |
We had a chat recently about this topic and that repo was my inspiration actually |
d8a8a6d
to
aafa900
Compare
I've tested the Symfony one-file demo with this approach for a single-app command, and this is the result: <?php
require_once __DIR__.'/vendor/autoload_runtime.php';
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\HttpKernel\Kernel;
#[AsCommand('app:start')]
class SymfonyOneFileApp extends Kernel
{
use MicroKernelTrait;
public function __invoke(SymfonyStyle $io, #[Argument] string $name = 'World'): void
{
$io->success(sprintf('Hello %s!', $name));
}
}
return static function (array $context) {
return new Application(new SymfonyOneFileApp($context['APP_ENV'], (bool) $context['APP_DEBUG']));
}; It's missing having services autowired on the method, though. |
@94noni They are related yes. I'm waiting for a deeper conversation with @chalasr to get more insights about the work he is doing in this regard, but in theory this should close & replace those proposals. |
I love it! Really nice DX addition 👏 |
aafa900
to
61271ae
Compare
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
or callable
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'])]
where getPermissions
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)
61271ae
to
f6347ce
Compare
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
to
14e8332
Compare
Woo hoo! I love https://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. |
Can't the behavior of the #[When(...)]
#[AsCommand(...)] ? |
156e18b
to
07c281f
Compare
Update! Added two deprecations noticies:
|
114a39f
to
82034ee
Compare
@@ -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)
82034ee
to
cf6c3aa
Compare
Thank you @yceruto. |
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 | MIT I 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 | |||
*/ | |||
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.
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…
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
Show resolved
Hide resolved
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 | MIT Adjustments based on the latest reviews from #59340 Commits ------- 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 | MIT Adjustments based on the latest reviews from symfony/symfony#59340 Commits ------- 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 | MIT Follow up #59340 Invokable 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: <<<TXT The <info>%command.name%</info> command generates a new user class for security and updates your security.yaml file for it. It will also generate a user provider class 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 | MIT Currently, 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 | MIT This 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()`
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! 🎉