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:
commit
0574c1586a
@ -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" />
|
||||
|
@ -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 {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user