10000 keep valid submitted choices when additional choices are submitted · symfony/symfony@4089c9d · GitHub
[go: up one dir, main page]

Skip to content

Commit 4089c9d

Browse files
committed
keep valid submitted choices when additional choices are submitted
1 parent 0ed047f commit 4089c9d

File tree

3 files changed

+75
-26
lines changed

3 files changed

+75
-26
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
<service id="form.type.choice" class="Symfony\Component\Form\Extension\Core\Type\ChoiceType">
7070
<tag name="form.type" />
7171
<argument type="service" id="form.choice_list_factory"/>
72+
<argument type="service" id="translator" on-invalid="ignore" />
7273
</service>
7374
<service id="form.type.file" class="Symfony\Component\Form\Extension\Core\Type\FileType" public="true">
7475
<tag name="form.type" />

src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,31 +27,41 @@
2727
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
2828
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
2929
use Symfony\Component\Form\FormBuilderInterface;
30+
use Symfony\Component\Form\FormError;
3031
use Symfony\Component\Form\FormEvent;
3132
use Symfony\Component\Form\FormEvents;
3233
use Symfony\Component\Form\FormInterface;
3334
use Symfony\Component\Form\FormView;
3435
use Symfony\Component\OptionsResolver\Options;
3536
use Symfony\Component\OptionsResolver\OptionsResolver;
37+
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
38+
use Symfony\Contracts\Translation\TranslatorInterface;
3639

3740
class ChoiceType extends AbstractType
3841
{
3942
private $choiceListFactory;
43+
private $translator;
4044

41-
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null)
45+
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null, $translator = null)
4246
{
4347
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
4448
new PropertyAccessDecorator(
4549
new DefaultChoiceListFactory()
4650
)
4751
);
52+
53+
if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
54+
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
55+
}
56+
$this->translator = $translator;
4857
}
4958

5059
/**
5160
* {@inheritdoc}
5261
*/
5362
public function buildForm(FormBuilderInterface $builder, array $options)
5463
{
64+
$unknownValues = [];
5565
$choiceList = $this->createChoiceList($options);
5666
$builder->setAttribute('choice_list', $choiceList);
5767

@@ -79,10 +89,12 @@ public function buildForm(FormBuilderInterface $builder, array $options)
7989

8090
$this->addSubForms($builder, $choiceListView->preferredChoices, $options);
8191
$this->addSubForms($builder, $choiceListView->choices, $options);
92+
}
8293

94+
if ($options['expanded'] || $options['multiple']) {
8395
// Make sure that scalar, submitted values are converted to arrays
8496
// which can be submitted to the checkboxes/radio buttons
85-
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
97+
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($choiceList, &$unknownValues) {
8698
$form = $event->getForm();
8799
$data = $event->getData();
88100

@@ -97,6 +109,10 @@ public function buildForm(FormBuilderInterface $builder, array $options)
97109
// Convert the submitted data to a string, if scalar, before
98110
// casting it to an array
99111
if (!\is_array($data)) {
112+
if ($form->getConfig()->getOption('multiple', false)) {
113+
throw new TransformationFailedException('Expected an array.');
114+
}
115+
100116
$data = (array) (string) $data;
101117
}
102118

@@ -108,34 +124,61 @@ public function buildForm(FormBuilderInterface $builder, array $options)
108124
$unknownValues = $valueMap;
109125

110126
// Reconstruct the data as mapping from child names to values
111-
$data = [];
112-
113-
/** @var FormInterface $child */
114-
foreach ($form as $child) {
115-
$value = $child->getConfig()->getOption('value');
116-
117-
// Add the value to $data with the child's name as key
118-
if (isset($valueMap[$value])) {
119-
$data[$child->getName()] = $value;
120-
unset($unknownValues[$value]);
121-
continue;
127+
$knownValues = [];
128+
129+
if ($form->getConfig()->getOption('expanded', false)) {
130+
/** @var FormInterface $child */
131+
foreach ($form as $child) {
132+
$value = $child->getConfig()->getOption('value');
133+
134+
// Add the value to $data with the child's name as key
135+
if (isset($valueMap[$value])) {
136+
$knownValues[$child->getName()] = $value;
137+
unset($unknownValues[$value]);
138+
continue;
139+
}
140+
}
141+
} else {
142+
foreach ($data as $value) {
143+
if ($choiceList->getChoicesForValues([$value])) {
144+
$knownValues[] = $value;
145+
unset($unknownValues[$value]);
146+
}
122147
}
123148
}
124149

125150
// The empty value is always known, independent of whether a
126151
// field exists for it or not
127152
unset($unknownValues['']);
128153

129-
// Throw exception if unknown values were submitted
130-
if (\count($unknownValues) > 0) {
154+
// Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
155+
if (\count($unknownValues) > 0 && !$form->getConfig()->getOption('multiple', false)) {
131156
throw new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))));
132157
}
133158

134-
$event->setData($data);
159+
$event->setData($knownValues);
135160
});
136161
}
137162

138163
if ($options['multiple']) {
164+
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use (&$unknownValues) {
165+
// Throw exception if unknown values were submitted
166+
if (\count($unknownValues) > 0) {
167+
$form = $event->getForm();
168+
169+
$clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : \gettype($form->getViewData());
170+
$messageTemplate = 'The value {{ value }} is not valid.';
171+
172+
if (null !== $this->translator) {
173+
$message = $this->translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators');
174+
} else {
175+
$message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]);
176+
}
177+
178+
$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))))));
179+
}
180+
});
181+
139182
// <select> tag with "multiple" option or list of checkbox inputs
140183
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
141184
} else {

src/Symfony/Component/Form/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)
0