]> git.immae.eu Git - github/wallabag/wallabag.git/blob - vendor/symfony/form/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
twig implementation
[github/wallabag/wallabag.git] / vendor / symfony / form / Symfony / Component / Form / Extension / Core / Type / ChoiceType.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\Type;
13
14 use Symfony\Component\Form\AbstractType;
15 use Symfony\Component\Form\Extension\Core\View\ChoiceView;
16 use Symfony\Component\Form\FormBuilderInterface;
17 use Symfony\Component\Form\FormInterface;
18 use Symfony\Component\Form\FormView;
19 use Symfony\Component\Form\Exception\LogicException;
20 use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
21 use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
22 use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener;
23 use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
24 use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
25 use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
26 use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
27 use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer;
28 use Symfony\Component\OptionsResolver\Options;
29 use Symfony\Component\OptionsResolver\OptionsResolverInterface;
30
31 class ChoiceType extends AbstractType
32 {
33 /**
34 * Caches created choice lists.
35 * @var array
36 */
37 private $choiceListCache = array();
38
39 /**
40 * {@inheritdoc}
41 */
42 public function buildForm(FormBuilderInterface $builder, array $options)
43 {
44 if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
45 throw new LogicException('Either the option "choices" or "choice_list" must be set.');
46 }
47
48 if ($options['expanded']) {
49 // Initialize all choices before doing the index check below.
50 // This helps in cases where index checks are optimized for non
51 // initialized choice lists. For example, when using an SQL driver,
52 // the index check would read in one SQL query and the initialization
53 // requires another SQL query. When the initialization is done first,
54 // one SQL query is sufficient.
55 $preferredViews = $options['choice_list']->getPreferredViews();
56 $remainingViews = $options['choice_list']->getRemainingViews();
57
58 // Check if the choices already contain the empty value
59 // Only add the empty value option if this is not the case
60 if (null !== $options['empty_value'] && 0 === count($options['choice_list']->getIndicesForValues(array('')))) {
61 $placeholderView = new ChoiceView(null, '', $options['empty_value']);
62
63 // "placeholder" is a reserved index
64 // see also ChoiceListInterface::getIndicesForChoices()
65 $this->addSubForms($builder, array('placeholder' => $placeholderView), $options);
66 }
67
68 $this->addSubForms($builder, $preferredViews, $options);
69 $this->addSubForms($builder, $remainingViews, $options);
70
71 if ($options['multiple']) {
72 $builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']));
73 $builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10);
74 } else {
75 $builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder')));
76 $builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10);
77 }
78 } else {
79 if ($options['multiple']) {
80 $builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
81 } else {
82 $builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
83 }
84 }
85
86 if ($options['multiple'] && $options['by_reference']) {
87 // Make sure the collection created during the client->norm
88 // transformation is merged back into the original collection
89 $builder->addEventSubscriber(new MergeCollectionListener(true, true));
90 }
91 }
92
93 /**
94 * {@inheritdoc}
95 */
96 public function buildView(FormView $view, FormInterface $form, array $options)
97 {
98 $view->vars = array_replace($view->vars, array(
99 'multiple' => $options['multiple'],
100 'expanded' => $options['expanded'],
101 'preferred_choices' => $options['choice_list']->getPreferredViews(),
102 'choices' => $options['choice_list']->getRemainingViews(),
103 'separator' => '-------------------',
104 'empty_value' => null,
105 ));
106
107 // The decision, whether a choice is selected, is potentially done
108 // thousand of times during the rendering of a template. Provide a
109 // closure here that is optimized for the value of the form, to
110 // avoid making the type check inside the closure.
111 if ($options['multiple']) {
112 $view->vars['is_selected'] = function ($choice, array $values) {
113 return false !== array_search($choice, $values, true);
114 };
115 } else {
116 $view->vars['is_selected'] = function ($choice, $value) {
117 return $choice === $value;
118 };
119 }
120
121 // Check if the choices already contain the empty value
122 // Only add the empty value option if this is not the case
123 if (null !== $options['empty_value'] && 0 === count($options['choice_list']->getIndicesForValues(array('')))) {
124 $view->vars['empty_value'] = $options['empty_value'];
125 }
126
127 if ($options['multiple'] && !$options['expanded']) {
128 // Add "[]" to the name in case a select tag with multiple options is
129 // displayed. Otherwise only one of the selected options is sent in the
130 // POST request.
131 $view->vars['full_name'] = $view->vars['full_name'].'[]';
132 }
133 }
134
135 /**
136 * {@inheritdoc}
137 */
138 public function finishView(FormView $view, FormInterface $form, array $options)
139 {
140 if ($options['expanded']) {
141 // Radio buttons should have the same name as the parent
142 $childName = $view->vars['full_name'];
143
144 // Checkboxes should append "[]" to allow multiple selection
145 if ($options['multiple']) {
146 $childName .= '[]';
147 }
148
149 foreach ($view as $childView) {
150 $childView->vars['full_name'] = $childName;
151 }
152 }
153 }
154
155 /**
156 * {@inheritdoc}
157 */
158 public function setDefaultOptions(OptionsResolverInterface $resolver)
159 {
160 $choiceListCache =& $this->choiceListCache;
161
162 $choiceList = function (Options $options) use (&$choiceListCache) {
163 // Harden against NULL values (like in EntityType and ModelType)
164 $choices = null !== $options['choices'] ? $options['choices'] : array();
165
166 // Reuse existing choice lists in order to increase performance
167 $hash = md5(json_encode(array($choices, $options['preferred_choices'])));
168
169 if (!isset($choiceListCache[$hash])) {
170 $choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']);
171 }
172
173 return $choiceListCache[$hash];
174 };
175
176 $emptyData = function (Options $options) {
177 if ($options['multiple'] || $options['expanded']) {
178 return array();
179 }
180
181 return '';
182 };
183
184 $emptyValue = function (Options $options) {
185 return $options['required'] ? null : '';
186 };
187
188 $emptyValueNormalizer = function (Options $options, $emptyValue) {
189 if ($options['multiple']) {
190 // never use an empty value for this case
191 return null;
192 } elseif (false === $emptyValue) {
193 // an empty value should be added but the user decided otherwise
194 return null;
195 } elseif ($options['expanded'] && '' === $emptyValue) {
196 // never use an empty label for radio buttons
197 return 'None';
198 }
199
200 // empty value has been set explicitly
201 return $emptyValue;
202 };
203
204 $compound = function (Options $options) {
205 return $options['expanded'];
206 };
207
208 $resolver->setDefaults(array(
209 'multiple' => false,
210 'expanded' => false,
211 'choice_list' => $choiceList,
212 'choices' => array(),
213 'preferred_choices' => array(),
214 'empty_data' => $emptyData,
215 'empty_value' => $emptyValue,
216 'error_bubbling' => false,
217 'compound' => $compound,
218 // The view data is always a string, even if the "data" option
219 // is manually set to an object.
220 // See https://github.com/symfony/symfony/pull/5582
221 'data_class' => null,
222 ));
223
224 $resolver->setNormalizers(array(
225 'empty_value' => $emptyValueNormalizer,
226 ));
227
228 $resolver->setAllowedTypes(array(
229 'choice_list' => array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'),
230 ));
231 }
232
233 /**
234 * {@inheritdoc}
235 */
236 public function getName()
237 {
238 return 'choice';
239 }
240
241 /**
242 * Adds the sub fields for an expanded choice field.
243 *
244 * @param FormBuilderInterface $builder The form builder.
245 * @param array $choiceViews The choice view objects.
246 * @param array $options The build options.
247 */
248 private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
249 {
250 foreach ($choiceViews as $i => $choiceView) {
251 if (is_array($choiceView)) {
252 // Flatten groups
253 $this->addSubForms($builder, $choiceView, $options);
254 } else {
255 $choiceOpts = array(
256 'value' => $choiceView->value,
257 'label' => $choiceView->label,
258 'translation_domain' => $options['translation_domain'],
259 );
260
261 if ($options['multiple']) {
262 $choiceType = 'checkbox';
263 // The user can check 0 or more checkboxes. If required
264 // is true, he is required to check all of them.
265 $choiceOpts['required'] = false;
266 } else {
267 $choiceType = 'radio';
268 }
269
270 $builder->add($i, $choiceType, $choiceOpts);
271 }
272 }
273 }
274 }