1313
1414use Symfony \Component \Console \Attribute \Reflection \ReflectionMember ;
1515use Symfony \Component \Console \Exception \InvalidArgumentException ;
16+ use Symfony \Component \Console \Exception \LogicException ;
1617use Symfony \Component \Console \Input \InputInterface ;
18+ use Symfony \Component \Console \Question \ConfirmationQuestion ;
1719use Symfony \Component \Console \Question \Question ;
1820use Symfony \Component \Console \Style \SymfonyStyle ;
1921
2022#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY )]
2123class Ask implements InteractiveAttributeInterface
2224{
25+ public ?\Closure $ normalizer ;
2326 public ?\Closure $ validator ;
2427 private \Closure $ closure ;
2528
@@ -41,9 +44,11 @@ public function __construct(
4144 public bool $ multiline = false ,
4245 public bool $ trimmable = true ,
4346 public ?int $ timeout = null ,
47+ ?callable $ normalizer = null ,
4448 ?callable $ validator = null ,
4549 public ?int $ maxAttempts = null ,
4650 ) {
51+ $ this ->normalizer = $ normalizer ? $ normalizer (...) : null ;
4752 $ this ->validator = $ validator ? $ validator (...) : null ;
4853 }
4954
@@ -58,18 +63,38 @@ public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member,
5863 return null ;
5964 }
6065
61- $ self ->closure = function (SymfonyStyle $ io , InputInterface $ input ) use ($ self , $ reflection , $ name ) {
62- if (($ reflection ->isProperty () && isset ($ this ->{$ reflection ->getName ()})) || ($ reflection ->isParameter () && null !== $ input ->getArgument ($ name ))) {
66+ $ type = $ reflection ->getType ();
67+
68+ if (!$ type instanceof \ReflectionNamedType) {
69+ 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 ()));
70+ }
71+
72+ $ self ->closure = function (SymfonyStyle $ io , InputInterface $ input ) use ($ self , $ reflection , $ name , $ type ) {
73+ if ($ reflection ->isProperty () && isset ($ this ->{$ reflection ->getName ()})) {
74+ return ;
75+ }
76+
77+ if ($ reflection ->isParameter () && !\in_array ($ input ->getArgument ($ name ), [null , []], true )) {
6378 return ;
6479 }
6580
66- $ question = new Question ($ self ->question , $ self ->default );
81+ if ('bool ' === $ type ->getName ()) {
82+ $ self ->default ??= false ;
83+
84+ if (!\is_bool ($ self ->default )) {
85+ 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 ()));
86+ }
87+
88+ $ question = new ConfirmationQuestion ($ self ->question , $ self ->default );
89+ } else {
90+ $ question = new Question ($ self ->question , $ self ->default );
91+ }
6792 $ question ->setHidden ($ self ->hidden );
6893 $ question ->setMultiline ($ self ->multiline );
6994 $ question ->setTrimmable ($ self ->trimmable );
7095 $ question ->setTimeout ($ self ->timeout );
7196
72- if (!$ self ->validator && $ reflection ->isProperty ()) {
97+ if (!$ self ->validator && $ reflection ->isProperty () && ' array ' !== $ type -> getName () ) {
7398 $ self ->validator = function (mixed $ value ) use ($ reflection ): mixed {
7499 return $ this ->{$ reflection ->getName ()} = $ value ;
75100 };
@@ -78,13 +103,25 @@ public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member,
78103 $ question ->setValidator ($ self ->validator );
79104 $ question ->setMaxAttempts ($ self ->maxAttempts );
80105
81- if ($ reflection ->isBackedEnumType ()) {
106+ if ($ self ->normalizer ) {
107+ $ question ->setNormalizer ($ self ->normalizer );
108+ } elseif (is_subclass_of ($ type ->getName (), \BackedEnum::class)) {
82109 /** @var class-string<\BackedEnum> $backedType */
83110 $ backedType = $ reflection ->getType ()->getName ();
84- $ question ->setNormalizer (fn (string |int $ value ) => $ backedType ::tryFrom ($ value ) ?? throw InvalidArgumentException::fromEnumValue ($ reflection ->getName (), $ value , array_map ( fn ( \ BackedEnum $ enum ): string | int => $ enum -> value , $ backedType ::cases ())));
111+ $ question ->setNormalizer (fn (string |int $ value ) => $ backedType ::tryFrom ($ value ) ?? throw InvalidArgumentException::fromEnumValue ($ reflection ->getName (), $ value , array_column ( $ backedType ::cases (), ' value ' )));
85112 }
86113
87- $ value = $ io ->askQuestion ($ question );
114+ if ('array ' === $ type ->getName ()) {
115+ $ value = [];
116+ while ($ v = $ io ->askQuestion ($ question )) {
117+ if ("\x4" === $ v || \PHP_EOL === $ v || ($ question ->isTrimmable () && '' === $ v = trim ($ v ))) {
118+ break ;
119+ }
120+ $ value [] = $ v ;
121+ }
122+ } else {
123+ $ value = $ io ->askQuestion ($ question );
124+ }
88125
89126 if (null === $ value && !$ reflection ->isNullable ()) {
90127 return ;
0 commit comments