8000 bug #39659 [Form] keep valid submitted choices when additional choice… · symfony/symfony@0574c15 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0574c15

Browse files
committed
bug #39659 [Form] keep valid submitted choices when additional choices are submitted (xabbuh)
This PR was merged into the 4.4 branch. Discussion ---------- [Form] keep valid submitted choices when additional choices are submitted | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #9738 | License | MIT | Doc PR | Commits ------- 85989c3 keep valid submitted choices when additional choices are submitted
2 parents eab9155 + 85989c3 commit 0574c15

File tree

3 files changed

+78
-26
lines changed

3 files changed

+78
-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: 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 {

src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php

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

809809
$form->submit(['a', 'foobar']);
810810

811-
$this->assertNull($form->getData());
812-
$this->assertEquals(['a', 'foobar'], $form->getViewData());
813-
$this->assertFalse($form->isSynchronized());
811+
$this->assertEquals(['a'], $form->getData());
812+
$this->assertEquals(['a'], $form->getViewData());
813+
$this->assertFalse($form->isValid());
814814
}
815815

816816
public function testSubmitMultipleNonExpandedObjectChoices()
@@ -1351,17 +1351,17 @@ public function testSubmitMultipleExpandedInvalidArrayChoice()
13511351

13521352
$form->submit(['a', 'foobar']);
13531353

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

1359-
$this->assertFalse($form[0]->getData());
1359+
$this->assertTrue($form[0]->getData());
13601360
$this->assertFalse($form[1]->getData());
13611361
$this->assertFalse($form[2]->getData());
13621362
$this->assertFalse($form[3]->getData());
13631363
$this->assertFalse($form[4]->getData());
1364-
$this->assertNull($form[0]->getViewData());
1364+
$this->assertSame('a', $form[0]->getViewData());
13651365
$this->assertNull($form[1]->getViewData());
13661366
$this->assertNull($form[2]->getViewData());
13671367
$this->assertNull($form[3]->getViewData());
@@ -2036,8 +2036,13 @@ public function testTrimIsDisabled($multiple, $expanded)
20362036
$form->submit($multiple ? (array) $submittedData : $submittedData);
20372037

20382038
// When the choice does not exist the transformation fails
2039-
$this->assertFalse($form->isSynchronized());
2040-
$this->assertNull($form->getData());
2039+
$this->assertFalse($form->isValid());
2040+
2041+
if ($multiple) {
2042+
$this->assertSame([], $form->getData());
2043+
} else {
2044+
$this->assertNull($form->getData());
2045+
}
20412046
}
20422047

20432048
/**

0 commit comments

Comments
 (0)
0