/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php

http://github.com/symfony/symfony · PHP · 302 lines · 214 code · 36 blank · 52 comment · 24 complexity · f53037ed9ede3e607390bb890360f3b6 MD5 · raw file

  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\Factory;
  11. use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
  12. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  13. use Symfony\Component\Form\ChoiceList\LazyChoiceList;
  14. use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
  15. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  16. use Symfony\Component\Form\ChoiceList\Loader\FilterChoiceLoaderDecorator;
  17. use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
  18. use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
  19. use Symfony\Component\Form\ChoiceList\View\ChoiceView;
  20. /**
  21. * Default implementation of {@link ChoiceListFactoryInterface}.
  22. *
  23. * @author Bernhard Schussek <bschussek@gmail.com>
  24. * @author Jules Pietri <jules@heahprod.com>
  25. */
  26. class DefaultChoiceListFactory implements ChoiceListFactoryInterface
  27. {
  28. /**
  29. * {@inheritdoc}
  30. *
  31. * @param callable|null $filter
  32. */
  33. public function createListFromChoices(iterable $choices, callable $value = null/*, callable $filter = null*/)
  34. {
  35. $filter = \func_num_args() > 2 ? func_get_arg(2) : null;
  36. if ($filter) {
  37. // filter the choice list lazily
  38. return $this->createListFromLoader(new FilterChoiceLoaderDecorator(
  39. new CallbackChoiceLoader(static function () use ($choices) {
  40. return $choices;
  41. }
  42. ), $filter), $value);
  43. }
  44. return new ArrayChoiceList($choices, $value);
  45. }
  46. /**
  47. * {@inheritdoc}
  48. *
  49. * @param callable|null $filter
  50. */
  51. public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null/*, callable $filter = null*/)
  52. {
  53. $filter = \func_num_args() > 2 ? func_get_arg(2) : null;
  54. if ($filter) {
  55. $loader = new FilterChoiceLoaderDecorator($loader, $filter);
  56. }
  57. return new LazyChoiceList($loader, $value);
  58. }
  59. /**
  60. * {@inheritdoc}
  61. */
  62. public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, callable $index = null, callable $groupBy = null, $attr = null)
  63. {
  64. $preferredViews = [];
  65. $preferredViewsOrder = [];
  66. $otherViews = [];
  67. $choices = $list->getChoices();
  68. $keys = $list->getOriginalKeys();
  69. if (!\is_callable($preferredChoices)) {
  70. if (empty($preferredChoices)) {
  71. $preferredChoices = null;
  72. } else {
  73. // make sure we have keys that reflect order
  74. $preferredChoices = array_values($preferredChoices);
  75. $preferredChoices = static function ($choice) use ($preferredChoices) {
  76. return array_search($choice, $preferredChoices, true);
  77. };
  78. }
  79. }
  80. // The names are generated from an incrementing integer by default
  81. if (null === $index) {
  82. $index = 0;
  83. }
  84. // If $groupBy is a callable returning a string
  85. // choices are added to the group with the name returned by the callable.
  86. // If $groupBy is a callable returning an array
  87. // choices are added to the groups with names returned by the callable
  88. // If the callable returns null, the choice is not added to any group
  89. if (\is_callable($groupBy)) {
  90. foreach ($choices as $value => $choice) {
  91. self::addChoiceViewsGroupedByCallable(
  92. $groupBy,
  93. $choice,
  94. $value,
  95. $label,
  96. $keys,
  97. $index,
  98. $attr,
  99. $preferredChoices,
  100. $preferredViews,
  101. $preferredViewsOrder,
  102. $otherViews
  103. );
  104. }
  105. // Remove empty group views that may have been created by
  106. // addChoiceViewsGroupedByCallable()
  107. foreach ($preferredViews as $key => $view) {
  108. if ($view instanceof ChoiceGroupView && 0 === \count($view->choices)) {
  109. unset($preferredViews[$key]);
  110. }
  111. }
  112. foreach ($otherViews as $key => $view) {
  113. if ($view instanceof ChoiceGroupView && 0 === \count($view->choices)) {
  114. unset($otherViews[$key]);
  115. }
  116. }
  117. foreach ($preferredViewsOrder as $key => $groupViewsOrder) {
  118. if ($groupViewsOrder) {
  119. $preferredViewsOrder[$key] = min($groupViewsOrder);
  120. } else {
  121. unset($preferredViewsOrder[$key]);
  122. }
  123. }
  124. } else {
  125. // Otherwise use the original structure of the choices
  126. self::addChoiceViewsFromStructuredValues(
  127. $list->getStructuredValues(),
  128. $label,
  129. $choices,
  130. $keys,
  131. $index,
  132. $attr,
  133. $preferredChoices,
  134. $preferredViews,
  135. $preferredViewsOrder,
  136. $otherViews
  137. );
  138. }
  139. uksort($preferredViews, static function ($a, $b) use ($preferredViewsOrder): int {
  140. return isset($preferredViewsOrder[$a], $preferredViewsOrder[$b])
  141. ? $preferredViewsOrder[$a] <=> $preferredViewsOrder[$b]
  142. : 0;
  143. });
  144. return new ChoiceListView($otherViews, $preferredViews);
  145. }
  146. private static function addChoiceView($choice, string $value, $label, array $keys, &$index, $attr, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews)
  147. {
  148. // $value may be an integer or a string, since it's stored in the array
  149. // keys. We want to guarantee it's a string though.
  150. $key = $keys[$value];
  151. $nextIndex = \is_int($index) ? $index++ : $index($choice, $key, $value);
  152. // BC normalize label to accept a false value
  153. if (null === $label) {
  154. // If the labels are null, use the original choice key by default
  155. $label = (string) $key;
  156. } elseif (false !== $label) {
  157. // If "choice_label" is set to false and "expanded" is true, the value false
  158. // should be passed on to the "label" option of the checkboxes/radio buttons
  159. $dynamicLabel = $label($choice, $key, $value);
  160. $label = false === $dynamicLabel ? false : (string) $dynamicLabel;
  161. }
  162. $view = new ChoiceView(
  163. $choice,
  164. $value,
  165. $label,
  166. // The attributes may be a callable or a mapping from choice indices
  167. // to nested arrays
  168. \is_callable($attr) ? $attr($choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : [])
  169. );
  170. // $isPreferred may be null if no choices are preferred
  171. if (null !== $isPreferred && false !== $preferredKey = $isPreferred($choice, $key, $value)) {
  172. $preferredViews[$nextIndex] = $view;
  173. $preferredViewsOrder[$nextIndex] = $preferredKey;
  174. }
  175. $otherViews[$nextIndex] = $view;
  176. }
  177. private static function addChoiceViewsFromStructuredValues(array $values, $label, array $choices, array $keys, &$index, $attr, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews)
  178. {
  179. foreach ($values as $key => $value) {
  180. if (null === $value) {
  181. continue;
  182. }
  183. // Add the contents of groups to new ChoiceGroupView instances
  184. if (\is_array($value)) {
  185. $preferredViewsForGroup = [];
  186. $otherViewsForGroup = [];
  187. self::addChoiceViewsFromStructuredValues(
  188. $value,
  189. $label,
  190. $choices,
  191. $keys,
  192. $index,
  193. $attr,
  194. $isPreferred,
  195. $preferredViewsForGroup,
  196. $preferredViewsOrder,
  197. $otherViewsForGroup
  198. );
  199. if (\count($preferredViewsForGroup) > 0) {
  200. $preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup);
  201. }
  202. if (\count($otherViewsForGroup) > 0) {
  203. $otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup);
  204. }
  205. continue;
  206. }
  207. // Add ungrouped items directly
  208. self::addChoiceView(
  209. $choices[$value],
  210. $value,
  211. $label,
  212. $keys,
  213. $index,
  214. $attr,
  215. $isPreferred,
  216. $preferredViews,
  217. $preferredViewsOrder,
  218. $otherViews
  219. );
  220. }
  221. }
  222. private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choice, string $value, $label, array $keys, &$index, $attr, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews)
  223. {
  224. $groupLabels = $groupBy($choice, $keys[$value], $value);
  225. if (null === $groupLabels) {
  226. // If the callable returns null, don't group the choice
  227. self::addChoiceView(
  228. $choice,
  229. $value,
  230. $label,
  231. $keys,
  232. $index,
  233. $attr,
  234. $isPreferred,
  235. $preferredViews,
  236. $preferredViewsOrder,
  237. $otherViews
  238. );
  239. return;
  240. }
  241. $groupLabels = \is_array($groupLabels) ? array_map('strval', $groupLabels) : [(string) $groupLabels];
  242. foreach ($groupLabels as $groupLabel) {
  243. // Initialize the group views if necessary. Unnecessarily built group
  244. // views will be cleaned up at the end of createView()
  245. if (!isset($preferredViews[$groupLabel])) {
  246. $preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel);
  247. $otherViews[$groupLabel] = new ChoiceGroupView($groupLabel);
  248. }
  249. if (!isset($preferredViewsOrder[$groupLabel])) {
  250. $preferredViewsOrder[$groupLabel] = [];
  251. }
  252. self::addChoiceView(
  253. $choice,
  254. $value,
  255. $label,
  256. $keys,
  257. $index,
  258. $attr,
  259. $isPreferred,
  260. $preferredViews[$groupLabel]->choices,
  261. $preferredViewsOrder[$groupLabel],
  262. $otherViews[$groupLabel]->choices
  263. );
  264. }
  265. }
  266. }