Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[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

Merged
fabpot merged 1 commit intosymfony:7.4fromyceruto:console_input_attribute
Sep 6, 2025

Conversation

@yceruto
Copy link
Member

@ycerutoyceruto commentedAug 21, 2025
edited
Loading

QA
Branch?7.4
Bug fix?no
New feature?yes
Deprecations?no
Issues-
LicenseMIT

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

  • You put#[Input] on a__invoke() parameter, typed as your DTO class (it's not limited to a single parameter)
  • Inside that DTO, public non-static properties marked with#[Argument] and#[Option] become the command's input definition
  • At runtime, Symfony instantiates the DTO (without calling its constructor), resolves CLI values, and assigns them to the properties (recursively, if you nest DTOs)

Example

class UserInput{    #[Argument]publicstring$email {        set =>strtolower($value);// normalize with a property hook    }    #[Argument]publicstring$password;    #[Option]publicbool$admin =false;}#[AsCommand('app:create-user')]class CreateUserCommand{publicfunction__invoke(#[Input]UserInput$user):int    {// use $user->email, $user->password, $user->adminreturn0;    }}

This produces the usage:

$ bin/console help app:create-userUsage:  app:create-user [options] [--] <email> <password>Arguments:  email  passwordOptions:      --admin|--no-admin

(booleans render as--foo/--no-foo as usual)

Nested DTOs (group inputs)

You can compose inputs by nesting DTOs and giving them a form:

class UserInput{    #[Argument]publicstring$email;    #[Argument]publicstring$password;    #[Option]publicbool$active =true;    #[Input]publicProfileInput$profile;}class ProfileInput{    #[Argument]publicstring$name;        #[Option]public ?string$phone =null;}

The resulting signature merges everything:

Usage:  app:create-user [options] [--] <email> <password> <name>Arguments:  email                       password                    name                      Options:      --active|--no-active        --phone=PHONE

The resolver walks nested DTOs and fills them accordingly.

Rules & constraints (important)

Same rules as parameter-based#[Argument]/#[Option] apply, plus:

  • Public, non-static properties only. Private/protected/static properties are ignored
  • Constructor is not called. The DTO is instantiated without invoking__construct, values are assigned directly to properties (property hooks run on assignment if present)
  • If an#[Input] class hasno#[Argument],#[Option] or nested inputs, that's a logic error

Why this is better

  • One source of truth for command input (definition + mapping live in the DTO)
  • Shorter commands:__invoke(#[Input] Foo $foo) instead of a dozen parameters
  • Composable: group related inputs with nested DTOs
  • Built-in normalization: use property hooks on DTO properties (e.g. lowercase emails)

Future follow-ups (out of scope here)

  • Automatic validation (if Validator is installed) with CLI-friendly violations
  • Delegated interactivity (Command::interact()) to the DTO

Cheers!

94noni, andreybolonin, and BafS reacted with thumbs up emojiOskarStark, mtarld, damienfern, adamwojs, GromNaN, alamirault, rvanlaak, W0rma, and yceruto reacted with heart emoji
@carsonbotcarsonbot added this to the7.4 milestoneAug 21, 2025
@ycerutoyceruto added the DXDX = Developer eXperience (anything that improves the experience of using Symfony) labelAug 21, 2025
@chalasr
Copy link
Member

Nice, it was on my list!

yceruto reacted with heart emoji

@yceruto
Copy link
MemberAuthor

Nice, it was on my list!

it seems we shared the same list at some point :)

@ycerutoycerutoforce-pushed theconsole_input_attribute branch from1a792fb to0ed04d7CompareAugust 22, 2025 21:53
@yceruto
Copy link
MemberAuthor

Appreciate the feedback@nicolas-grekas 🙏, all points have been addressed.

@ycerutoycerutoforce-pushed theconsole_input_attribute branch 3 times, most recently from6cdbc96 to84d092fCompareAugust 23, 2025 09:00
@ycerutoycerutoforce-pushed theconsole_input_attribute branch from84d092f to28f49feCompareAugust 25, 2025 13:31
Copy link
Member

@chalasrchalasr left a comment

Choose a reason for hiding this comment

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

Failures unrelated:shipit:

@yceruto
Copy link
MemberAuthor

The Fabbot failures are false positives, are there any other failures I might have overlooked?

@ycerutoycerutoforce-pushed theconsole_input_attribute branch from28f49fe to8727de8CompareAugust 27, 2025 13:31
@yceruto
Copy link
MemberAuthor

Opps, I didn't see the new CS output, it should be ok now. Thanks!

(remaining checks are unrelated or false positives)

Copy link
Member

@fabpotfabpot left a 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?

rvanlaak and yceruto reacted with hooray emoji
@ycerutoycerutoforce-pushed theconsole_input_attribute branch from8727de8 to273c507CompareSeptember 3, 2025 08:23
@yceruto
Copy link
MemberAuthor

@fabpot Looks super nice! Can we read phpdocs to extract a description for each argument/option?

Yes, absolutely! we can do that. Just to mention though, arguments and options already support adescription option that serves the same purpose, do you think that could be enough for your case?

For example:

class UserInput{    #[Argument(description:'The user email address.')]publicstring$email;}

@ycerutoycerutoforce-pushed theconsole_input_attribute branch from273c507 to7209cbcCompareSeptember 3, 2025 09:30
@ycerutoycerutoforce-pushed theconsole_input_attribute branch from7209cbc to14d0f6bCompareSeptember 3, 2025 09:49
@yceruto
Copy link
MemberAuthor

Thanks for the review! this is ready on my side.

Looking forward to starting work on the follow-up PRs/features :)

Copy link
Contributor

@mtarldmtarld left a comment

Choose a reason for hiding this comment

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

Really nice one 🚀

@ycerutoycerutoforce-pushed theconsole_input_attribute branch from14d0f6b to0cff2ebCompareSeptember 6, 2025 12:32
@fabpot
Copy link
Member

@fabpot Looks super nice! Can we read phpdocs to extract a description for each argument/option?

Yes, absolutely! we can do that. Just to mention though, arguments and options already support adescription option that serves the same purpose, do you think that could be enough for your case?

For example:

class UserInput{    #[Argument(description: 'The user email address.')]    public string $email;}

Indeed, that's good enough.

@fabpot
Copy link
Member

Thank you@yceruto.

@fabpotfabpot merged commitae256f9 intosymfony:7.4Sep 6, 2025
6 of 12 checks passed
@ycerutoyceruto deleted the console_input_attribute branchSeptember 6, 2025 13:23
fabpot added a commit that referenced this pull requestOct 4, 2025
…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
symfony-splitter pushed a commit to symfony/console that referenced this pull requestOct 4, 2025
…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
This was referencedOct 27, 2025
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@fabpotfabpotfabpot approved these changes

@nicolas-grekasnicolas-grekasnicolas-grekas approved these changes

@GromNaNGromNaNGromNaN approved these changes

@mtarldmtarldmtarld approved these changes

@chalasrchalasrchalasr approved these changes

+1 more reviewer

@94noni94noni94noni left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

ConsoleDXDX = Developer eXperience (anything that improves the experience of using Symfony)FeatureStatus: Reviewed

Projects

None yet

Milestone

7.4

Development

Successfully merging this pull request may close these issues.

8 participants

@yceruto@chalasr@fabpot@nicolas-grekas@GromNaN@94noni@mtarld@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp