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">
|
<service id="form.type.choice" class="Symfony\Component\Form\Extension\Core\Type\ChoiceType">
|
||||||
<tag name="form.type" />
|
<tag name="form.type" />
|
||||||
<argument type="service" id="form.choice_list_factory"/>
|
<argument type="service" id="form.choice_list_factory"/>
|
||||||
|
<argument type="service" id="translator" on-invalid="ignore" />
|
||||||
</service>
|
</service>
|
||||||
<service id="form.type.file" class="Symfony\Component\Form\Extension\Core\Type\FileType" public="true">
|
<service id="form.type.file" class="Symfony\Component\Form\Extension\Core\Type\FileType" public="true">
|
||||||
<tag name="form.type" />
|
<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\DataTransformer\ChoiceToValueTransformer;
|
||||||
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
|
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
use Symfony\Component\Form\FormEvent;
|
use Symfony\Component\Form\FormEvent;
|
||||||
use Symfony\Component\Form\FormEvents;
|
use Symfony\Component\Form\FormEvents;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
@ -35,18 +36,29 @@ use Symfony\Component\Form\FormView;
|
|||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\PropertyAccess\PropertyPath;
|
use Symfony\Component\PropertyAccess\PropertyPath;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
class ChoiceType extends AbstractType
|
class ChoiceType extends AbstractType
|
||||||
{
|
{
|
||||||
private $choiceListFactory;
|
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(
|
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
|
||||||
new PropertyAccessDecorator(
|
new PropertyAccessDecorator(
|
||||||
new DefaultChoiceListFactory()
|
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)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
|
$unknownValues = [];
|
||||||
$choiceList = $this->createChoiceList($options);
|
$choiceList = $this->createChoiceList($options);
|
||||||
$builder->setAttribute('choice_list', $choiceList);
|
$builder->setAttribute('choice_list', $choiceList);
|
||||||
|
|
||||||
@ -81,10 +94,12 @@ class ChoiceType extends AbstractType
|
|||||||
|
|
||||||
$this->addSubForms($builder, $choiceListView->preferredChoices, $options);
|
$this->addSubForms($builder, $choiceListView->preferredChoices, $options);
|
||||||
$this->addSubForms($builder, $choiceListView->choices, $options);
|
$this->addSubForms($builder, $choiceListView->choices, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['expanded'] || $options['multiple']) {
|
||||||
// Make sure that scalar, submitted values are converted to arrays
|
// Make sure that scalar, submitted values are converted to arrays
|
||||||
// which can be submitted to the checkboxes/radio buttons
|
// 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();
|
$form = $event->getForm();
|
||||||
$data = $event->getData();
|
$data = $event->getData();
|
||||||
|
|
||||||
@ -99,6 +114,10 @@ class ChoiceType extends AbstractType
|
|||||||
// Convert the submitted data to a string, if scalar, before
|
// Convert the submitted data to a string, if scalar, before
|
||||||
// casting it to an array
|
// casting it to an array
|
||||||
if (!\is_array($data)) {
|
if (!\is_array($data)) {
|
||||||
|
if ($options['multiple']) {
|
||||||
|
throw new TransformationFailedException('Expected an array.');
|
||||||
|
}
|
||||||
|
|
||||||
$data = (array) (string) $data;
|
$data = (array) (string) $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,34 +129,61 @@ class ChoiceType extends AbstractType
|
|||||||
$unknownValues = $valueMap;
|
$unknownValues = $valueMap;
|
||||||
|
|
||||||
// Reconstruct the data as mapping from child names to values
|
// Reconstruct the data as mapping from child names to values
|
||||||
$data = [];
|
$knownValues = [];
|
||||||
|
|
||||||
|
if ($options['expanded']) {
|
||||||
/** @var FormInterface $child */
|
/** @var FormInterface $child */
|
||||||
foreach ($form as $child) {
|
foreach ($form as $child) {
|
||||||
$value = $child->getConfig()->getOption('value');
|
$value = $child->getConfig()->getOption('value');
|
||||||
|
|
||||||
// Add the value to $data with the child's name as key
|
// Add the value to $data with the child's name as key
|
||||||
if (isset($valueMap[$value])) {
|
if (isset($valueMap[$value])) {
|
||||||
$data[$child->getName()] = $value;
|
$knownValues[$child->getName()] = $value;
|
||||||
unset($unknownValues[$value]);
|
unset($unknownValues[$value]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($data as $value) {
|
||||||
|
if ($choiceList->getChoicesForValues([$value])) {
|
||||||
|
$knownValues[] = $value;
|
||||||
|
unset($unknownValues[$value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The empty value is always known, independent of whether a
|
// The empty value is always known, independent of whether a
|
||||||
// field exists for it or not
|
// field exists for it or not
|
||||||
unset($unknownValues['']);
|
unset($unknownValues['']);
|
||||||
|
|
||||||
// Throw exception if unknown values were submitted
|
// Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
|
||||||
if (\count($unknownValues) > 0) {
|
if (\count($unknownValues) > 0 && !$options['multiple']) {
|
||||||
throw new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))));
|
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']) {
|
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
|
// <select> tag with "multiple" option or list of checkbox inputs
|
||||||
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
|
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
|
||||||
} else {
|
} else {
|
||||||
|
@ -808,9 +808,9 @@ class ChoiceTypeTest extends BaseTypeTest
|
|||||||
|
|
||||||
$form->submit(['a', 'foobar']);
|
$form->submit(['a', 'foobar']);
|
||||||
|
|
||||||
$this->assertNull($form->getData());
|
$this->assertEquals(['a'], $form->getData());
|
||||||
$this->assertEquals(['a', 'foobar'], $form->getViewData());
|
$this->assertEquals(['a'], $form->getViewData());
|
||||||
$this->assertFalse($form->isSynchronized());
|
$this->assertFalse($form->isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSubmitMultipleNonExpandedObjectChoices()
|
public function testSubmitMultipleNonExpandedObjectChoices()
|
||||||
@ -1351,17 +1351,17 @@ class ChoiceTypeTest extends BaseTypeTest
|
|||||||
|
|
||||||
$form->submit(['a', 'foobar']);
|
$form->submit(['a', 'foobar']);
|
||||||
|
|
||||||
$this->assertNull($form->getData());
|
$this->assertSame(['a'], $form->getData());
|
||||||
$this->assertSame(['a', 'foobar'], $form->getViewData());
|
$this->assertSame(['a'], $form->getViewData());
|
||||||
$this->assertEmpty($form->getExtraData());
|
$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[1]->getData());
|
||||||
$this->assertFalse($form[2]->getData());
|
$this->assertFalse($form[2]->getData());
|
||||||
$this->assertFalse($form[3]->getData());
|
$this->assertFalse($form[3]->getData());
|
||||||
$this->assertFalse($form[4]->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[1]->getViewData());
|
||||||
$this->assertNull($form[2]->getViewData());
|
$this->assertNull($form[2]->getViewData());
|
||||||
$this->assertNull($form[3]->getViewData());
|
$this->assertNull($form[3]->getViewData());
|
||||||
@ -2036,9 +2036,14 @@ class ChoiceTypeTest extends BaseTypeTest
|
|||||||
$form->submit($multiple ? (array) $submittedData : $submittedData);
|
$form->submit($multiple ? (array) $submittedData : $submittedData);
|
||||||
|
|
||||||
// When the choice does not exist the transformation fails
|
// When the choice does not exist the transformation fails
|
||||||
$this->assertFalse($form->isSynchronized());
|
$this->assertFalse($form->isValid());
|
||||||
|
|
||||||
|
if ($multiple) {
|
||||||
|
$this->assertSame([], $form->getData());
|
||||||
|
} else {
|
||||||
$this->assertNull($form->getData());
|
$this->assertNull($form->getData());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider provideTrimCases
|
* @dataProvider provideTrimCases
|
||||||
|
Reference in New Issue
Block a user