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
-------

85989c3678 keep valid submitted choices when additional choices are submitted
This commit is contained in:
Christian Flothmann 2021-02-15 12:22:00 +01:00
commit 0574c1586a
3 changed files with 76 additions and 24 deletions

View File

@ -69,6 +69,7 @@
<service id="form.type.choice" class="Symfony\Component\Form\Extension\Core\Type\ChoiceType">
<tag name="form.type" />
<argument type="service" id="form.choice_list_factory"/>
<argument type="service" id="translator" on-invalid="ignore" />
</service>
<service id="form.type.file" class="Symfony\Component\Form\Extension\Core\Type\FileType" public="true">
<tag name="form.type" />

View File

@ -28,6 +28,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransfo
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
@ -35,18 +36,29 @@ use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ChoiceType extends AbstractType
{
private $choiceListFactory;
private $translator;
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null)
/**
* @param TranslatorInterface $translator
*/
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null, $translator = null)
{
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
new PropertyAccessDecorator(
new DefaultChoiceListFactory()
)
);
if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
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)));
}
$this->translator = $translator;
}
/**
@ -54,6 +66,7 @@ class ChoiceType extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$unknownValues = [];
$choiceList = $this->createChoiceList($options);
$builder->setAttribute('choice_list', $choiceList);
@ -81,10 +94,12 @@ class ChoiceType extends AbstractType
$this->addSubForms($builder, $choiceListView->preferredChoices, $options);
$this->addSubForms($builder, $choiceListView->choices, $options);
}
if ($options['expanded'] || $options['multiple']) {
// Make sure that scalar, submitted values are converted to arrays
// which can be submitted to the checkboxes/radio buttons
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($choiceList, $options, &$unknownValues) {
$form = $event->getForm();
$data = $event->getData();
@ -99,6 +114,10 @@ class ChoiceType extends AbstractType
// Convert the submitted data to a string, if scalar, before
// casting it to an array
if (!\is_array($data)) {
if ($options['multiple']) {
throw new TransformationFailedException('Expected an array.');
}
$data = (array) (string) $data;
}
@ -110,17 +129,26 @@ class ChoiceType extends AbstractType
$unknownValues = $valueMap;
// Reconstruct the data as mapping from child names to values
$data = [];
$knownValues = [];
/** @var FormInterface $child */
foreach ($form as $child) {
$value = $child->getConfig()->getOption('value');
if ($options['expanded']) {
/** @var FormInterface $child */
foreach ($form as $child) {
$value = $child->getConfig()->getOption('value');
// Add the value to $data with the child's name as key
if (isset($valueMap[$value])) {
$data[$child->getName()] = $value;
unset($unknownValues[$value]);
continue;
// Add the value to $data with the child's name as key
if (isset($valueMap[$value])) {
$knownValues[$child->getName()] = $value;
unset($unknownValues[$value]);
continue;
}
}
} else {
foreach ($data as $value) {
if ($choiceList->getChoicesForValues([$value])) {
$knownValues[] = $value;
unset($unknownValues[$value]);
}
}
}
@ -128,16 +156,34 @@ class ChoiceType extends AbstractType
// field exists for it or not
unset($unknownValues['']);
// Throw exception if unknown values were submitted
if (\count($unknownValues) > 0) {
// Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
if (\count($unknownValues) > 0 && !$options['multiple']) {
throw new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))));
}
$event->setData($data);
$event->setData($knownValues);
});
}
if ($options['multiple']) {
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use (&$unknownValues) {
// Throw exception if unknown values were submitted
if (\count($unknownValues) > 0) {
$form = $event->getForm();
$clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : \gettype($form->getViewData());
$messageTemplate = 'The value {{ value }} is not valid.';
if (null !== $this->translator) {
$message = $this->translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators');
} else {
$message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]);
}
$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))))));
}
});
// <select> tag with "multiple" option or list of checkbox inputs
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
} else {

View File

@ -808,9 +808,9 @@ class ChoiceTypeTest extends BaseTypeTest
$form->submit(['a', 'foobar']);
$this->assertNull($form->getData());
$this->assertEquals(['a', 'foobar'], $form->getViewData());
$this->assertFalse($form->isSynchronized());
$this->assertEquals(['a'], $form->getData());
$this->assertEquals(['a'], $form->getViewData());
$this->assertFalse($form->isValid());
}
public function testSubmitMultipleNonExpandedObjectChoices()
@ -1351,17 +1351,17 @@ class ChoiceTypeTest extends BaseTypeTest
$form->submit(['a', 'foobar']);
$this->assertNull($form->getData());
$this->assertSame(['a', 'foobar'], $form->getViewData());
$this->assertSame(['a'], $form->getData());
$this->assertSame(['a'], $form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertFalse($form->isSynchronized());
$this->assertFalse($form->isValid());
$this->assertFalse($form[0]->getData());
$this->assertTrue($form[0]->getData());
$this->assertFalse($form[1]->getData());
$this->assertFalse($form[2]->getData());
$this->assertFalse($form[3]->getData());
$this->assertFalse($form[4]->getData());
$this->assertNull($form[0]->getViewData());
$this->assertSame('a', $form[0]->getViewData());
$this->assertNull($form[1]->getViewData());
$this->assertNull($form[2]->getViewData());
$this->assertNull($form[3]->getViewData());
@ -2036,8 +2036,13 @@ class ChoiceTypeTest extends BaseTypeTest
$form->submit($multiple ? (array) $submittedData : $submittedData);
// When the choice does not exist the transformation fails
$this->assertFalse($form->isSynchronized());
$this->assertNull($form->getData());
$this->assertFalse($form->isValid());
if ($multiple) {
$this->assertSame([], $form->getData());
} else {
$this->assertNull($form->getData());
}
}
/**