* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Form\ChoiceList; /** * A list of choices with arbitrary data types. * * The user of this class is responsible for assigning string values to the * choices annd for their uniqueness. * Both the choices and their values are passed to the constructor. * Each choice must have a corresponding value (with the same key) in * the values array. * * @author Bernhard Schussek */ class ArrayChoiceList implements ChoiceListInterface { /** * The choices in the list. * * @var array */ protected $choices; /** * The values indexed by the original keys. * * @var array */ protected $structuredValues; /** * The original keys of the choices array. * * @var int[]|string[] */ protected $originalKeys; protected $valueCallback; /** * Creates a list with the given choices and values. * * The given choice array must have the same array keys as the value array. * * @param iterable $choices The selectable choices * @param callable|null $value The callable for creating the value * for a choice. If `null` is passed, * incrementing integers are used as * values */ public function __construct(iterable $choices, callable $value = null) { if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } if (null === $value && $this->castableToString($choices)) { $value = function ($choice) { return false === $choice ? '0' : (string) $choice; }; } if (null !== $value) { // If a deterministic value generator was passed, use it later $this->valueCallback = $value; } else { // Otherwise generate incrementing integers as values $i = 0; $value = function () use (&$i) { return $i++; }; } // If the choices are given as recursive array (i.e. with explicit // choice groups), flatten the array. The grouping information is needed // in the view only. $this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues); $this->choices = $choicesByValues; $this->originalKeys = $keysByValues; $this->structuredValues = $structuredValues; } /** * {@inheritdoc} */ public function getChoices() { return $this->choices; } /** * {@inheritdoc} */ public function getValues() { return array_map('strval', array_keys($this->choices)); } /** * {@inheritdoc} */ public function getStructuredValues() { return $this->structuredValues; } /** * {@inheritdoc} */ public function getOriginalKeys() { return $this->originalKeys; } /** * {@inheritdoc} */ public function getChoicesForValues(array $values) { $choices = []; foreach ($values as $i => $givenValue) { if (\array_key_exists($givenValue, $this->choices)) { $choices[$i] = $this->choices[$givenValue]; } } return $choices; } /** * {@inheritdoc} */ public function getValuesForChoices(array $choices) { $values = []; // Use the value callback to compare choices by their values, if present if ($this->valueCallback) { $givenValues = []; foreach ($choices as $i => $givenChoice) { $givenValues[$i] = (string) ($this->valueCallback)($givenChoice); } return array_intersect($givenValues, array_keys($this->choices)); } // Otherwise compare choices by identity foreach ($choices as $i => $givenChoice) { foreach ($this->choices as $value => $choice) { if ($choice === $givenChoice) { $values[$i] = (string) $value; break; } } } return $values; } /** * Flattens an array into the given output variables. * * @param array $choices The array to flatten * @param callable $value The callable for generating choice values * @param array|null $choicesByValues The flattened choices indexed by the * corresponding values * @param array|null $keysByValues The original keys indexed by the * corresponding values * @param array|null $structuredValues The values indexed by the original keys * * @internal */ protected function flatten(array $choices, callable $value, ?array &$choicesByValues, ?array &$keysByValues, ?array &$structuredValues) { if (null === $choicesByValues) { $choicesByValues = []; $keysByValues = []; $structuredValues = []; } foreach ($choices as $key => $choice) { if (\is_array($choice)) { $this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]); continue; } $choiceValue = (string) $value($choice); $choicesByValues[$choiceValue] = $choice; $keysByValues[$choiceValue] = $key; $structuredValues[$key] = $choiceValue; } } /** * Checks whether the given choices can be cast to strings without * generating duplicates. * This method is responsible for preventing conflict between scalar values * and the empty value. */ private function castableToString(array $choices, array &$cache = []): bool { foreach ($choices as $choice) { if (\is_array($choice)) { if (!$this->castableToString($choice, $cache)) { return false; } continue; } elseif (!is_scalar($choice)) { return false; } // prevent having false casted to the empty string by isset() $choice = false === $choice ? '0' : (string) $choice; if (isset($cache[$choice])) { return false; } $cache[$choice] = true; } return true; } }