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

Appearance settings

Commit cf6c3aa

Browse filesBrowse files
ycerutochalasr
authored andcommitted
Add support for invokable commands and input attributes
1 parent c98cfb6 commit cf6c3aa
Copy full SHA for cf6c3aa

File tree

Expand file treeCollapse file tree

13 files changed

+542
-29
lines changed
Filter options
Expand file treeCollapse file tree

13 files changed

+542
-29
lines changed

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
use Symfony\Component\Config\Resource\FileResource;
5050
use Symfony\Component\Config\ResourceCheckerInterface;
5151
use Symfony\Component\Console\Application;
52+
use Symfony\Component\Console\Attribute\AsCommand;
5253
use Symfony\Component\Console\Command\Command;
5354
use Symfony\Component\Console\DataCollector\CommandDataCollector;
5455
use Symfony\Component\Console\Debug\CliRequest;
@@ -608,6 +609,9 @@ public function load(array $configs, ContainerBuilder $container): void
608609
->addTag('assets.package');
609610
$container->registerForAutoconfiguration(AssetCompilerInterface::class)
610611
->addTag('asset_mapper.compiler');
612+
$container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute, \ReflectionClass $reflector): void {
613+
$definition->addTag('console.command', ['command' => $attribute->name, 'description' => $attribute->description ?? $reflector->getName()]);
614+
});
611615
$container->registerForAutoconfiguration(Command::class)
612616
->addTag('console.command');
613617
$container->registerForAutoconfiguration(ResourceCheckerInterface::class)
+104Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Attribute;
13+
14+
use Symfony\Component\Console\Completion\CompletionInput;
15+
use Symfony\Component\Console\Completion\Suggestion;
16+
use Symfony\Component\Console\Exception\LogicException;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
20+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
21+
class Argument
22+
{
23+
private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
24+
25+
private ?int $mode = null;
26+
27+
/**
28+
* Represents a console command <argument> definition.
29+
*
30+
* If unset, the `name` and `default` values will be inferred from the parameter definition.
31+
*
32+
* @param string|bool|int|float|array|null $default The default value (for InputArgument::OPTIONAL mode only)
33+
* @param array|callable-string(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
34+
*/
35+
public function __construct(
36+
public string $name = '',
37+
public string $description = '',
38+
public string|bool|int|float|array|null $default = null,
39+
public array|string $suggestedValues = [],
40+
) {
41+
if (\is_string($suggestedValues) && !\is_callable($suggestedValues)) {
42+
throw new \TypeError(\sprintf('Argument 4 passed to "%s()" must be either an array or a callable-string.', __METHOD__));
43+
}
44+
}
45+
46+
/**
47+
* @internal
48+
*/
49+
public static function tryFrom(\ReflectionParameter $parameter): ?self
50+
{
51+
/** @var self $self */
52+
if (null === $self = ($parameter->getAttributes(self::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance()) {
53+
return null;
54+
}
55+
56+
$type = $parameter->getType();
57+
$name = $parameter->getName();
58+
59+
if (!$type instanceof \ReflectionNamedType) {
60+
throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name));
61+
}
62+
63+
$parameterTypeName = $type->getName();
64+
65+
if (!\in_array($parameterTypeName, self::ALLOWED_TYPES, true)) {
66+
throw new LogicException(\sprintf('The type "%s" of parameter "$%s" is not supported as a command argument. Only "%s" types are allowed.', $parameterTypeName, $name, implode('", "', self::ALLOWED_TYPES)));
67+
}
68+
69+
if (!$self->name) {
70+
$self->name = $name;
71+
}
72+
73+
$self->mode = null !== $self->default || $parameter->isDefaultValueAvailable() ? InputArgument::OPTIONAL : InputArgument::REQUIRED;
74+
if ('array' === $parameterTypeName) {
75+
$self->mode |= InputArgument::IS_ARRAY;
76+
}
77+
78+
$self->default ??= $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
79+
80+
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $parameter->getDeclaringFunction()->getClosureThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
81+
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
82+
}
83+
84+
return $self;
85+
}
86+
87+
/**
88+
* @internal
89+
*/
90+
public function toInputArgument(): InputArgument
91+
{
92+
$suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
93+
94+
return new InputArgument($this->name, $this->mode, $this->description, $this->default, $suggestedValues);
95+
}
96+
97+
/**
98+
* @internal
99+
*/
100+
public function resolveValue(InputInterface $input): mixed
101+
{
102+
return $input->hasArgument($this->name) ? $input->getArgument($this->name) : null;
103+
}
104+
}
+119Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Attribute;
13+
14+
use Symfony\Component\Console\Completion\CompletionInput;
15+
use Symfony\Component\Console\Completion\Suggestion;
16+
use Symfony\Component\Console\Exception\LogicException;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
20+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
21+
class Option
22+
{
23+
private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
24+
25+
private ?int $mode = null;
26+
private string $typeName = '';
27+
28+
/**
29+
* Represents a console command --option definition.
30+
*
31+
* If unset, the `name` and `default` values will be inferred from the parameter definition.
32+
*
33+
* @param array|string|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
34+
* @param scalar|array|null $default The default value (must be null for self::VALUE_NONE)
35+
* @param array|callable-string(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
36+
*/
37+
public function __construct(
38+
public string $name = '',
39+
public array|string|null $shortcut = null,
40+
public string $description = '',
41+
public string|bool|int|float|array|null $default = null,
42+
public array|string $suggestedValues = [],
43+
) {
44+
if (\is_string($suggestedValues) && !\is_callable($suggestedValues)) {
45+
throw new \TypeError(\sprintf('Argument 5 passed to "%s()" must be either an array or a callable-string.', __METHOD__));
46+
}
47+
}
48+
49+
/**
50+
* @internal
51+
*/
52+
public static function tryFrom(\ReflectionParameter $parameter): ?self
53+
{
54+
/** @var self $self */
55+
if (null === $self = ($parameter->getAttributes(self::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance()) {
56+
return null;
57+
}
58+
59+
$type = $parameter->getType();
60+
$name = $parameter->getName();
61+
62+
if (!$type instanceof \ReflectionNamedType) {
63+
throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported for command options.', $name));
64+
}
65+
66+
$self->typeName = $type->getName();
67+
68+
if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) {
69+
throw new LogicException(\sprintf('The type "%s" of parameter "$%s" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, implode('", "', self::ALLOWED_TYPES)));
70+
}
71+
72+
if (!$self->name) {
73+
$self->name = $name;
74+
}
75+
76+
if ('bool' === $self->typeName) {
77+
$self->mode = InputOption::VALUE_NONE | InputOption::VALUE_NEGATABLE;
78+
} else {
79+
$self->mode = null !== $self->default || $parameter->isDefaultValueAvailable() ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
80+
if ('array' === $self->typeName) {
81+
$self->mode |= InputOption::VALUE_IS_ARRAY;
82+
}
83+
}
84+
85+
if (InputOption::VALUE_NONE === (InputOption::VALUE_NONE & $self->mode)) {
86+
$self->default = null;
87+
} else {
88+
$self->default ??= $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
89+
}
90+
91+
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $parameter->getDeclaringFunction()->getClosureThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
92+
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
93+
}
94+
95+
return $self;
96+
}
97+
98+
/**
99+
* @internal
100+
*/
101+
public function toInputOption(): InputOption
102+
{
103+
$suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
104+
105+
return new InputOption($this->name, $this->shortcut, $this->mode, $this->description, $this->default, $suggestedValues);
106+
}
107+
108+
/**
109+
* @internal
110+
*/
111+
public function resolveValue(InputInterface $input): mixed
112+
{
113+
if ('bool' === $this->typeName) {
114+
return $input->hasOption($this->name) && null !== $input->getOption($this->name) ? $input->getOption($this->name) : ($this->default ?? false);
115+
}
116+
117+
return $input->hasOption($this->name) ? $input->getOption($this->name) : null;
118+
}
119+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/CHANGELOG.md
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Add support for invokable commands
8+
* Add `#[Argument]` and `#[Option]` attributes to define input arguments and options for invokable commands
9+
410
7.2
511
---
612

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Command/Command.php
+14-7Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class Command
4949
private string $description = '';
5050
private ?InputDefinition $fullDefinition = null;
5151
private bool $ignoreValidationErrors = false;
52-
private ?\Closure $code = null;
52+
private ?InvokableCommand $code = null;
5353
private array $synopsis = [];
5454
private array $usages = [];
5555
private ?HelperSet $helperSet = null;
@@ -164,6 +164,9 @@ public function isEnabled(): bool
164164
*/
165165
protected function configure()
166166
{
167+
if (!$this->code && \is_callable($this)) {
168+
$this->code = new InvokableCommand($this, $this(...));
169+
}
167170
}
168171

169172
/**
@@ -274,12 +277,10 @@ public function run(InputInterface $input, OutputInterface $output): int
274277
$input->validate();
275278

276279
if ($this->code) {
277-
$statusCode = ($this->code)($input, $output);
278-
} else {
279-
$statusCode = $this->execute($input, $output);
280+
return ($this->code)($input, $output);
280281
}
281282

282-
return is_numeric($statusCode) ? (int) $statusCode : 0;
283+
return $this->execute($input, $output);
283284
}
284285

285286
/**
@@ -327,7 +328,7 @@ public function setCode(callable $code): static
327328
$code = $code(...);
328329
}
329330

330-
$this->code = $code;
331+
$this->code = new InvokableCommand($this, $code);
331332

332333
return $this;
333334
}
@@ -395,7 +396,13 @@ public function getDefinition(): InputDefinition
395396
*/
396397
public function getNativeDefinition(): InputDefinition
397398
{
398-
return $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
399+
$definition = $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
400+
401+
if ($this->code && !$definition->getArguments() && !$definition->getOptions()) {
402+
$this->code->configure($definition);
403+
}
404+
405+
return $definition;
399406
}
400407

401408
/**

0 commit comments

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