Ask.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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\Attribute\Reflection\ReflectionMember;
  12. use Symfony\Component\Console\Exception\InvalidArgumentException;
  13. use Symfony\Component\Console\Exception\LogicException;
  14. use Symfony\Component\Console\Input\InputInterface;
  15. use Symfony\Component\Console\Question\ConfirmationQuestion;
  16. use Symfony\Component\Console\Question\Question;
  17. use Symfony\Component\Console\Style\SymfonyStyle;
  18. #[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
  19. class Ask implements InteractiveAttributeInterface
  20. {
  21. public ?\Closure $normalizer;
  22. public ?\Closure $validator;
  23. private \Closure $closure;
  24. /**
  25. * @param string $question The question to ask the user
  26. * @param string|bool|int|float|null $default The default answer to return if the user enters nothing
  27. * @param bool $hidden Whether the user response must be hidden or not
  28. * @param bool $multiline Whether the user response should accept newline characters
  29. * @param bool $trimmable Whether the user response must be trimmed or not
  30. * @param int|null $timeout The maximum time the user has to answer the question in seconds
  31. * @param callable|null $validator The validator for the question
  32. * @param int|null $maxAttempts The maximum number of attempts allowed to answer the question.
  33. * Null means an unlimited number of attempts
  34. */
  35. public function __construct(
  36. public string $question,
  37. public string|bool|int|float|null $default = null,
  38. public bool $hidden = false,
  39. public bool $multiline = false,
  40. public bool $trimmable = true,
  41. public ?int $timeout = null,
  42. ?callable $normalizer = null,
  43. ?callable $validator = null,
  44. public ?int $maxAttempts = null,
  45. ) {
  46. $this->normalizer = $normalizer ? $normalizer(...) : null;
  47. $this->validator = $validator ? $validator(...) : null;
  48. }
  49. /**
  50. * @internal
  51. */
  52. public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member, string $name): ?self
  53. {
  54. $reflection = new ReflectionMember($member);
  55. if (!$self = $reflection->getAttribute(self::class)) {
  56. return null;
  57. }
  58. $type = $reflection->getType();
  59. if (!$type instanceof \ReflectionNamedType) {
  60. throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for interactive questions.', $reflection->getMemberName(), $name, $reflection->getSourceName()));
  61. }
  62. $self->closure = function (SymfonyStyle $io, InputInterface $input) use ($self, $reflection, $name, $type) {
  63. if ($reflection->isProperty() && isset($this->{$reflection->getName()})) {
  64. return;
  65. }
  66. if ($reflection->isParameter() && !\in_array($input->getArgument($name), [null, []], true)) {
  67. return;
  68. }
  69. if ('bool' === $type->getName()) {
  70. $self->default ??= false;
  71. if (!\is_bool($self->default)) {
  72. throw new LogicException(\sprintf('The "%s::$default" value for the %s "$%s" of "%s" must be a boolean.', self::class, $reflection->getMemberName(), $name, $reflection->getSourceName()));
  73. }
  74. $question = new ConfirmationQuestion($self->question, $self->default);
  75. } else {
  76. $question = new Question($self->question, $self->default);
  77. }
  78. $question->setHidden($self->hidden);
  79. $question->setMultiline($self->multiline);
  80. $question->setTrimmable($self->trimmable);
  81. $question->setTimeout($self->timeout);
  82. if (!$self->validator && $reflection->isProperty() && 'array' !== $type->getName()) {
  83. $self->validator = function (mixed $value) use ($reflection): mixed {
  84. return $this->{$reflection->getName()} = $value;
  85. };
  86. }
  87. $question->setValidator($self->validator);
  88. $question->setMaxAttempts($self->maxAttempts);
  89. if ($self->normalizer) {
  90. $question->setNormalizer($self->normalizer);
  91. } elseif (is_subclass_of($type->getName(), \BackedEnum::class)) {
  92. /** @var class-string<\BackedEnum> $backedType */
  93. $backedType = $reflection->getType()->getName();
  94. $question->setNormalizer(fn (string|int $value) => $backedType::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($reflection->getName(), $value, array_column($backedType::cases(), 'value')));
  95. }
  96. if ('array' === $type->getName()) {
  97. $value = [];
  98. while ($v = $io->askQuestion($question)) {
  99. if ("\x4" === $v || \PHP_EOL === $v || ($question->isTrimmable() && '' === $v = trim($v))) {
  100. break;
  101. }
  102. $value[] = $v;
  103. }
  104. } else {
  105. $value = $io->askQuestion($question);
  106. }
  107. if (null === $value && !$reflection->isNullable()) {
  108. return;
  109. }
  110. if ($reflection->isProperty()) {
  111. $this->{$reflection->getName()} = $value;
  112. } else {
  113. $input->setArgument($name, $value);
  114. }
  115. };
  116. return $self;
  117. }
  118. /**
  119. * @internal
  120. */
  121. public function getFunction(object $instance): \ReflectionFunction
  122. {
  123. return new \ReflectionFunction($this->closure->bindTo($instance, $instance::class));
  124. }
  125. }