]> git.immae.eu Git - github/wallabag/wallabag.git/blob - vendor/symfony/form/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php
twig implementation
[github/wallabag/wallabag.git] / vendor / symfony / form / Symfony / Component / Form / Extension / Core / ChoiceList / ChoiceList.php
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
12 namespace Symfony\Component\Form\Extension\Core\ChoiceList;
13
14 use Symfony\Component\Form\FormConfigBuilder;
15 use Symfony\Component\Form\Exception\UnexpectedTypeException;
16 use Symfony\Component\Form\Exception\InvalidConfigurationException;
17 use Symfony\Component\Form\Exception\InvalidArgumentException;
18 use Symfony\Component\Form\Extension\Core\View\ChoiceView;
19
20 /**
21 * A choice list for choices of arbitrary data types.
22 *
23 * Choices and labels are passed in two arrays. The indices of the choices
24 * and the labels should match. Choices may also be given as hierarchy of
25 * unlimited depth by creating nested arrays. The title of the sub-hierarchy
26 * can be stored in the array key pointing to the nested array. The topmost
27 * level of the hierarchy may also be a \Traversable.
28 *
29 * <code>
30 * $choices = array(true, false);
31 * $labels = array('Agree', 'Disagree');
32 * $choiceList = new ChoiceList($choices, $labels);
33 * </code>
34 *
35 * @author Bernhard Schussek <bschussek@gmail.com>
36 */
37 class ChoiceList implements ChoiceListInterface
38 {
39 /**
40 * The choices with their indices as keys.
41 *
42 * @var array
43 */
44 private $choices = array();
45
46 /**
47 * The choice values with the indices of the matching choices as keys.
48 *
49 * @var array
50 */
51 private $values = array();
52
53 /**
54 * The preferred view objects as hierarchy containing also the choice groups
55 * with the indices of the matching choices as bottom-level keys.
56 *
57 * @var array
58 */
59 private $preferredViews = array();
60
61 /**
62 * The non-preferred view objects as hierarchy containing also the choice
63 * groups with the indices of the matching choices as bottom-level keys.
64 *
65 * @var array
66 */
67 private $remainingViews = array();
68
69 /**
70 * Creates a new choice list.
71 *
72 * @param array|\Traversable $choices The array of choices. Choices may also be given
73 * as hierarchy of unlimited depth. Hierarchies are
74 * created by creating nested arrays. The title of
75 * the sub-hierarchy can be stored in the array
76 * key pointing to the nested array. The topmost
77 * level of the hierarchy may also be a \Traversable.
78 * @param array $labels The array of labels. The structure of this array
79 * should match the structure of $choices.
80 * @param array $preferredChoices A flat array of choices that should be
81 * presented to the user with priority.
82 *
83 * @throws UnexpectedTypeException If the choices are not an array or \Traversable.
84 */
85 public function __construct($choices, array $labels, array $preferredChoices = array())
86 {
87 if (!is_array($choices) && !$choices instanceof \Traversable) {
88 throw new UnexpectedTypeException($choices, 'array or \Traversable');
89 }
90
91 $this->initialize($choices, $labels, $preferredChoices);
92 }
93
94 /**
95 * Initializes the list with choices.
96 *
97 * Safe to be called multiple times. The list is cleared on every call.
98 *
99 * @param array|\Traversable $choices The choices to write into the list.
100 * @param array $labels The labels belonging to the choices.
101 * @param array $preferredChoices The choices to display with priority.
102 */
103 protected function initialize($choices, array $labels, array $preferredChoices)
104 {
105 $this->choices = array();
106 $this->values = array();
107 $this->preferredViews = array();
108 $this->remainingViews = array();
109
110 $this->addChoices(
111 $this->preferredViews,
112 $this->remainingViews,
113 $choices,
114 $labels,
115 $preferredChoices
116 );
117 }
118
119 /**
120 * {@inheritdoc}
121 */
122 public function getChoices()
123 {
124 return $this->choices;
125 }
126
127 /**
128 * {@inheritdoc}
129 */
130 public function getValues()
131 {
132 return $this->values;
133 }
134
135 /**
136 * {@inheritdoc}
137 */
138 public function getPreferredViews()
139 {
140 return $this->preferredViews;
141 }
142
143 /**
144 * {@inheritdoc}
145 */
146 public function getRemainingViews()
147 {
148 return $this->remainingViews;
149 }
150
151 /**
152 * {@inheritdoc}
153 */
154 public function getChoicesForValues(array $values)
155 {
156 $values = $this->fixValues($values);
157 $choices = array();
158
159 foreach ($values as $j => $givenValue) {
160 foreach ($this->values as $i => $value) {
161 if ($value === $givenValue) {
162 $choices[] = $this->choices[$i];
163 unset($values[$j]);
164
165 if (0 === count($values)) {
166 break 2;
167 }
168 }
169 }
170 }
171
172 return $choices;
173 }
174
175 /**
176 * {@inheritdoc}
177 */
178 public function getValuesForChoices(array $choices)
179 {
180 $choices = $this->fixChoices($choices);
181 $values = array();
182
183 foreach ($this->choices as $i => $choice) {
184 foreach ($choices as $j => $givenChoice) {
185 if ($choice === $givenChoice) {
186 $values[] = $this->values[$i];
187 unset($choices[$j]);
188
189 if (0 === count($choices)) {
190 break 2;
191 }
192 }
193 }
194 }
195
196 return $values;
197 }
198
199 /**
200 * {@inheritdoc}
201 */
202 public function getIndicesForChoices(array $choices)
203 {
204 $choices = $this->fixChoices($choices);
205 $indices = array();
206
207 foreach ($this->choices as $i => $choice) {
208 foreach ($choices as $j => $givenChoice) {
209 if ($choice === $givenChoice) {
210 $indices[] = $i;
211 unset($choices[$j]);
212
213 if (0 === count($choices)) {
214 break 2;
215 }
216 }
217 }
218 }
219
220 return $indices;
221 }
222
223 /**
224 * {@inheritdoc}
225 */
226 public function getIndicesForValues(array $values)
227 {
228 $values = $this->fixValues($values);
229 $indices = array();
230
231 foreach ($this->values as $i => $value) {
232 foreach ($values as $j => $givenValue) {
233 if ($value === $givenValue) {
234 $indices[] = $i;
235 unset($values[$j]);
236
237 if (0 === count($values)) {
238 break 2;
239 }
240 }
241 }
242 }
243
244 return $indices;
245 }
246
247 /**
248 * Recursively adds the given choices to the list.
249 *
250 * @param array $bucketForPreferred The bucket where to store the preferred
251 * view objects.
252 * @param array $bucketForRemaining The bucket where to store the
253 * non-preferred view objects.
254 * @param array|\Traversable $choices The list of choices.
255 * @param array $labels The labels corresponding to the choices.
256 * @param array $preferredChoices The preferred choices.
257 *
258 * @throws InvalidArgumentException If the structures of the choices and labels array do not match.
259 * @throws InvalidConfigurationException If no valid value or index could be created for a choice.
260 */
261 protected function addChoices(array &$bucketForPreferred, array &$bucketForRemaining, $choices, array $labels, array $preferredChoices)
262 {
263 // Add choices to the nested buckets
264 foreach ($choices as $group => $choice) {
265 if (!array_key_exists($group, $labels)) {
266 throw new InvalidArgumentException('The structures of the choices and labels array do not match.');
267 }
268
269 if (is_array($choice)) {
270 // Don't do the work if the array is empty
271 if (count($choice) > 0) {
272 $this->addChoiceGroup(
273 $group,
274 $bucketForPreferred,
275 $bucketForRemaining,
276 $choice,
277 $labels[$group],
278 $preferredChoices
279 );
280 }
281 } else {
282 $this->addChoice(
283 $bucketForPreferred,
284 $bucketForRemaining,
285 $choice,
286 $labels[$group],
287 $preferredChoices
288 );
289 }
290 }
291 }
292
293 /**
294 * Recursively adds a choice group.
295 *
296 * @param string $group The name of the group.
297 * @param array $bucketForPreferred The bucket where to store the preferred
298 * view objects.
299 * @param array $bucketForRemaining The bucket where to store the
300 * non-preferred view objects.
301 * @param array $choices The list of choices in the group.
302 * @param array $labels The labels corresponding to the choices in the group.
303 * @param array $preferredChoices The preferred choices.
304 *
305 * @throws InvalidConfigurationException If no valid value or index could be created for a choice.
306 */
307 protected function addChoiceGroup($group, array &$bucketForPreferred, array &$bucketForRemaining, array $choices, array $labels, array $preferredChoices)
308 {
309 // If this is a choice group, create a new level in the choice
310 // key hierarchy
311 $bucketForPreferred[$group] = array();
312 $bucketForRemaining[$group] = array();
313
314 $this->addChoices(
315 $bucketForPreferred[$group],
316 $bucketForRemaining[$group],
317 $choices,
318 $labels,
319 $preferredChoices
320 );
321
322 // Remove child levels if empty
323 if (empty($bucketForPreferred[$group])) {
324 unset($bucketForPreferred[$group]);
325 }
326 if (empty($bucketForRemaining[$group])) {
327 unset($bucketForRemaining[$group]);
328 }
329 }
330
331 /**
332 * Adds a new choice.
333 *
334 * @param array $bucketForPreferred The bucket where to store the preferred
335 * view objects.
336 * @param array $bucketForRemaining The bucket where to store the
337 * non-preferred view objects.
338 * @param mixed $choice The choice to add.
339 * @param string $label The label for the choice.
340 * @param array $preferredChoices The preferred choices.
341 *
342 * @throws InvalidConfigurationException If no valid value or index could be created.
343 */
344 protected function addChoice(array &$bucketForPreferred, array &$bucketForRemaining, $choice, $label, array $preferredChoices)
345 {
346 $index = $this->createIndex($choice);
347
348 if ('' === $index || null === $index || !FormConfigBuilder::isValidName((string) $index)) {
349 throw new InvalidConfigurationException(sprintf('The index "%s" created by the choice list is invalid. It should be a valid, non-empty Form name.', $index));
350 }
351
352 $value = $this->createValue($choice);
353
354 if (!is_string($value)) {
355 throw new InvalidConfigurationException(sprintf('The value created by the choice list is of type "%s", but should be a string.', gettype($value)));
356 }
357
358 $view = new ChoiceView($choice, $value, $label);
359
360 $this->choices[$index] = $this->fixChoice($choice);
361 $this->values[$index] = $value;
362
363 if ($this->isPreferred($choice, $preferredChoices)) {
364 $bucketForPreferred[$index] = $view;
365 } else {
366 $bucketForRemaining[$index] = $view;
367 }
368 }
369
370 /**
371 * Returns whether the given choice should be preferred judging by the
372 * given array of preferred choices.
373 *
374 * Extension point to optimize performance by changing the structure of the
375 * $preferredChoices array.
376 *
377 * @param mixed $choice The choice to test.
378 * @param array $preferredChoices An array of preferred choices.
379 *
380 * @return Boolean Whether the choice is preferred.
381 */
382 protected function isPreferred($choice, array $preferredChoices)
383 {
384 return false !== array_search($choice, $preferredChoices, true);
385 }
386
387 /**
388 * Creates a new unique index for this choice.
389 *
390 * Extension point to change the indexing strategy.
391 *
392 * @param mixed $choice The choice to create an index for
393 *
394 * @return integer|string A unique index containing only ASCII letters,
395 * digits and underscores.
396 */
397 protected function createIndex($choice)
398 {
399 return count($this->choices);
400 }
401
402 /**
403 * Creates a new unique value for this choice.
404 *
405 * By default, an integer is generated since it cannot be guaranteed that
406 * all values in the list are convertible to (unique) strings. Subclasses
407 * can override this behaviour if they can guarantee this property.
408 *
409 * @param mixed $choice The choice to create a value for
410 *
411 * @return string A unique string.
412 */
413 protected function createValue($choice)
414 {
415 return (string) count($this->values);
416 }
417
418 /**
419 * Fixes the data type of the given choice value to avoid comparison
420 * problems.
421 *
422 * @param mixed $value The choice value.
423 *
424 * @return string The value as string.
425 */
426 protected function fixValue($value)
427 {
428 return (string) $value;
429 }
430
431 /**
432 * Fixes the data types of the given choice values to avoid comparison
433 * problems.
434 *
435 * @param array $values The choice values.
436 *
437 * @return array The values as strings.
438 */
439 protected function fixValues(array $values)
440 {
441 foreach ($values as $i => $value) {
442 $values[$i] = $this->fixValue($value);
443 }
444
445 return $values;
446 }
447
448 /**
449 * Fixes the data type of the given choice index to avoid comparison
450 * problems.
451 *
452 * @param mixed $index The choice index.
453 *
454 * @return integer|string The index as PHP array key.
455 */
456 protected function fixIndex($index)
457 {
458 if (is_bool($index) || (string) (int) $index === (string) $index) {
459 return (int) $index;
460 }
461
462 return (string) $index;
463 }
464
465 /**
466 * Fixes the data types of the given choice indices to avoid comparison
467 * problems.
468 *
469 * @param array $indices The choice indices.
470 *
471 * @return array The indices as strings.
472 */
473 protected function fixIndices(array $indices)
474 {
475 foreach ($indices as $i => $index) {
476 $indices[$i] = $this->fixIndex($index);
477 }
478
479 return $indices;
480 }
481
482 /**
483 * Fixes the data type of the given choice to avoid comparison problems.
484 *
485 * Extension point. In this implementation, choices are guaranteed to
486 * always maintain their type and thus can be typesafely compared.
487 *
488 * @param mixed $choice The choice.
489 *
490 * @return mixed The fixed choice.
491 */
492 protected function fixChoice($choice)
493 {
494 return $choice;
495 }
496
497 /**
498 * Fixes the data type of the given choices to avoid comparison problems.
499 *
500 * @param array $choices The choices.
501 *
502 * @return array The fixed choices.
503 *
504 * @see fixChoice
505 */
506 protected function fixChoices(array $choices)
507 {
508 return $choices;
509 }
510 }