Skip to content

Commit 08bf50f

Browse files
committed
keep valid submitted choices when additional choices are submitted
1 parent 346db57 commit 08bf50f

File tree

2 files changed

+77
-26
lines changed

2 files changed

+77
-26
lines changed

Extension/Core/Type/ChoiceType.php

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,45 @@
2828
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
2929
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
3030
use Symfony\Component\Form\FormBuilderInterface;
31+
use Symfony\Component\Form\FormError;
3132
use Symfony\Component\Form\FormEvent;
3233
use Symfony\Component\Form\FormEvents;
3334
use Symfony\Component\Form\FormInterface;
3435
use Symfony\Component\Form\FormView;
3536
use Symfony\Component\OptionsResolver\Options;
3637
use Symfony\Component\OptionsResolver\OptionsResolver;
3738
use Symfony\Component\PropertyAccess\PropertyPath;
39+
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
40+
use Symfony\Contracts\Translation\TranslatorInterface;
3841

3942
class ChoiceType extends AbstractType
4043
{
4144
private $choiceListFactory;
45+
private $translator;
4246

43-
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null)
47+
/**
48+
* @param TranslatorInterface $translator
49+
*/
50+
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null, $translator = null)
4451
{
4552
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
4653
new PropertyAccessDecorator(
4754
new DefaultChoiceListFactory()
4855
)
4956
);
57+
58+
if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
59+
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
60+
}
61+
$this->translator = $translator;
5062
}
5163

5264
/**
5365
* {@inheritdoc}
5466
*/
5567
public function buildForm(FormBuilderInterface $builder, array $options)
5668
{
69+
$unknownValues = [];
5770
$choiceList = $this->createChoiceList($options);
5871
$builder->setAttribute('choice_list', $choiceList);
5972

@@ -81,10 +94,12 @@ public function buildForm(FormBuilderInterface $builder, array $options)
8194

8295
$this->addSubForms($builder, $choiceListView->preferredChoices, $options);
8396
$this->addSubForms($builder, $choiceListView->choices, $options);
97+
}
8498

99+
if ($options['expanded'] || $options['multiple']) {
85100
// Make sure that scalar, submitted values are converted to arrays
86101
// which can be submitted to the checkboxes/radio buttons
87-
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
102+
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($choiceList, $options, &$unknownValues) {
88103
$form = $event->getForm();
89104
$data = $event->getData();
90105

@@ -99,6 +114,10 @@ public function buildForm(FormBuilderInterface $builder, array $options)
99114
// Convert the submitted data to a string, if scalar, before
100115
// casting it to an array
101116
if (!\is_array($data)) {
117+
if ($options['multiple']) {
118+
throw new TransformationFailedException('Expected an array.');
119+
}
120+
102121
$data = (array) (string) $data;
103122
}
104123

@@ -110,34 +129,61 @@ public function buildForm(FormBuilderInterface $builder, array $options)
110129
$unknownValues = $valueMap;
111130

112131
// Reconstruct the data as mapping from child names to values
113-
$data = [];
114-
115-
/** @var FormInterface $child */
116-
foreach ($form as $child) {
117-
$value = $child->getConfig()->getOption('value');
118-
119-
// Add the value to $data with the child's name as key
120-
if (isset($valueMap[$value])) {
121-
$data[$child->getName()] = $value;
122-
unset($unknownValues[$value]);
123-
continue;
132+
$knownValues = [];
133+
134+
if ($options['expanded']) {
135+
/** @var FormInterface $child */
136+
foreach ($form as $child) {
137+
$value = $child->getConfig()->getOption('value');
138+
139+
// Add the value to $data with the child's name as key
140+
if (isset($valueMap[$value])) {
141+
$knownValues[$child->getName()] = $value;
142+
unset($unknownValues[$value]);
143+
continue;
144+
}
145+
}
146+
} else {
147+
foreach ($data as $value) {
148+
if ($choiceList->getChoicesForValues([$value])) {
149+
$knownValues[] = $value;
150+
unset($unknownValues[$value]);
151+
}
124152
}
125153
}
126154

127155
// The empty value is always known, independent of whether a
128156
// field exists for it or not
129157
unset($unknownValues['']);
130158

131-
// Throw exception if unknown values were submitted
132-
if (\count($unknownValues) > 0) {
159+
// Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
160+
if (\count($unknownValues) > 0 && !$options['multiple']) {
133161
throw new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))));
134162
}
135163

136-
$event->setData($data);
164+
$event->setData($knownValues);
137165
});
138166
}
139167

140168
if ($options['multiple']) {
169+
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use (&$unknownValues) {
170+
// Throw exception if unknown values were submitted
171+
if (\count($unknownValues) > 0) {
172+
$form = $event->getForm();
173+
174+
$clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : \gettype($form->getViewData());
175+
$messageTemplate = 'The value {{ value }} is not valid.';
176+
177+
if (null !== $this->translator) {
178+
$message = $this->translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators');
179+
} else {
180+
$message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]);
181+
}
182+
183+
$form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))))));
184+
}
185+
});
186+
141187
// <select> tag with "multiple" option or list of checkbox inputs
142188
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
143189
} else {

Tests/Extension/Core/Type/ChoiceTypeTest.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -806,9 +806,9 @@ public function testSubmitMultipleNonExpandedInvalidArrayChoice()
806806

807807
$form->submit(['a', 'foobar']);
808808

809-
$this->assertNull($form->getData());
810-
$this->assertEquals(['a', 'foobar'], $form->getViewData());
811-
$this->assertFalse($form->isSynchronized());
809+
$this->assertEquals(['a'], $form->getData());
810+
$this->assertEquals(['a'], $form->getViewData());
811+
$this->assertFalse($form->isValid());
812812
}
813813

814814
public function testSubmitMultipleNonExpandedObjectChoices()
@@ -1349,17 +1349,17 @@ public function testSubmitMultipleExpandedInvalidArrayChoice()
13491349

13501350
$form->submit(['a', 'foobar']);
13511351

1352-
$this->assertNull($form->getData());
1353-
$this->assertSame(['a', 'foobar'], $form->getViewData());
1352+
$this->assertSame(['a'], $form->getData());
1353+
$this->assertSame(['a'], $form->getViewData());
13541354
$this->assertEmpty($form->getExtraData());
1355-
$this->assertFalse($form->isSynchronized());
1355+
$this->assertFalse($form->isValid());
13561356

1357-
$this->assertFalse($form[0]->getData());
1357+
$this->assertTrue($form[0]->getData());
13581358
$this->assertFalse($form[1]->getData());
13591359
$this->assertFalse($form[2]->getData());
13601360
$this->assertFalse($form[3]->getData());
13611361
$this->assertFalse($form[4]->getData());
1362-
$this->assertNull($form[0]->getViewData());
1362+
$this->assertSame('a', $form[0]->getViewData());
13631363
$this->assertNull($form[1]->getViewData());
13641364
$this->assertNull($form[2]->getViewData());
13651365
$this->assertNull($form[3]->getViewData());
@@ -2033,8 +2033,13 @@ public function testTrimIsDisabled($multiple, $expanded)
20332033
$form->submit($multiple ? (array) $submittedData : $submittedData);
20342034

20352035
// When the choice does not exist the transformation fails
2036-
$this->assertFalse($form->isSynchronized());
2037-
$this->assertNull($form->getData());
2036+
$this->assertFalse($form->isValid());
2037+
2038+
if ($multiple) {
2039+
$this->assertSame([], $form->getData());
2040+
} else {
2041+
$this->assertNull($form->getData());
2042+
}
20382043
}
20392044

20402045
/**

0 commit comments

Comments
 (0)