Skip to content

Conversation

@yceruto
Copy link
Member

@yceruto yceruto commented Sep 15, 2025

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

The #[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:

#[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):

#[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):

#[AsCommand('app:create-user')]
class CreateUserCommand
{
    public function __invoke(
        #[Argument, Ask('Enter the username')] 
        string $username,
    ): int {
        // ...
    }
}

DTO‑friendly interaction

In 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 interactivity

You 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:

class 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:

bin/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 interactivity

For 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:

class 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 prompts

For 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:

#[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 parameters
  2. #[Ask] on DTO properties
  3. #[Interact] on the DTO class
  4. #[Interact] on the command class

More related features will be unveiled later.

Cheers!

@yceruto yceruto requested a review from chalasr as a code owner September 15, 2025 04:34
@carsonbot carsonbot added this to the 7.4 milestone Sep 15, 2025
@yceruto yceruto added the DX DX = Developer eXperience (anything that improves the experience of using Symfony) label Sep 15, 2025
@yceruto yceruto force-pushed the console_iteractive branch 5 times, most recently from b6193b6 to 5871af6 Compare September 15, 2025 17:06
Copy link
Member

@GromNaN GromNaN left a comment

Choose a reason for hiding this comment

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

Very nice feature. I had the idea of adding a Question $question property to the #[Argument] attribute, but #[Ask] provides a better separation of concerns.

@yceruto yceruto force-pushed the console_iteractive branch 2 times, most recently from 6b62f9e to c70568c Compare September 15, 2025 19:02
@yceruto yceruto force-pushed the console_iteractive branch 6 times, most recently from cfd0c52 to fd86d3f Compare September 21, 2025 11:24
@yceruto
Copy link
Member Author

yceruto commented Sep 21, 2025

Updates about Ask attribute:

  • Added default option. This value cannot be taken from the property/parameter default value as doing that, for the question default purpose, will impact the argument definition concerning required/optional argument.
  • Added validator and maxAttempts options, and a built-in question validator for properties, allowing input values to be validated through property hooks while repeatedly prompting the user until valid input is provided
  • Added multiline, trimmable, and timeout options.

It's pending to play with autocompleter values (not a blocker)

@yceruto
Copy link
Member Author

yceruto commented Sep 21, 2025

Example of question value validation using property set hook:

class UserDto
{
    #[Argument, Ask('Enter the user email')]
    public string $email {
        set (?string $email) {
            if (null === $email || '' === $email) {
                throw new InvalidArgumentException('Email cannot be empty.');
            }

            if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException('Email is not valid.');
            }

            $this->email = strtolower($email);
        }
    }

    // ...
}

I particularly like this approach because it guarantees that your DTO will never contain an invalid value.

@yceruto
Copy link
Member Author

yceruto commented Sep 27, 2025

Minor update regarding future #[Choice] (for another PR) with seamless support for BackedEnum inputs (including multiple-choice support):

  • Added InteractiveAttributeInterface to define a contract for interactive attributes, what matter here is get the interactive ReflectionFunction.

@yceruto
Copy link
Member Author

yceruto commented Sep 27, 2025

I believe this is ready for final review, with no remaining tasks on my side.

@yceruto yceruto added the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Sep 29, 2025
Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

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

With minor comments. Nice one!

@yceruto yceruto force-pushed the console_iteractive branch 3 times, most recently from 696bd3d to 9113944 Compare October 1, 2025 14:48
@yceruto
Copy link
Member Author

yceruto commented Oct 1, 2025

Rebased and PR description updated after #61890

Copy link
Member

@alexandre-daubois alexandre-daubois left a comment

Choose a reason for hiding this comment

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

Nice!

@stof
Copy link
Member

stof commented Oct 3, 2025

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.

how does this interact with the validation of required arguments done when validating the input (after the interact state) if they are not set back in the input ?

@yceruto
Copy link
Member Author

yceruto commented Oct 3, 2025

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.

how does this interact with the validation of required arguments done when validating the input (after the interact state) if they are not set back in the input ?

the point is, they are being set back in the input :)

(see the new MapInput::setValue() method)

@yceruto yceruto changed the title [Console] Add support for interactive invokable commands with #[Interactive] and #[InteractiveQuestion] attributes [Console] Add support for interactive invokable commands with #[Interact] and #[Ask] attributes Oct 3, 2025
@yceruto
Copy link
Member Author

yceruto commented Oct 3, 2025

As discussed internally, I renamed the new attribute names:

  • Interactive -> Interact
  • InteractiveQuestion -> Ask

(failures are unrelated btw)

@fabpot fabpot force-pushed the console_iteractive branch from cb30e6e to 6e837c4 Compare October 4, 2025 08:03
@fabpot
Copy link
Member

fabpot commented Oct 4, 2025

Thank you @yceruto.

@fabpot fabpot merged commit 770dc40 into symfony:7.4 Oct 4, 2025
4 of 12 checks passed
@yceruto yceruto deleted the console_iteractive branch October 4, 2025 11:11
chalasr added a commit that referenced this pull request Oct 4, 2025
…nges for interactive invokable commands (yceruto)

This PR was merged into the 7.4 branch.

Discussion
----------

[Console] Update CHANGELOG to reflect attribute name changes for interactive invokable commands

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

Missed this on #61748

Commits
-------

c976917 [Console] Update CHANGELOG to reflect attribute name changes for interactive invokable commands
nicolas-grekas added a commit that referenced this pull request Oct 8, 2025
… (yceruto)

This PR was merged into the 7.4 branch.

Discussion
----------

[Console] Fine-tuning the interactive `#[Ask]` attribute

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

Complements #61748, concerning interactive array and boolean arguments.

**Definition:**
```php
public function __invoke(
    #[Argument, Ask('Must be unique?')]
    bool $unique,

    #[Argument, Ask('Enter the tag (leave blank to finish)')]
    array $tags,
): int
```
**Input:**
```sh
$ bin/console app:add-tags

Must be unique? (yes/no) [no]:
> yes

Enter the tag (leave blank to finish):
> tag1

Enter the tag (leave blank to finish):
> tag2

Enter the tag (leave blank to finish):
>
```
As expected, you'll get a `true` value in `$unique`, and `["tag1", "tag2"]` in `$tags`.

Commits
-------

29ec76c [Console] Fine-tuning the interactive Ask attribute
symfony-splitter pushed a commit to symfony/console that referenced this pull request Oct 8, 2025
… (yceruto)

This PR was merged into the 7.4 branch.

Discussion
----------

[Console] Fine-tuning the interactive `#[Ask]` attribute

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

Complements symfony/symfony#61748, concerning interactive array and boolean arguments.

**Definition:**
```php
public function __invoke(
    #[Argument, Ask('Must be unique?')]
    bool $unique,

    #[Argument, Ask('Enter the tag (leave blank to finish)')]
    array $tags,
): int
```
**Input:**
```sh
$ bin/console app:add-tags

Must be unique? (yes/no) [no]:
> yes

Enter the tag (leave blank to finish):
> tag1

Enter the tag (leave blank to finish):
> tag2

Enter the tag (leave blank to finish):
>
```
As expected, you'll get a `true` value in `$unique`, and `["tag1", "tag2"]` in `$tags`.

Commits
-------

29ec76c80a0 [Console] Fine-tuning the interactive Ask attribute
This was referenced Oct 27, 2025
@VincentLanglet
Copy link
Contributor

Minor update regarding future #[Choice] (for another PR) with seamless support for BackedEnum inputs (including multiple-choice support):

  • Added InteractiveAttributeInterface to define a contract for interactive attributes, what matter here is get the interactive ReflectionFunction.

Hi @yceruto do you still plan working on a #[Choice] attribute for ChoiceQuestion ?

@yceruto
Copy link
Member Author

yceruto commented Oct 29, 2025

Yes, I already have a draft of the #[Choice] attribute on hold until the 8.1 branch is created

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Console DX DX = Developer eXperience (anything that improves the experience of using Symfony) Feature ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" Status: Reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants