From 85989c3678d243e229f929008ede8b2e24c0c3fa Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 29 Dec 2020 14:49:11 +0100 Subject: [PATCH] keep valid submitted choices when additional choices are submitted --- .../FrameworkBundle/Resources/config/form.xml | 1 + .../Form/Extension/Core/Type/ChoiceType.php | 74 +++++++++++++++---- .../Extension/Core/Type/ChoiceTypeTest.php | 25 ++++--- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index 17598fa958..05a58c4c4c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -69,6 +69,7 @@ + diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 4297460d34..bd2985b313 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -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)))))); + } + }); + //