8000 [Form] keep valid submitted choices when additional choices are submitted by xabbuh · Pull Request #39659 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content
8000

[Form] keep valid submitted choices when additional choices are submitted #39659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
78 changes: 62 additions & 16 deletions src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,45 @@
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;
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;
}

/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$unknownValues = [];
$choiceList = $this->createChoiceList($options);
$builder->setAttribute('choice_list', $choiceList);

Expand Down Expand Up @@ -81,10 +94,12 @@ public function buildForm(FormBuilderInterface $builder, array $options)

$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();

Expand All @@ -99,6 +114,10 @@ public function buildForm(FormBuilderInterface $builder, array $options)
// 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;
}

Expand All @@ -110,34 +129,61 @@ public function buildForm(FormBuilderInterface $builder, array $options)
$unknownValues = $valueMap;

// Reconstruct the data as mapping from child names to values
$data = [];

/** @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;
$knownValues = [];

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])) {
$knownValues[$child->getName()] = $value;
unset($unknownValues[$value]);
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
// 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,9 +806,9 @@ public function testSubmitMultipleNonExpandedInvalidArrayChoice()

$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()
Expand Down Expand Up @@ -1349,17 +1349,17 @@ public function testSubmitMultipleExpandedInvalidArrayChoice()

$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());
Expand Down Expand Up @@ -2033,8 +2033,13 @@ public function testTrimIsDisabled($multiple, $expanded)
$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 {
4B79 $this->assertNull($form->getData());
}
}

/**
Expand Down
0