diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php b/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php index 19ff62c154a04..dd59185cf1dd3 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php @@ -33,6 +33,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) // doing so also calls setDataLocked(true). $builder->setData(isset($options['data']) ? $options['data'] : false); $builder->addViewTransformer(new BooleanToStringTransformer($options['value'])); + $builder->setAttribute('false_is_empty', true); } /** diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 67fd234fe9612..27b117b613a96 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -725,7 +725,9 @@ public function isEmpty() // arrays, countables ((\is_array($this->modelData) || $this->modelData instanceof \Countable) && 0 === \count($this->modelData)) || // traversables that are not countable - ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData)); + ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData)) || + // @internal - Do not rely on it, it will be removed in Symfony 5.1. + (false === $this->modelData && $this->config->getAttribute('false_is_empty')); } /** diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index b81ecb87671ab..573557fff7780 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -2046,4 +2046,45 @@ public function provideTrimCases() 'Multiple expanded' => [true, true], ]; } + + /** + * @dataProvider expandedIsEmptyWhenNoRealChoiceIsSelectedProvider + */ + public function testExpandedIsEmptyWhenNoRealChoiceIsSelected(bool $expected, $submittedData, bool $multiple, bool $required, $placeholder) + { + $options = [ + 'expanded' => true, + 'choices' => [ + 'foo' => 'bar', + ], + 'multiple' => $multiple, + 'required' => $required, + ]; + + if (!$multiple) { + $options['placeholder'] = $placeholder; + } + + $form = $this->factory->create(static::TESTED_TYPE, null, $options); + + $form->submit($submittedData); + + $this->assertSame($expected, $form->isEmpty()); + } + + public function expandedIsEmptyWhenNoRealChoiceIsSelectedProvider() + { + // Some invalid cases are voluntarily not tested: + // - multiple with placeholder + // - required with placeholder + return [ + 'Nothing submitted / single / not required / without a placeholder -> should be empty' => [true, null, false, false, null], + 'Nothing submitted / single / not required / with a placeholder -> should not be empty' => [false, null, false, false, 'ccc'], // It falls back on the placeholder + 'Nothing submitted / single / required / without a placeholder -> should be empty' => [true, null, false, true, null], + 'Nothing submitted / single / required / with a placeholder -> should be empty' => [true, null, false, true, 'ccc'], + 'Nothing submitted / multiple / not required / without a placeholder -> should be empty' => [true, null, true, false, null], + 'Nothing submitted / multiple / required / without a placeholder -> should be empty' => [true, null, true, true, null], + 'Placeholder submitted / single / not required / with a placeholder -> should not be empty' => [false, '', false, false, 'ccc'], // The placeholder is a selected value + ]; + } }