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
12namespace Symfony\Component\Console\Question;
13
14use Symfony\Component\Console\Exception\InvalidArgumentException;
15use Symfony\Component\Console\Exception\LogicException;
16
17/**
18 * Represents a Question.
19 *
20 * @author Fabien Potencier <fabien@symfony.com>
21 */
22class Question
23{
24    private $question;
25    private $attempts;
26    private $hidden = false;
27    private $hiddenFallback = true;
28    private $autocompleterCallback;
29    private $validator;
30    private $default;
31    private $normalizer;
32    private $trimmable = true;
33
34    /**
35     * @param string $question The question to ask to the user
36     * @param mixed  $default  The default answer to return if the user enters nothing
37     */
38    public function __construct(string $question, $default = null)
39    {
40        $this->question = $question;
41        $this->default = $default;
42    }
43
44    /**
45     * Returns the question.
46     *
47     * @return string
48     */
49    public function getQuestion()
50    {
51        return $this->question;
52    }
53
54    /**
55     * Returns the default answer.
56     *
57     * @return mixed
58     */
59    public function getDefault()
60    {
61        return $this->default;
62    }
63
64    /**
65     * Returns whether the user response must be hidden.
66     *
67     * @return bool
68     */
69    public function isHidden()
70    {
71        return $this->hidden;
72    }
73
74    /**
75     * Sets whether the user response must be hidden or not.
76     *
77     * @param bool $hidden
78     *
79     * @return $this
80     *
81     * @throws LogicException In case the autocompleter is also used
82     */
83    public function setHidden($hidden)
84    {
85        if ($this->autocompleterCallback) {
86            throw new LogicException('A hidden question cannot use the autocompleter.');
87        }
88
89        $this->hidden = (bool) $hidden;
90
91        return $this;
92    }
93
94    /**
95     * In case the response can not be hidden, whether to fallback on non-hidden question or not.
96     *
97     * @return bool
98     */
99    public function isHiddenFallback()
100    {
101        return $this->hiddenFallback;
102    }
103
104    /**
105     * Sets whether to fallback on non-hidden question if the response can not be hidden.
106     *
107     * @param bool $fallback
108     *
109     * @return $this
110     */
111    public function setHiddenFallback($fallback)
112    {
113        $this->hiddenFallback = (bool) $fallback;
114
115        return $this;
116    }
117
118    /**
119     * Gets values for the autocompleter.
120     *
121     * @return iterable|null
122     */
123    public function getAutocompleterValues()
124    {
125        $callback = $this->getAutocompleterCallback();
126
127        return $callback ? $callback('') : null;
128    }
129
130    /**
131     * Sets values for the autocompleter.
132     *
133     * @param iterable|null $values
134     *
135     * @return $this
136     *
137     * @throws InvalidArgumentException
138     * @throws LogicException
139     */
140    public function setAutocompleterValues($values)
141    {
142        if (\is_array($values)) {
143            $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values);
144
145            $callback = static function () use ($values) {
146                return $values;
147            };
148        } elseif ($values instanceof \Traversable) {
149            $valueCache = null;
150            $callback = static function () use ($values, &$valueCache) {
151                return $valueCache ?? $valueCache = iterator_to_array($values, false);
152            };
153        } elseif (null === $values) {
154            $callback = null;
155        } else {
156            throw new InvalidArgumentException('Autocompleter values can be either an array, "null" or a "Traversable" object.');
157        }
158
159        return $this->setAutocompleterCallback($callback);
160    }
161
162    /**
163     * Gets the callback function used for the autocompleter.
164     */
165    public function getAutocompleterCallback(): ?callable
166    {
167        return $this->autocompleterCallback;
168    }
169
170    /**
171     * Sets the callback function used for the autocompleter.
172     *
173     * The callback is passed the user input as argument and should return an iterable of corresponding suggestions.
174     *
175     * @return $this
176     */
177    public function setAutocompleterCallback(callable $callback = null): self
178    {
179        if ($this->hidden && null !== $callback) {
180            throw new LogicException('A hidden question cannot use the autocompleter.');
181        }
182
183        $this->autocompleterCallback = $callback;
184
185        return $this;
186    }
187
188    /**
189     * Sets a validator for the question.
190     *
191     * @return $this
192     */
193    public function setValidator(callable $validator = null)
194    {
195        $this->validator = $validator;
196
197        return $this;
198    }
199
200    /**
201     * Gets the validator for the question.
202     *
203     * @return callable|null
204     */
205    public function getValidator()
206    {
207        return $this->validator;
208    }
209
210    /**
211     * Sets the maximum number of attempts.
212     *
213     * Null means an unlimited number of attempts.
214     *
215     * @param int|null $attempts
216     *
217     * @return $this
218     *
219     * @throws InvalidArgumentException in case the number of attempts is invalid
220     */
221    public function setMaxAttempts($attempts)
222    {
223        if (null !== $attempts && $attempts < 1) {
224            throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
225        }
226
227        $this->attempts = $attempts;
228
229        return $this;
230    }
231
232    /**
233     * Gets the maximum number of attempts.
234     *
235     * Null means an unlimited number of attempts.
236     *
237     * @return int|null
238     */
239    public function getMaxAttempts()
240    {
241        return $this->attempts;
242    }
243
244    /**
245     * Sets a normalizer for the response.
246     *
247     * The normalizer can be a callable (a string), a closure or a class implementing __invoke.
248     *
249     * @return $this
250     */
251    public function setNormalizer(callable $normalizer)
252    {
253        $this->normalizer = $normalizer;
254
255        return $this;
256    }
257
258    /**
259     * Gets the normalizer for the response.
260     *
261     * The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
262     *
263     * @return callable|null
264     */
265    public function getNormalizer()
266    {
267        return $this->normalizer;
268    }
269
270    protected function isAssoc($array)
271    {
272        return (bool) \count(array_filter(array_keys($array), 'is_string'));
273    }
274
275    public function isTrimmable(): bool
276    {
277        return $this->trimmable;
278    }
279
280    /**
281     * @return $this
282     */
283    public function setTrimmable(bool $trimmable): self
284    {
285        $this->trimmable = $trimmable;
286
287        return $this;
288    }
289}
290