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;
15
16/**
17 * Represents a choice question.
18 *
19 * @author Fabien Potencier <fabien@symfony.com>
20 */
21class ChoiceQuestion extends Question
22{
23    private $choices;
24    private $multiselect = false;
25    private $prompt = ' > ';
26    private $errorMessage = 'Value "%s" is invalid';
27
28    /**
29     * @param string $question The question to ask to the user
30     * @param array  $choices  The list of available choices
31     * @param mixed  $default  The default answer to return
32     */
33    public function __construct(string $question, array $choices, $default = null)
34    {
35        if (!$choices) {
36            throw new \LogicException('Choice question must have at least 1 choice available.');
37        }
38
39        parent::__construct($question, $default);
40
41        $this->choices = $choices;
42        $this->setValidator($this->getDefaultValidator());
43        $this->setAutocompleterValues($choices);
44    }
45
46    /**
47     * Returns available choices.
48     *
49     * @return array
50     */
51    public function getChoices()
52    {
53        return $this->choices;
54    }
55
56    /**
57     * Sets multiselect option.
58     *
59     * When multiselect is set to true, multiple choices can be answered.
60     *
61     * @param bool $multiselect
62     *
63     * @return $this
64     */
65    public function setMultiselect($multiselect)
66    {
67        $this->multiselect = $multiselect;
68        $this->setValidator($this->getDefaultValidator());
69
70        return $this;
71    }
72
73    /**
74     * Returns whether the choices are multiselect.
75     *
76     * @return bool
77     */
78    public function isMultiselect()
79    {
80        return $this->multiselect;
81    }
82
83    /**
84     * Gets the prompt for choices.
85     *
86     * @return string
87     */
88    public function getPrompt()
89    {
90        return $this->prompt;
91    }
92
93    /**
94     * Sets the prompt for choices.
95     *
96     * @param string $prompt
97     *
98     * @return $this
99     */
100    public function setPrompt($prompt)
101    {
102        $this->prompt = $prompt;
103
104        return $this;
105    }
106
107    /**
108     * Sets the error message for invalid values.
109     *
110     * The error message has a string placeholder (%s) for the invalid value.
111     *
112     * @param string $errorMessage
113     *
114     * @return $this
115     */
116    public function setErrorMessage($errorMessage)
117    {
118        $this->errorMessage = $errorMessage;
119        $this->setValidator($this->getDefaultValidator());
120
121        return $this;
122    }
123
124    private function getDefaultValidator(): callable
125    {
126        $choices = $this->choices;
127        $errorMessage = $this->errorMessage;
128        $multiselect = $this->multiselect;
129        $isAssoc = $this->isAssoc($choices);
130
131        return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) {
132            if ($multiselect) {
133                // Check for a separated comma values
134                if (!preg_match('/^[^,]+(?:,[^,]+)*$/', $selected, $matches)) {
135                    throw new InvalidArgumentException(sprintf($errorMessage, $selected));
136                }
137
138                $selectedChoices = explode(',', $selected);
139            } else {
140                $selectedChoices = [$selected];
141            }
142
143            if ($this->isTrimmable()) {
144                foreach ($selectedChoices as $k => $v) {
145                    $selectedChoices[$k] = trim($v);
146                }
147            }
148
149            $multiselectChoices = [];
150            foreach ($selectedChoices as $value) {
151                $results = [];
152                foreach ($choices as $key => $choice) {
153                    if ($choice === $value) {
154                        $results[] = $key;
155                    }
156                }
157
158                if (\count($results) > 1) {
159                    throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results)));
160                }
161
162                $result = array_search($value, $choices);
163
164                if (!$isAssoc) {
165                    if (false !== $result) {
166                        $result = $choices[$result];
167                    } elseif (isset($choices[$value])) {
168                        $result = $choices[$value];
169                    }
170                } elseif (false === $result && isset($choices[$value])) {
171                    $result = $value;
172                }
173
174                if (false === $result) {
175                    throw new InvalidArgumentException(sprintf($errorMessage, $value));
176                }
177
178                $multiselectChoices[] = (string) $result;
179            }
180
181            if ($multiselect) {
182                return $multiselectChoices;
183            }
184
185            return current($multiselectChoices);
186        };
187    }
188}
189