Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings

Commit 0270644

Browse filesBrowse the repository at this point in the historyBrowse files
feature #61677 [Console] Allow callables for the description and help of #[AsCommand], #[Argument] and #[Option] (MacDada)
This PR was merged into the 8.2 branch. Discussion ---------- [Console] Allow callables for the description and help of `#[AsCommand]`, `#[Argument]` and `#[Option]` | Q | A | ------------- | --- | Branch? | 8.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT | Doc PR | TODO Attribute arguments are constant expressions, and PHP allows no function calls there. A description that has to be computed, typically one listing the accepted values, is therefore not expressible today: ```php public function __invoke( SymfonyStyle $io, // Fatal error: Constant expression contains invalid operations #[Argument(description: self::generateDescription())] string $status, ): int { } ``` This PR makes `description` on `#[Argument]` and `#[Option]`, and `description` and `help` on `#[AsCommand]`, accept a callable next to a string: ```php public function __invoke( SymfonyStyle $io, #[Argument(description: [self::class, 'generateDescription'])] string $status, ): int { } public static function generateDescription(): string { return 'One of: '.implode(', ', array_column(Status::cases(), 'value')); } ``` The callable is called once, when the attribute is instantiated, and the attribute exposes the resolved string, so nothing downstream sees a callable. A `string` is never resolved as a callable: `description: 'define'` is the description "define", not a call to `define()`. On PHP 8.5 no separate method is needed, since constant expressions accept static closures and first-class callable syntax: ```php #[Argument(description: static function (): string { return 'One of: '.implode(', ', array_column(Status::cases(), 'value')); })] string $status = '', ``` Arrow functions stay rejected there, as they capture by value. `usages` on `#[AsCommand]` is deliberately left out, since an array of strings and an array callable cannot be told apart. Commits ------- a6a5fa7 [Console] Allow callable for the `description` and `help` options of `#[AsCommand]`, `#[Argument]` and `#[Option]`
2 parents 3feb3b5 + a6a5fa7 commit 0270644
Copy full SHA for 0270644

6 files changed

+88-6Lines changed: 88 additions & 6 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/Symfony/Component/Console/Attribute/Argument.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/Argument.php
+4-1Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
2222
class Argument
2323
{
24+
public string $description;
2425
public mixed $default = null;
2526
public array|\Closure $suggestedValues;
2627

@@ -38,13 +39,15 @@ class Argument
3839
*
3940
* If unset, the `name` value will be inferred from the parameter definition.
4041
*
42+
* @param string|(callable():string) $description The description of the argument, displayed with the help page
4143
* @param array<string|Suggestion>|callable(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
4244
*/
4345
public function __construct(
44-
public string $description = '',
46+
string|callable $description = '',
4547
public string $name = '',
4648
array|callable $suggestedValues = [],
4749
) {
50+
$this->description = \is_string($description) ? $description : $description();
4851
$this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
4952
}
5053

Collapse file

‎src/Symfony/Component/Console/Attribute/AsCommand.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/AsCommand.php
+10-4Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,30 @@
1919
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
2020
final class AsCommand
2121
{
22+
public ?string $description;
23+
public ?string $help;
24+
2225
/**
2326
* @param string $name The name of the command, used when calling it (i.e. "cache:clear")
24-
* @param string|null $description The description of the command, displayed with the help page
27+
* @param string|(callable():string)|null $description The description of the command, displayed with the help page
2528
* @param string[] $aliases The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean")
2629
* @param bool $hidden If true, the command won't be shown when listing all the available commands, but it can still be run as any other command
27-
* @param string|null $help The help content of the command, displayed with the help page
30+
* @param string|(callable():string)|null $help The help content of the command, displayed with the help page
2831
* @param string[] $usages The list of usage examples, displayed with the help page
2932
* @param OutputInterface::VERBOSITY_*|null $listedAt The verbosity from which the command is listed, null to always list it. It can still be run as any other command
3033
*/
3134
public function __construct(
3235
public string $name,
33-
public ?string $description = null,
36+
string|callable|null $description = null,
3437
array $aliases = [],
3538
bool $hidden = false,
36-
public ?string $help = null,
39+
string|callable|null $help = null,
3740
public array $usages = [],
3841
public ?int $listedAt = null,
3942
) {
43+
$this->description = null === $description || \is_string($description) ? $description : $description();
44+
$this->help = null === $help || \is_string($help) ? $help : $help();
45+
4046
if (!$hidden && !$aliases) {
4147
return;
4248
}
Collapse file

‎src/Symfony/Component/Console/Attribute/Option.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Attribute/Option.php
+4-1Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
class Option
2323
{
2424
public const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float'];
25+
public string $description;
2526
public mixed $default = null;
2627
public array|\Closure $suggestedValues;
2728

@@ -42,17 +43,19 @@ class Option
4243
*
4344
* If unset, the `name` value will be inferred from the parameter definition.
4445
*
46+
* @param string|(callable():string) $description The description of the option, displayed with the help page
4547
* @param array|string|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
4648
* @param array<string|Suggestion>|callable(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
4749
*/
4850
public function __construct(
49-
public string $description = '',
51+
string|callable $description = '',
5052
public string $name = '',
5153
public array|string|null $shortcut = null,
5254
array|callable $suggestedValues = [],
5355
public bool $deprecated = false,
5456
public bool $hidden = false,
5557
) {
58+
$this->description = \is_string($description) ? $description : $description();
5659
$this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
5760
}
5861

Collapse file

‎src/Symfony/Component/Console/CHANGELOG.md‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `InputOption::HIDDEN` and `InputOption::DEPRECATED` modes
88
* Add the `listedAt` option to `#[AsCommand]` and the `listed_at` attribute to the `console.command` tag, to list a command only from the given verbosity
9+
* Allow a callable for the `description` and `help` options of `#[AsCommand]`, and for the `description` option of `#[Argument]` and `#[Option]`
910

1011
8.1
1112
---
Collapse file

‎src/Symfony/Component/Console/Tests/Command/CommandTest.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Tests/Command/CommandTest.php
+35Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,23 @@ public function testCommandAttribute()
511511
$this->assertNull($command->getCode());
512512
}
513513

514+
public function testCommandAttributeWithCallables()
515+
{
516+
$command = new Php8CommandWithCallables();
517+
518+
$this->assertSame('Generated description', $command->getDescription());
519+
$this->assertSame('Generated help', $command->getHelp());
520+
}
521+
522+
public function testCommandAttributeDescriptionIsNeverResolvedAsCallable()
523+
{
524+
// "define" is the name of a PHP function, it must still be taken as a plain description
525+
$command = new #[AsCommand(name: 'foo', description: 'define', help: 'define')] class extends Command {};
526+
527+
$this->assertSame('define', $command->getDescription());
528+
$this->assertSame('define', $command->getHelp());
529+
}
530+
514531
public function testDefaultCommand()
515532
{
516533
$apl = new Application();
@@ -559,3 +576,21 @@ class Php8Command extends Command
559576
class Php8Command2 extends Command
560577
{
561578
}
579+
580+
#[AsCommand(
581+
name: 'foo3',
582+
description: [Php8CommandWithCallables::class, 'generateDescription'],
583+
help: [Php8CommandWithCallables::class, 'generateHelp'],
584+
)]
585+
class Php8CommandWithCallables extends Command
586+
{
587+
public static function generateDescription(): string
588+
{
589+
return 'Generated description';
590+
}
591+
592+
public static function generateHelp(): string
593+
{
594+
return 'Generated help';
595+
}
596+
}
Collapse file

‎src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,35 @@ public function testCommandInputArgumentDefinition()
9292
self::assertSame(['ROLE_ADMIN', 'ROLE_USER'], array_map(static fn (Suggestion $s) => $s->getValue(), $suggestions->getValueSuggestions()));
9393
}
9494

95+
public function testCommandInputDescriptionFromCallable()
96+
{
97+
$command = new Command('foo');
98+
$command->setCode(static function (
99+
#[Argument(description: [self::class, 'generateBioDescription'])] string $bio = '',
100+
#[Option(description: [self::class, 'generateBioDescription'])] string $summary = '',
101+
): int {
102+
return 0;
103+
});
104+
105+
self::assertSame('Generated bio description', $command->getDefinition()->getArgument('bio')->getDescription());
106+
self::assertSame('Generated bio description', $command->getDefinition()->getOption('summary')->getDescription());
107+
}
108+
109+
public function testCommandInputDescriptionIsNeverResolvedAsCallable()
110+
{
111+
$command = new Command('foo');
112+
$command->setCode(static function (
113+
// "define" is the name of a PHP function, it must still be taken as a plain description
114+
#[Argument(description: 'define')] string $bio = '',
115+
#[Option(description: 'define')] string $summary = '',
116+
): int {
117+
return 0;
118+
});
119+
120+
self::assertSame('define', $command->getDefinition()->getArgument('bio')->getDescription());
121+
self::assertSame('define', $command->getDefinition()->getOption('summary')->getDescription());
122+
}
123+
95124
public function testCommandInputOptionDefinition()
96125
{
97126
$command = new Command('foo');
@@ -600,6 +629,11 @@ public function getSuggestedRoles(CompletionInput $input): array
600629
return ['ROLE_ADMIN', 'ROLE_USER'];
601630
}
602631

632+
public static function generateBioDescription(): string
633+
{
634+
return 'Generated bio description';
635+
}
636+
603637
public function testAskWithInputFileAndConstraints()
604638
{
605639
if (!\extension_loaded('fileinfo')) {

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.