]>
Commit | Line | Data |
---|---|---|
4f5b44bd NL |
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 | } |