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     * @return $this
62     */
63    public function setMultiselect(bool $multiselect)
64    {
65        $this->multiselect = $multiselect;
66        $this->setValidator($this->getDefaultValidator());
67
68        return $this;
69    }
70
71    /**
72     * Returns whether the choices are multiselect.
73     *
74     * @return bool
75     */
76    public function isMultiselect()
77    {
78        return $this->multiselect;
79    }
80
81    /**
82     * Gets the prompt for choices.
83     *
84     * @return string
85     */
86    public function getPrompt()
87    {
88        return $this->prompt;
89    }
90
91    /**
92     * Sets the prompt for choices.
93     *
94     * @return $this
95     */
96    public function setPrompt(string $prompt)
97    {
98        $this->prompt = $prompt;
99
100        return $this;
101    }
102
103    /**
104     * Sets the error message for invalid values.
105     *
106     * The error message has a string placeholder (%s) for the invalid value.
107     *
108     * @return $this
109     */
110    public function setErrorMessage(string $errorMessage)
111    {
112        $this->errorMessage = $errorMessage;
113        $this->setValidator($this->getDefaultValidator());
114
115        return $this;
116    }
117
118    private function getDefaultValidator(): callable
119    {
120        $choices = $this->choices;
121        $errorMessage = $this->errorMessage;
122        $multiselect = $this->multiselect;
123        $isAssoc = $this->isAssoc($choices);
124
125        return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) {
126            if ($multiselect) {
127                // Check for a separated comma values
128                if (!preg_match('/^[^,]+(?:,[^,]+)*$/', $selected, $matches)) {
129                    throw new InvalidArgumentException(sprintf($errorMessage, $selected));
130                }
131
132                $selectedChoices = explode(',', $selected);
133            } else {
134                $selectedChoices = [$selected];
135            }
136
137            if ($this->isTrimmable()) {
138                foreach ($selectedChoices as $k => $v) {
139                    $selectedChoices[$k] = trim($v);
140                }
141            }
142
143            $multiselectChoices = [];
144            foreach ($selectedChoices as $value) {
145                $results = [];
146                foreach ($choices as $key => $choice) {
147                    if ($choice === $value) {
148                        $results[] = $key;
149                    }
150                }
151
152                if (\count($results) > 1) {
153                    throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results)));
154                }
155
156                $result = array_search($value, $choices);
157
158                if (!$isAssoc) {
159                    if (false !== $result) {
160                        $result = $choices[$result];
161                    } elseif (isset($choices[$value])) {
162                        $result = $choices[$value];
163                    }
164                } elseif (false === $result && isset($choices[$value])) {
165                    $result = $value;
166                }
167
168                if (false === $result) {
169                    throw new InvalidArgumentException(sprintf($errorMessage, $value));
170                }
171
172                // For associative choices, consistently return the key as string:
173                $multiselectChoices[] = $isAssoc ? (string) $result : $result;
174            }
175
176            if ($multiselect) {
177                return $multiselectChoices;
178            }
179
180            return current($multiselectChoices);
181        };
182    }
183}
184