ArrayChoiceList.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Form\ChoiceList;
  11. /**
  12. * A list of choices with arbitrary data types.
  13. *
  14. * The user of this class is responsible for assigning string values to the
  15. * choices annd for their uniqueness.
  16. * Both the choices and their values are passed to the constructor.
  17. * Each choice must have a corresponding value (with the same key) in
  18. * the values array.
  19. *
  20. * @author Bernhard Schussek <bschussek@gmail.com>
  21. */
  22. class ArrayChoiceList implements ChoiceListInterface
  23. {
  24. /**
  25. * The choices in the list.
  26. *
  27. * @var array
  28. */
  29. protected $choices;
  30. /**
  31. * The values indexed by the original keys.
  32. *
  33. * @var array
  34. */
  35. protected $structuredValues;
  36. /**
  37. * The original keys of the choices array.
  38. *
  39. * @var int[]|string[]
  40. */
  41. protected $originalKeys;
  42. protected $valueCallback;
  43. /**
  44. * Creates a list with the given choices and values.
  45. *
  46. * The given choice array must have the same array keys as the value array.
  47. *
  48. * @param iterable $choices The selectable choices
  49. * @param callable|null $value The callable for creating the value
  50. * for a choice. If `null` is passed,
  51. * incrementing integers are used as
  52. * values
  53. */
  54. public function __construct(iterable $choices, callable $value = null)
  55. {
  56. if ($choices instanceof \Traversable) {
  57. $choices = iterator_to_array($choices);
  58. }
  59. if (null === $value && $this->castableToString($choices)) {
  60. $value = function ($choice) {
  61. return false === $choice ? '0' : (string) $choice;
  62. };
  63. }
  64. if (null !== $value) {
  65. // If a deterministic value generator was passed, use it later
  66. $this->valueCallback = $value;
  67. } else {
  68. // Otherwise generate incrementing integers as values
  69. $i = 0;
  70. $value = function () use (&$i) {
  71. return $i++;
  72. };
  73. }
  74. // If the choices are given as recursive array (i.e. with explicit
  75. // choice groups), flatten the array. The grouping information is needed
  76. // in the view only.
  77. $this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues);
  78. $this->choices = $choicesByValues;
  79. $this->originalKeys = $keysByValues;
  80. $this->structuredValues = $structuredValues;
  81. }
  82. /**
  83. * {@inheritdoc}
  84. */
  85. public function getChoices()
  86. {
  87. return $this->choices;
  88. }
  89. /**
  90. * {@inheritdoc}
  91. */
  92. public function getValues()
  93. {
  94. return array_map('strval', array_keys($this->choices));
  95. }
  96. /**
  97. * {@inheritdoc}
  98. */
  99. public function getStructuredValues()
  100. {
  101. return $this->structuredValues;
  102. }
  103. /**
  104. * {@inheritdoc}
  105. */
  106. public function getOriginalKeys()
  107. {
  108. return $this->originalKeys;
  109. }
  110. /**
  111. * {@inheritdoc}
  112. */
  113. public function getChoicesForValues(array $values)
  114. {
  115. $choices = [];
  116. foreach ($values as $i => $givenValue) {
  117. if (\array_key_exists($givenValue, $this->choices)) {
  118. $choices[$i] = $this->choices[$givenValue];
  119. }
  120. }
  121. return $choices;
  122. }
  123. /**
  124. * {@inheritdoc}
  125. */
  126. public function getValuesForChoices(array $choices)
  127. {
  128. $values = [];
  129. // Use the value callback to compare choices by their values, if present
  130. if ($this->valueCallback) {
  131. $givenValues = [];
  132. foreach ($choices as $i => $givenChoice) {
  133. $givenValues[$i] = (string) ($this->valueCallback)($givenChoice);
  134. }
  135. return array_intersect($givenValues, array_keys($this->choices));
  136. }
  137. // Otherwise compare choices by identity
  138. foreach ($choices as $i => $givenChoice) {
  139. foreach ($this->choices as $value => $choice) {
  140. if ($choice === $givenChoice) {
  141. $values[$i] = (string) $value;
  142. break;
  143. }
  144. }
  145. }
  146. return $values;
  147. }
  148. /**
  149. * Flattens an array into the given output variables.
  150. *
  151. * @param array $choices The array to flatten
  152. * @param callable $value The callable for generating choice values
  153. * @param array|null $choicesByValues The flattened choices indexed by the
  154. * corresponding values
  155. * @param array|null $keysByValues The original keys indexed by the
  156. * corresponding values
  157. * @param array|null $structuredValues The values indexed by the original keys
  158. *
  159. * @internal
  160. */
  161. protected function flatten(array $choices, callable $value, ?array &$choicesByValues, ?array &$keysByValues, ?array &$structuredValues)
  162. {
  163. if (null === $choicesByValues) {
  164. $choicesByValues = [];
  165. $keysByValues = [];
  166. $structuredValues = [];
  167. }
  168. foreach ($choices as $key => $choice) {
  169. if (\is_array($choice)) {
  170. $this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]);
  171. continue;
  172. }
  173. $choiceValue = (string) $value($choice);
  174. $choicesByValues[$choiceValue] = $choice;
  175. $keysByValues[$choiceValue] = $key;
  176. $structuredValues[$key] = $choiceValue;
  177. }
  178. }
  179. /**
  180. * Checks whether the given choices can be cast to strings without
  181. * generating duplicates.
  182. * This method is responsible for preventing conflict between scalar values
  183. * and the empty value.
  184. */
  185. private function castableToString(array $choices, array &$cache = []): bool
  186. {
  187. foreach ($choices as $choice) {
  188. if (\is_array($choice)) {
  189. if (!$this->castableToString($choice, $cache)) {
  190. return false;
  191. }
  192. continue;
  193. } elseif (!is_scalar($choice)) {
  194. return false;
  195. }
  196. // prevent having false casted to the empty string by isset()
  197. $choice = false === $choice ? '0' : (string) $choice;
  198. if (isset($cache[$choice])) {
  199. return false;
  200. }
  201. $cache[$choice] = true;
  202. }
  203. return true;
  204. }
  205. }