Option.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console\Attribute;
  11. use Symfony\Component\Console\Completion\CompletionInput;
  12. use Symfony\Component\Console\Completion\Suggestion;
  13. use Symfony\Component\Console\Exception\LogicException;
  14. use Symfony\Component\Console\Input\InputInterface;
  15. use Symfony\Component\Console\Input\InputOption;
  16. use Symfony\Component\String\UnicodeString;
  17. #[\Attribute(\Attribute::TARGET_PARAMETER)]
  18. class Option
  19. {
  20. private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
  21. private const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float'];
  22. private string|bool|int|float|array|null $default = null;
  23. private array|\Closure $suggestedValues;
  24. private ?int $mode = null;
  25. private string $typeName = '';
  26. private bool $allowNull = false;
  27. private string $function = '';
  28. /**
  29. * Represents a console command --option definition.
  30. *
  31. * If unset, the `name` value 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 array<string|Suggestion>|callable(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
  35. */
  36. public function __construct(
  37. public string $description = '',
  38. public string $name = '',
  39. public array|string|null $shortcut = null,
  40. array|callable $suggestedValues = [],
  41. ) {
  42. $this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
  43. }
  44. /**
  45. * @internal
  46. */
  47. public static function tryFrom(\ReflectionParameter $parameter): ?self
  48. {
  49. /** @var self $self */
  50. if (null === $self = ($parameter->getAttributes(self::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance()) {
  51. return null;
  52. }
  53. if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) {
  54. $self->function = $function->class.'::'.$function->name;
  55. } else {
  56. $self->function = $function->name;
  57. }
  58. $name = $parameter->getName();
  59. $type = $parameter->getType();
  60. if (!$parameter->isDefaultValueAvailable()) {
  61. throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must declare a default value.', $name, $self->function));
  62. }
  63. if (!$self->name) {
  64. $self->name = (new UnicodeString($name))->kebab();
  65. }
  66. $self->default = $parameter->getDefaultValue();
  67. $self->allowNull = $parameter->allowsNull();
  68. if ($type instanceof \ReflectionUnionType) {
  69. return $self->handleUnion($type);
  70. }
  71. if (!$type instanceof \ReflectionNamedType) {
  72. throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped or Intersection types are not supported for command options.', $name, $self->function));
  73. }
  74. $self->typeName = $type->getName();
  75. if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) {
  76. throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES)));
  77. }
  78. if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) {
  79. throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must not be nullable when it has a default boolean value.', $name, $self->function));
  80. }
  81. if ($self->allowNull && null !== $self->default) {
  82. throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must either be not-nullable or have a default of null.', $name, $self->function));
  83. }
  84. if ('bool' === $self->typeName) {
  85. $self->mode = InputOption::VALUE_NONE;
  86. if (false !== $self->default) {
  87. $self->mode |= InputOption::VALUE_NEGATABLE;
  88. }
  89. } elseif ('array' === $self->typeName) {
  90. $self->mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY;
  91. } else {
  92. $self->mode = InputOption::VALUE_REQUIRED;
  93. }
  94. 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]])) {
  95. $self->suggestedValues = [$instance, $self->suggestedValues[1]];
  96. }
  97. return $self;
  98. }
  99. /**
  100. * @internal
  101. */
  102. public function toInputOption(): InputOption
  103. {
  104. $default = InputOption::VALUE_NONE === (InputOption::VALUE_NONE & $this->mode) ? null : $this->default;
  105. $suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
  106. return new InputOption($this->name, $this->shortcut, $this->mode, $this->description, $default, $suggestedValues);
  107. }
  108. /**
  109. * @internal
  110. */
  111. public function resolveValue(InputInterface $input): mixed
  112. {
  113. $value = $input->getOption($this->name);
  114. if (null === $value && \in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
  115. return true;
  116. }
  117. if ('array' === $this->typeName && $this->allowNull && [] === $value) {
  118. return null;
  119. }
  120. if ('bool' !== $this->typeName) {
  121. return $value;
  122. }
  123. if ($this->allowNull && null === $value) {
  124. return null;
  125. }
  126. return $value ?? $this->default;
  127. }
  128. private function handleUnion(\ReflectionUnionType $type): self
  129. {
  130. $types = array_map(
  131. static fn (\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null,
  132. $type->getTypes(),
  133. );
  134. sort($types);
  135. $this->typeName = implode('|', array_filter($types));
  136. if (!\in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
  137. throw new LogicException(\sprintf('The union type for parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $this->name, $this->function, implode('", "', self::ALLOWED_UNION_TYPES)));
  138. }
  139. if (false !== $this->default) {
  140. throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must have a default value of false.', $this->name, $this->function));
  141. }
  142. $this->mode = InputOption::VALUE_OPTIONAL;
  143. return $this;
  144. }
  145. }