Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.7k
[Console] Add#[Input] attribute to support DTOs in invokable commands#61478
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
Nice, it was on my list! |
Uh oh!
There was an error while loading.Please reload this page.
it seems we shared the same list at some point :) |
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.
Uh oh!
There was an error while loading.Please reload this page.
1a792fb to0ed04d7CompareAppreciate the feedback@nicolas-grekas 🙏, all points have been addressed. |
6cdbc96 to84d092fCompareUh oh!
There was an error while loading.Please reload this page.
84d092f to28f49feCompareThere 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.
Failures unrelated![]()
The Fabbot failures are false positives, are there any other failures I might have overlooked? |
Uh oh!
There was an error while loading.Please reload this page.
28f49fe to8727de8CompareOpps, I didn't see the new CS output, it should be ok now. Thanks! (remaining checks are unrelated or false positives) |
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.
Looks super nice!
Can we read phpdocs to extract a description for each argument/option?
src/Symfony/Component/Console/Tests/Fixtures/InvokableWithInputTestCommand.php OutdatedShow 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.
8727de8 to273c507Compare
Yes, absolutely! we can do that. Just to mention though, arguments and options already support a For example: class UserInput{ #[Argument(description:'The user email address.')]publicstring$email;} |
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.
273c507 to7209cbcCompare7209cbc to14d0f6bCompareThanks for the review! this is ready on my side. Looking forward to starting work on the follow-up PRs/features :) |
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.
Really nice one 🚀
14d0f6b to0cff2ebCompare
Indeed, that's good enough. |
Thank you@yceruto. |
ae256f9 intosymfony:7.4Uh oh!
There was an error while loading.Please reload this page.
…ds with `#[Interact]` and `#[Ask]` attributes (yceruto)This PR was squashed before being merged into the 7.4 branch.Discussion----------[Console] Add support for interactive invokable commands with `#[Interact]` and `#[Ask]` attributes| Q | A| ------------- | ---| Branch? | 7.4| Bug fix? | no| New feature? | yes| Deprecations? | no| Issues | -| License | MITThe `#[Interact]` attribute lets you hook into the command *interactive phase* without extending `Command`, and unlocks even more flexibility!**Characteristics**- The method marked with `#[Interact]` attribute will be called during interactive mode- The method must be **public, non‑static**, otherwise a `LogicException` is thrown- As usual, it runs only when the interactive mode is enabled (e.g. not with `--no-interaction`)- Supports common helpers and DTOs parameters just like `__invoke()`**Before:**```php#[AsCommand('app:create-user')]class CreateUserCommand extends Command{ protected function interact(InputInterface $input, OutputInterface $output): void { $io = new SymfonyStyle($input, $output); if (!$input->getArgument('username')) { $input->setArgument('username', $io->ask('Enter the username')); } } public function __invoke(#[Argument] string $username): int { // ... }}```**After (long version):**```php#[AsCommand('app:create-user')]class CreateUserCommand{ #[Interact] public function prompt(InputInterface $input, SymfonyStyle $io): void { if (!$input->getArgument('username')) { $input->setArgument('username', $io->ask('Enter the username')); } } public function __invoke(#[Argument] string $username): int { // ... }}```This PR also adds the `#[Ask]` attribute for the most basic use cases. It lets you declare interactive prompts directly on parameters. Symfony will automatically ask the user for missing values during the interactive phase, without needing to implement a custom "interact" method yourself:**After (short version):**```php#[AsCommand('app:create-user')]class CreateUserCommand{ public function __invoke( #[Argument, Ask('Enter the username')] string $username, ): int { // ... }}```### DTO‑friendly interactionIn more complex commands, the DTO approach (see PR#61478) lets you work directly with the DTO instance and its properties. You've got three ways to do this, so let's start with the simplest and move toward the most flexible:#### 1) Attribute-driven interactivityYou can also use the `#[Ask]` attribute on DTO properties that have the `#[Argument]` attribute and no default value; if such a property is unset when running the command (e.g. when the linked argument isn't passed), the component automatically triggers a prompt using your defined question:```phpclass UserDto{ #[Argument] #[Ask('Enter the username')] public string $username; #[Argument] #[Ask('Enter the password', hidden: true)] public string $password;}#[AsCommand('app:create-user')]class CreateUserCommand{ public function __invoke(#[MapInput] UserDto $user): int { // use $user->username and $user->password }}```Example run:```bashbin/console app:create-user Enter the username: > yceruto Enter the password: > 🔑```This makes the most common interactive cases completely declarative.> [RFC] You may also find other declarative prompts useful, such as #[Choice] (with better support for BackedEnum properties) and #[Confirm] (for bool properties).#### 2) DTO-driven interactivityFor scenarios that go beyond simple prompts, you can handle interactivity inside the DTO itself. As long as it only concerns the DTO's own properties (and doesn't require external services), you can mark a method with `#[Interact]`. Symfony will call it during the interactive phase, giving you access to helpers to implement custom logic:```phpclass UserDto{ #[Argument] #[Ask('Enter the username')] public string $username; #[Argument] #[Ask('Enter the password (or press Enter for a random one)', hidden: true)] public string $password; #[Interact] public function prompt(SymfonyStyle $io): void { if (!isset($this->password)) { $this->password = generate_password(10); copy_to_clipboard($this->password); $io->writeln('Password generated and copied to your clipboard.'); } }}```Yes! `#[Ask]` and `#[Interact]` complement each other and are executed in sequence during the interactive phase.#### 3) Service‑aware promptsFor cases where prompts depend on external services or need a broader context, you can declare the `#[Interact]` method on the command class itself, giving you full control over the interactive phase:```php#[AsCommand('app:create-user')]class CreateUserCommand{ public function __construct( private PasswordStrengthValidatorInterface $validator, ) { } #[Interact] public function prompt(SymfonyStyle $io, #[MapInput] UserDto $user): void { $user->password ??= $io->askHidden('Enter password', $this->validator->isValid(...)); } public function __invoke(#[MapInput] UserDto $user): int { // ... }}```In earlier approaches, you had to set arguments manually with `$input->setArgument()`. With DTOs, you can now work directly on typed properties, which makes the code more expressive and less error-prone.All three ways can coexist, and the execution order is:1. `#[Ask]` on `__invoke` parameters2. `#[Ask]` on DTO properties3. `#[Interact]` on the DTO class4. `#[Interact]` on the command classMore related features will be unveiled later.Cheers!Commits-------6e837c4 [Console] Add support for interactive invokable commands with `#[Interact]` and `#[Ask]` attributes…ds with `#[Interact]` and `#[Ask]` attributes (yceruto)This PR was squashed before being merged into the 7.4 branch.Discussion----------[Console] Add support for interactive invokable commands with `#[Interact]` and `#[Ask]` attributes| Q | A| ------------- | ---| Branch? | 7.4| Bug fix? | no| New feature? | yes| Deprecations? | no| Issues | -| License | MITThe `#[Interact]` attribute lets you hook into the command *interactive phase* without extending `Command`, and unlocks even more flexibility!**Characteristics**- The method marked with `#[Interact]` attribute will be called during interactive mode- The method must be **public, non‑static**, otherwise a `LogicException` is thrown- As usual, it runs only when the interactive mode is enabled (e.g. not with `--no-interaction`)- Supports common helpers and DTOs parameters just like `__invoke()`**Before:**```php#[AsCommand('app:create-user')]class CreateUserCommand extends Command{ protected function interact(InputInterface $input, OutputInterface $output): void { $io = new SymfonyStyle($input, $output); if (!$input->getArgument('username')) { $input->setArgument('username', $io->ask('Enter the username')); } } public function __invoke(#[Argument] string $username): int { // ... }}```**After (long version):**```php#[AsCommand('app:create-user')]class CreateUserCommand{ #[Interact] public function prompt(InputInterface $input, SymfonyStyle $io): void { if (!$input->getArgument('username')) { $input->setArgument('username', $io->ask('Enter the username')); } } public function __invoke(#[Argument] string $username): int { // ... }}```This PR also adds the `#[Ask]` attribute for the most basic use cases. It lets you declare interactive prompts directly on parameters. Symfony will automatically ask the user for missing values during the interactive phase, without needing to implement a custom "interact" method yourself:**After (short version):**```php#[AsCommand('app:create-user')]class CreateUserCommand{ public function __invoke( #[Argument, Ask('Enter the username')] string $username, ): int { // ... }}```### DTO‑friendly interactionIn more complex commands, the DTO approach (see PRsymfony/symfony#61478) lets you work directly with the DTO instance and its properties. You've got three ways to do this, so let's start with the simplest and move toward the most flexible:#### 1) Attribute-driven interactivityYou can also use the `#[Ask]` attribute on DTO properties that have the `#[Argument]` attribute and no default value; if such a property is unset when running the command (e.g. when the linked argument isn't passed), the component automatically triggers a prompt using your defined question:```phpclass UserDto{ #[Argument] #[Ask('Enter the username')] public string $username; #[Argument] #[Ask('Enter the password', hidden: true)] public string $password;}#[AsCommand('app:create-user')]class CreateUserCommand{ public function __invoke(#[MapInput] UserDto $user): int { // use $user->username and $user->password }}```Example run:```bashbin/console app:create-user Enter the username: > yceruto Enter the password: > 🔑```This makes the most common interactive cases completely declarative.> [RFC] You may also find other declarative prompts useful, such as #[Choice] (with better support for BackedEnum properties) and #[Confirm] (for bool properties).#### 2) DTO-driven interactivityFor scenarios that go beyond simple prompts, you can handle interactivity inside the DTO itself. As long as it only concerns the DTO's own properties (and doesn't require external services), you can mark a method with `#[Interact]`. Symfony will call it during the interactive phase, giving you access to helpers to implement custom logic:```phpclass UserDto{ #[Argument] #[Ask('Enter the username')] public string $username; #[Argument] #[Ask('Enter the password (or press Enter for a random one)', hidden: true)] public string $password; #[Interact] public function prompt(SymfonyStyle $io): void { if (!isset($this->password)) { $this->password = generate_password(10); copy_to_clipboard($this->password); $io->writeln('Password generated and copied to your clipboard.'); } }}```Yes! `#[Ask]` and `#[Interact]` complement each other and are executed in sequence during the interactive phase.#### 3) Service‑aware promptsFor cases where prompts depend on external services or need a broader context, you can declare the `#[Interact]` method on the command class itself, giving you full control over the interactive phase:```php#[AsCommand('app:create-user')]class CreateUserCommand{ public function __construct( private PasswordStrengthValidatorInterface $validator, ) { } #[Interact] public function prompt(SymfonyStyle $io, #[MapInput] UserDto $user): void { $user->password ??= $io->askHidden('Enter password', $this->validator->isValid(...)); } public function __invoke(#[MapInput] UserDto $user): int { // ... }}```In earlier approaches, you had to set arguments manually with `$input->setArgument()`. With DTOs, you can now work directly on typed properties, which makes the code more expressive and less error-prone.All three ways can coexist, and the execution order is:1. `#[Ask]` on `__invoke` parameters2. `#[Ask]` on DTO properties3. `#[Interact]` on the DTO class4. `#[Interact]` on the command classMore related features will be unveiled later.Cheers!Commits-------6e837c4b1f7 [Console] Add support for interactive invokable commands with `#[Interact]` and `#[Ask]` attributes
Uh oh!
There was an error while loading.Please reload this page.
Introduce
#[Input]to let invokable console commands receive a DTO that defines the command's arguments/options on its own public properties via#[Argument]/#[Option]. This avoids stuffing__invoke()with a long parameter list and keeps the command's input model in one place.How it works
#[Input]on a__invoke()parameter, typed as your DTO class (it's not limited to a single parameter)#[Argument]and#[Option]become the command's input definitionExample
This produces the usage:
(booleans render as
--foo/--no-fooas usual)Nested DTOs (group inputs)
You can compose inputs by nesting DTOs and giving them a form:
The resulting signature merges everything:
The resolver walks nested DTOs and fills them accordingly.
Rules & constraints (important)
Same rules as parameter-based
#[Argument]/#[Option]apply, plus:__construct, values are assigned directly to properties (property hooks run on assignment if present)#[Input]class hasno#[Argument],#[Option]or nested inputs, that's a logic errorWhy this is better
__invoke(#[Input] Foo $foo)instead of a dozen parametersFuture follow-ups (out of scope here)
Command::interact()) to the DTOCheers!