Skip to content

[Console] Invokable command adjustments #59493

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

Merged
merged 1 commit into from
Jan 20, 2025

Conversation

yceruto
Copy link
Member

@yceruto yceruto commented Jan 13, 2025

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

Adjustments based on the latest reviews from #59340

@yceruto
Copy link
Member Author

yceruto commented Jan 13, 2025

Still analyzing the last two #59340 (comment) #59340 (comment)

@yceruto yceruto force-pushed the invokable_command_tweaks branch 2 times, most recently from 6094cd1 to 730d374 Compare January 13, 2025 21:52
@yceruto
Copy link
Member Author

yceruto commented Jan 13, 2025

Update after latest changes:

  • Fixed default true value for boolean options
  • Non-boolean options will always require a value, e.g. --option=value (even if they set a default value) so the default value is given only when the option is omitted

I added more tests to cover those scenarios.

@yceruto yceruto requested a review from stof January 13, 2025 22:00
@yceruto
Copy link
Member Author

yceruto commented Jan 13, 2025

I've a question about non-boolean options: when defined without a default, e.g. #[Option] string $foo, and the option is omitted during the cmd call (which is acceptable by definition), it results in a null value, causing a TypeError. I was thinking that instead of a TypeError, we could throw a definition exception that enforces defining a default value for non-boolean options. What do you think?

@kbond
Copy link
Member

kbond commented Jan 13, 2025

I was thinking that instead of a TypeError, we could throw a definition exception that enforces defining a default value for non-boolean options.

This makes sense to me. Most important thing is a clear error message.

@chalasr
Copy link
Member

chalasr commented Jan 14, 2025

I was expecting this topic to arise, not trivial :) I wouldn't be happy with invokable commands not supporting InputOption::VALUE_OPTIONAL as it's a big limitation compared to regular commands.
What about requiring to opt-in explicitly by passing the mode to #[Option]?
And/or determine a special value to be set as parameter's default that would indicate that the option was not passed at all?

@yceruto yceruto force-pushed the invokable_command_tweaks branch from 730d374 to d740cfa Compare January 14, 2025 06:44
@yceruto
Copy link
Member Author

yceruto commented Jan 14, 2025

Let's lay out all the scenarios on the table so everyone can see the alternatives and how to implement them effectively with this new invokable approach.

We can use input options in three different ways:
(A) set with value: cmd --option=value
(B) set without value: cmd --option
(C) or omit it entirely: cmd

If a default value is defined, it will be used as the option's value only in case (C). In case (B), the option value will be null. In case (A), the value provided via the CLI will take precedence and become the final option value.

That's how regular commands work currently.

Input options are optional by nature. This means the parameter must either:

  • declare a default value: #[Option] string $role = 'ROLE_USER' In this case, only cases (A) and (C) apply
  • allow null value: #[Option] ?string $role All cases apply
  • or both: #[Option] ?string $role = 'ROLE_USER' All cases apply

However, input options can never be mandatory. This means defining them like #[Option] string $role is not allowed and should result in an exception.

The specific modeInputOption::VALUE_OPTIONAL applies exclusively to case (B), making it particularly useful for binary (boolean) options. Using "value optional" for non-binary options feels strange, as it results in a null value, indicating that the option was used, but no value was supplied. I couldn't find a use case for it but still possible on regular commands.

@yceruto
Copy link
Member Author

yceruto commented Jan 14, 2025

I've updated the code based on this analysis (it's pending throwing an exception for mandatory option parameters)

@kbond
Copy link
Member

kbond commented Jan 14, 2025

I think the only mode I don't support here: https://github.com/zenstruck/console-extra?tab=readme-ov-file#invokable-attributes is InputOption::VALUE_OPTIONAL. What about bool|string|null $role = null (or bool|string $role = false)?

@kbond
Copy link
Member

kbond commented Jan 14, 2025

However, input options can never be mandatory.

Requiring isDefaultValueAvailable() seems best.

@yceruto yceruto force-pushed the invokable_command_tweaks branch from d740cfa to 4e96f0a Compare January 15, 2025 05:11
@yceruto
Copy link
Member Author

yceruto commented Jan 15, 2025

I think the only mode I don't support here: https://github.com/zenstruck/console-extra?tab=readme-ov-file#invokable-attributes is InputOption::VALUE_OPTIONAL. What about bool|string|null $role = null (or bool|string $role = false)?

would making it nullable be sufficient? That's the approach used here, so you can declare an option with an optional value as #[Option] ?string $foo. This will allow cmd --foo resulting in null being received.

But to be honest, I don’t know for which use case it makes sense, because the intention of passing such an option without a value feels more like a boolean option instead.

@yceruto
Copy link
Member Author

yceruto commented Jan 15, 2025

To distinguish between an option passed without a value and not passed at all, set a default value: #[Option] ?string $foo = 'bar'. If the option is passed without a value, you'll get null. If not passed at all, you'll get bar. If passed with a value, you'll get that value.

@@ -164,9 +168,6 @@ public function isEnabled(): bool
*/
protected function configure()
{
if (!$this->code && \is_callable($this)) {
$this->code = new InvokableCommand($this, $this(...));
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved to the constructor to allow overriding the configure() method without requiring a call to the parent implementation

}
} else {
$code = $code(...);
}
Copy link
Member Author

@yceruto yceruto Jan 15, 2025

Choose a reason for hiding this comment

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

Moved to InvokableCommand which supports handling static anonymous functions too

@stof
Copy link
Member

stof commented Jan 15, 2025

The case of VALUE_OPTIONAL is a very weird case. In normal commands, using $input->getOption() is not sufficient to properly distinguish cases 1 and 2 (not passing the option or passing the option without values).
In 14 years using the console component, I have never had a use case for such option (but I reviewed lots of PRs adding a command using that VALUE_OPTIONAL mode by mistake by not understanding, thinking it is needed to make the option optional).

I agree about adding the negatable flag only for boolean options that don't have false as default value, to avoid cluttering the command help with negatable options that bring no value (maybe the attribute could have an optional argument to force setting the flag, in case you need to keep it for BC reasons)

@yceruto
Copy link
Member Author

yceruto commented Jan 15, 2025

That makes sense to me @stof. PR updated! Not entirely sure about the flag though, let’s see what others think

@@ -99,6 +99,6 @@ public function toInputArgument(): InputArgument
*/
public function resolveValue(InputInterface $input): mixed
{
return $input->hasArgument($this->name) ? $input->getArgument($this->name) : null;
return $input->getArgument($this->name);
Copy link
Member Author

Choose a reason for hiding this comment

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

removed $input->hasArgument($this->name) as it's better to get the "argument does not exist" exception than a TypeError or just null (if nullable)

@yceruto
Copy link
Member Author

yceruto commented Jan 15, 2025

After a deep analysis with Kevin, we decided to simplify the argument/option definition by removing the default option from both attributes, so we always rely on the parameter's default value. Additionally, boolean options are now required to declare a default value too.

@chalasr
Copy link
Member

chalasr commented Jan 17, 2025

The case of VALUE_OPTIONAL is a very weird case. In normal commands, using $input->getOption() is not sufficient to properly distinguish cases 1 and 2 (not passing the option or passing the option without values).

Yea it requires checking both hasOption() and getOption().

In 14 years using the console component, I have never had a use case for such option (but I reviewed lots of PRs adding a command using that VALUE_OPTIONAL mode by mistake by not understanding, thinking it is needed to make the option optional).

It's true there are certainly plenty of misuses, yet there are lots of usages and some are valid. Looking only at public github repos: https://github.com/search?q=InputOption%3A%3AVALUE_OPTIONAL&type=code.
So unless we want to deprecate the VALUE_OPTIONAL mode, I think supporting it is desired for the sake of reducing differences between this new way of registering commands and the regular one.

@kbond
Copy link
Member

kbond commented Jan 17, 2025

@chalasr after this pr is merged I'm going to open one dedicated to VALUE_OPTIONAL to discuss further and finalize.

@yceruto yceruto force-pushed the invokable_command_tweaks branch 2 times, most recently from 476b881 to e3c2c0d Compare January 17, 2025 13:51
@chalasr chalasr force-pushed the invokable_command_tweaks branch from e3c2c0d to 8ab2f32 Compare January 20, 2025 11:07
@chalasr
Copy link
Member

chalasr commented Jan 20, 2025

Thank you @yceruto.

@chalasr chalasr merged commit b684e7e into symfony:7.3 Jan 20, 2025
9 of 11 checks passed
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.

6 participants