239 lines
6.7 KiB
PHP
239 lines
6.7 KiB
PHP
|
<?php
|
||
|
|
||
|
/*
|
||
|
* This file is part of the Symfony package.
|
||
|
*
|
||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||
|
*
|
||
|
* 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 <bschussek@gmail.com>
|
||
|
*/
|
||
|
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;
|
||
|
}
|
||
|
}
|