CARVIEW |
Select Language
HTTP/2 200
date: Sun, 12 Oct 2025 07:59:32 GMT
content-type: text/html; charset=utf-8
vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame, X-Requested-With,Accept-Encoding, Accept, X-Requested-With
x-repository-download: git clone https://github.com/symfony/symfony.git
etag: W/"bc459173f779655cb9da326f32370de6"
cache-control: max-age=0, private, must-revalidate
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: no-referrer-when-downgrade
content-security-policy: default-src 'none'; base-uri 'self'; child-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/; connect-src 'self' uploads.github.com www.githubstatus.com collector.github.com raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com *.rel.tunnels.api.visualstudio.com wss://*.rel.tunnels.api.visualstudio.com github.githubassets.com objects-origin.githubusercontent.com copilot-proxy.githubusercontent.com proxy.individual.githubcopilot.com proxy.business.githubcopilot.com proxy.enterprise.githubcopilot.com *.actions.githubusercontent.com wss://*.actions.githubusercontent.com productionresultssa0.blob.core.windows.net/ productionresultssa1.blob.core.windows.net/ productionresultssa2.blob.core.windows.net/ productionresultssa3.blob.core.windows.net/ productionresultssa4.blob.core.windows.net/ productionresultssa5.blob.core.windows.net/ productionresultssa6.blob.core.windows.net/ productionresultssa7.blob.core.windows.net/ productionresultssa8.blob.core.windows.net/ productionresultssa9.blob.core.windows.net/ productionresultssa10.blob.core.windows.net/ productionresultssa11.blob.core.windows.net/ productionresultssa12.blob.core.windows.net/ productionresultssa13.blob.core.windows.net/ productionresultssa14.blob.core.windows.net/ productionresultssa15.blob.core.windows.net/ productionresultssa16.blob.core.windows.net/ productionresultssa17.blob.core.windows.net/ productionresultssa18.blob.core.windows.net/ productionresultssa19.blob.core.windows.net/ github-production-repository-image-32fea6.s3.amazonaws.com github-production-release-asset-2e65be.s3.amazonaws.com insights.github.com wss://alive.github.com wss://alive-staging.github.com api.githubcopilot.com api.individual.githubcopilot.com api.business.githubcopilot.com api.enterprise.githubcopilot.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com copilot-workspace.githubnext.com objects-origin.githubusercontent.com; frame-ancestors 'none'; frame-src viewscreen.githubusercontent.com notebooks.githubusercontent.com; img-src 'self' data: blob: github.githubassets.com media.githubusercontent.com camo.githubusercontent.com identicons.github.com avatars.githubusercontent.com private-avatars.githubusercontent.com github-cloud.s3.amazonaws.com objects.githubusercontent.com release-assets.githubusercontent.com secured-user-images.githubusercontent.com/ user-images.githubusercontent.com/ private-user-images.githubusercontent.com opengraph.githubassets.com marketplace-screenshots.githubusercontent.com/ copilotprodattachments.blob.core.windows.net/github-production-copilot-attachments/ github-production-user-asset-6210df.s3.amazonaws.com customer-stories-feed.github.com spotlights-feed.github.com objects-origin.githubusercontent.com *.githubusercontent.com; manifest-src 'self'; media-src github.com user-images.githubusercontent.com/ secured-user-images.githubusercontent.com/ private-user-images.githubusercontent.com github-production-user-asset-6210df.s3.amazonaws.com gist.github.com; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; upgrade-insecure-requests; worker-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/
server: github.com
content-encoding: gzip
accept-ranges: bytes
set-cookie: _gh_sess=yUXVrk27sTlQbWA1tH3Cg8e6opMIstp7A5xVWd%2FeXnyXn57YmS4PUiEatjNQkJIbhnC6fhXFm5%2FvTU00faiIyg2vRGJCt0kGBNQP8vxVkQL3bp09%2Bac%2F9zTKknAkEQpSz6YF3h2cdIKZklghnGwwjGE9JySnYYlzOVi8HtUx9T00fpxMVDfwzgHMfirg%2By%2FKZMi8NPnbjAuOkURENr86%2FfJo7JCzVQHtm23acnVw3taQGthATqeyu4pDaSwANcTxQTHRoenSeRdIO8crUv58BQ%3D%3D--qprOuZP32KgvLXd1--6ZmtBn5iIPFhoUtGGr4Q9Q%3D%3D; Path=/; HttpOnly; Secure; SameSite=Lax
set-cookie: _octo=GH1.1.1110055753.1760255971; Path=/; Domain=github.com; Expires=Mon, 12 Oct 2026 07:59:31 GMT; Secure; SameSite=Lax
set-cookie: logged_in=no; Path=/; Domain=github.com; Expires=Mon, 12 Oct 2026 07:59:31 GMT; HttpOnly; Secure; SameSite=Lax
x-github-request-id: A0EA:698AA:2B5DE2:3C63BA:68EB5FE3
feature #61748 [Console] Add support for interactive invokable comman… · symfony/symfony@770dc40 · GitHub
Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/Argument.php
Copy file name to clipboard
Copy file name to clipboard
Copy file name to clipboard
Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/MapInput.php
Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/Reflection/ReflectionMember.php
Skip to content
Navigation Menu
{{ message }}
-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Commit 770dc40
committed
feature #61748 [Console] Add support for interactive invokable commands with
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 | 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:**
```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 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:
```php
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:
```bash
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:
```php
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:
```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` 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!
Commits
-------
6e837c4 [Console] Add support for interactive invokable commands with `#[Interact]` and `#[Ask]` attributes#[Interact]
and #[Ask]
attributes (yceruto)File tree
Expand file treeCollapse file tree
13 files changed
+565
-22
lines changedFilter options
- src/Symfony/Component/Console
- Attribute
- Reflection
- Command
- Interaction
- Tests
- Fixtures
- Tester
Expand file treeCollapse file tree
13 files changed
+565
-22
lines changedCollapse file: src/Symfony/Component/Console/Attribute/Argument.php
src/Symfony/Component/Console/Attribute/Argument.php
Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/Argument.php+17-1Lines changed: 17 additions & 1 deletion
Original file line number | Diff line number | Diff line change | |
---|---|---|---|
| |||
32 | 32 |
| |
33 | 33 |
| |
34 | 34 |
| |
| 35 | + | |
35 | 36 |
| |
36 | 37 |
| |
37 | 38 |
| |
| |||
79 | 80 |
| |
80 | 81 |
| |
81 | 82 |
| |
82 |
| - | |
| 83 | + | |
| 84 | + | |
83 | 85 |
| |
84 | 86 |
| |
85 | 87 |
| |
| |||
92 | 94 |
| |
93 | 95 |
| |
94 | 96 |
| |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
95 | 103 |
| |
96 | 104 |
| |
97 | 105 |
| |
| |||
118 | 126 |
| |
119 | 127 |
| |
120 | 128 |
| |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
121 | 137 |
|
Collapse file: src/Symfony/Component/Console/Attribute/Ask.php
src/Symfony/Component/Console/Attribute/Ask.php
Copy file name to clipboard+110Lines changed: 110 additions & 0 deletions
Original file line number | Diff line number | Diff line change | |
---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + |
Collapse file: src/Symfony/Component/Console/Attribute/Interact.php
src/Symfony/Component/Console/Attribute/Interact.php
Copy file name to clipboard+51Lines changed: 51 additions & 0 deletions
Original file line number | Diff line number | Diff line change | |
---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + |
Collapse file: src/Symfony/Component/Console/Attribute/InteractiveAttributeInterface.php
src/Symfony/Component/Console/Attribute/InteractiveAttributeInterface.php
Copy file name to clipboard+20Lines changed: 20 additions & 0 deletions
Original file line number | Diff line number | Diff line change | |
---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + |
Collapse file: src/Symfony/Component/Console/Attribute/MapInput.php
src/Symfony/Component/Console/Attribute/MapInput.php
Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/MapInput.php+81-12Lines changed: 81 additions & 12 deletions
Original file line number | Diff line number | Diff line change | |
---|---|---|---|
| |||
14 | 14 |
| |
15 | 15 |
| |
16 | 16 |
| |
| 17 | + | |
17 | 18 |
| |
18 | 19 |
| |
19 | 20 |
| |
| |||
28 | 29 |
| |
29 | 30 |
| |
30 | 31 |
| |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
31 | 40 |
| |
32 | 41 |
| |
33 | 42 |
| |
| |||
49 | 58 |
| |
50 | 59 |
| |
51 | 60 |
| |
52 |
| - | |
53 |
| - | |
54 |
| - | |
55 |
| - | |
56 | 61 |
| |
57 | 62 |
| |
58 |
| - | |
59 |
| - | |
60 |
| - | |
61 |
| - | |
| 63 | + | |
62 | 64 |
| |
63 |
| - | |
| 65 | + | |
| 66 | + | |
64 | 67 |
| |
65 | 68 |
| |
66 |
| - | |
67 |
| - | |
| 69 | + | |
| 70 | + | |
68 | 71 |
| |
69 | 72 |
| |
70 | 73 |
| |
71 | 74 |
| |
72 | 75 |
| |
73 | 76 |
| |
74 | 77 |
| |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
75 | 84 |
| |
76 | 85 |
| |
77 | 86 |
| |
78 | 87 |
| |
79 | 88 |
| |
80 | 89 |
| |
81 |
| - | |
| 90 | + | |
82 | 91 |
| |
83 | 92 |
| |
84 | 93 |
| |
85 | 94 |
| |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
86 | 100 |
| |
87 | 101 |
| |
88 | 102 |
| |
89 | 103 |
| |
90 | 104 |
| |
91 | 105 |
| |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
92 | 127 |
| |
93 | 128 |
| |
94 | 129 |
| |
| |||
116 | 151 |
| |
117 | 152 |
| |
118 | 153 |
| |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
119 | 188 |
|
Collapse file: src/Symfony/Component/Console/Attribute/Reflection/ReflectionMember.php
src/Symfony/Component/Console/Attribute/Reflection/ReflectionMember.php
Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/Reflection/ReflectionMember.php+15Lines changed: 15 additions & 0 deletions
Original file line number | Diff line number | Diff line change | |
---|---|---|---|
| |||
96 | 96 |
| |
97 | 97 |
| |
98 | 98 |
| |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
99 | 114 |
|
You can’t perform that action at this time.
0 commit comments