Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cf6c3aa

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

File tree

13 files changed

+542
-29
lines changed

13 files changed

+542
-29
lines changed

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

+4
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)
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 <[email protected]>
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+
}
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 <[email protected]>
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

+6
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

+14-7
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)