diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md index d171b6b721..026334ba53 100644 --- a/UPGRADE-5.1.md +++ b/UPGRADE-5.1.md @@ -16,6 +16,14 @@ EventDispatcher * Deprecated `LegacyEventDispatcherProxy`. Use the event dispatcher without the proxy. +Form +---- + + * Implementing the `FormConfigInterface` without implementing the `getIsEmptyCallback()` method + is deprecated. The method will be added to the interface in 6.0. + * Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method + is deprecated. The method will be added to the interface in 6.0. + FrameworkBundle --------------- diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index a8c10465b8..08243073d4 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -16,6 +16,12 @@ EventDispatcher * Removed `LegacyEventDispatcherProxy`. Use the event dispatcher without the proxy. +Form +---- + + * Added the `getIsEmptyCallback()` method to the `FormConfigInterface`. + * Added the `setIsEmptyCallback()` method to the `FormConfigBuilderInterface`. + FrameworkBundle --------------- diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php index 5a85f8bc73..87adc69475 100644 --- a/src/Symfony/Component/Form/ButtonBuilder.php +++ b/src/Symfony/Component/Form/ButtonBuilder.php @@ -466,6 +466,16 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface return $config; } + /** + * Unsupported method. + * + * @throws BadMethodCallException + */ + public function setIsEmptyCallback(?callable $isEmptyCallback) + { + throw new BadMethodCallException('Buttons do not support "is empty" callback.'); + } + /** * Unsupported method. */ @@ -738,6 +748,16 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface return \array_key_exists($name, $this->options) ? $this->options[$name] : $default; } + /** + * Unsupported method. + * + * @throws BadMethodCallException + */ + public function getIsEmptyCallback(): ?callable + { + throw new BadMethodCallException('Buttons do not support "is empty" callback.'); + } + /** * Unsupported method. * diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 82231b3c45..97ed3b791d 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -6,6 +6,10 @@ CHANGELOG * The `view_timezone` option defaults to the `model_timezone` if no `reference_date` is configured. * Added default `inputmode` attribute to Search, Email and Tel form types. + * Implementing the `FormConfigInterface` without implementing the `getIsEmptyCallback()` method + is deprecated. The method will be added to the interface in 6.0. + * Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method + is deprecated. The method will be added to the interface in 6.0. 5.0.0 ----- diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php b/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php index 2b29c5ad9b..2741a9afd4 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CheckboxType.php @@ -60,6 +60,9 @@ class CheckboxType extends AbstractType 'empty_data' => $emptyData, 'compound' => false, 'false_values' => [null], + 'is_empty_callback' => static function ($modelData): bool { + return false === $modelData; + }, ]); $resolver->setAllowedTypes('false_values', 'array'); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index 1430261745..735fe4ea6d 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Extension\Core\EventListener\TrimListener; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormConfigBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\Options; @@ -58,6 +59,14 @@ class FormType extends BaseType if ($options['trim']) { $builder->addEventSubscriber(new TrimListener()); } + + if (!method_exists($builder, 'setIsEmptyCallback')) { + @trigger_error(sprintf('Not implementing the "%s::setIsEmptyCallback()" method in "%s" is deprecated since Symfony 5.1.', FormConfigBuilderInterface::class, \get_class($builder)), E_USER_DEPRECATED); + + return; + } + + $builder->setIsEmptyCallback($options['is_empty_callback']); } /** @@ -190,6 +199,7 @@ class FormType extends BaseType 'help_attr' => [], 'help_html' => false, 'help_translation_parameters' => [], + 'is_empty_callback' => null, ]); $resolver->setAllowedTypes('label_attr', 'array'); @@ -197,6 +207,7 @@ class FormType extends BaseType $resolver->setAllowedTypes('help', ['string', 'null']); $resolver->setAllowedTypes('help_attr', 'array'); $resolver->setAllowedTypes('help_html', 'bool'); + $resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']); } /** diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 823ea9a191..3e566fd201 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -726,6 +726,18 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac } } + if (!method_exists($this->config, 'getIsEmptyCallback')) { + @trigger_error(sprintf('Not implementing the "%s::getIsEmptyCallback()" method in "%s" is deprecated since Symfony 5.1.', FormConfigInterface::class, \get_class($this->config)), E_USER_DEPRECATED); + + $isEmptyCallback = null; + } else { + $isEmptyCallback = $this->config->getIsEmptyCallback(); + } + + if (null !== $isEmptyCallback) { + return $isEmptyCallback($this->modelData); + } + return FormUtil::isEmpty($this->modelData) || // arrays, countables ((\is_array($this->modelData) || $this->modelData instanceof \Countable) && 0 === \count($this->modelData)) || diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php index 19d1e586d1..7e7b40edac 100644 --- a/src/Symfony/Component/Form/FormConfigBuilder.php +++ b/src/Symfony/Component/Form/FormConfigBuilder.php @@ -102,6 +102,7 @@ class FormConfigBuilder implements FormConfigBuilderInterface private $autoInitialize = false; private $options; + private $isEmptyCallback; /** * Creates an empty form configuration. @@ -461,6 +462,14 @@ class FormConfigBuilder implements FormConfigBuilderInterface return \array_key_exists($name, $this->options) ? $this->options[$name] : $default; } + /** + * {@inheritdoc} + */ + public function getIsEmptyCallback(): ?callable + { + return $this->isEmptyCallback; + } + /** * {@inheritdoc} */ @@ -761,6 +770,16 @@ class FormConfigBuilder implements FormConfigBuilderInterface return $config; } + /** + * {@inheritdoc} + */ + public function setIsEmptyCallback(?callable $isEmptyCallback) + { + $this->isEmptyCallback = $isEmptyCallback; + + return $this; + } + /** * Validates whether the given variable is a valid form name. * diff --git a/src/Symfony/Component/Form/FormConfigBuilderInterface.php b/src/Symfony/Component/Form/FormConfigBuilderInterface.php index d1b30cebbf..d9064c1434 100644 --- a/src/Symfony/Component/Form/FormConfigBuilderInterface.php +++ b/src/Symfony/Component/Form/FormConfigBuilderInterface.php @@ -16,6 +16,8 @@ use Symfony\Component\PropertyAccess\PropertyPathInterface; /** * @author Bernhard Schussek + * + * @method $this setIsEmptyCallback(callable|null $isEmptyCallback) Sets the callback that will be called to determine if the model data of the form is empty or not - not implementing it is deprecated since Symfony 5.1 */ interface FormConfigBuilderInterface extends FormConfigInterface { diff --git a/src/Symfony/Component/Form/FormConfigInterface.php b/src/Symfony/Component/Form/FormConfigInterface.php index 3671164270..e76986c4fb 100644 --- a/src/Symfony/Component/Form/FormConfigInterface.php +++ b/src/Symfony/Component/Form/FormConfigInterface.php @@ -18,6 +18,8 @@ use Symfony\Component\PropertyAccess\PropertyPathInterface; * The configuration of a {@link Form} object. * * @author Bernhard Schussek + * + * @method callable|null getIsEmptyCallback() Returns a callable that takes the model data as argument and that returns if it is empty or not - not implementing it is deprecated since Symfony 5.1 */ interface FormConfigInterface { 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 f417c234af..b061786169 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -2049,4 +2049,45 @@ class ChoiceTypeTest extends BaseTypeTest '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 + ]; + } } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json index 6b1204c6b8..e02e667318 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json @@ -42,6 +42,7 @@ "help_html", "help_translation_parameters", "inherit_data", + "is_empty_callback", "label", "label_attr", "label_format", diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt index 6c6d38628d..bc56245a99 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt @@ -22,6 +22,7 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") help_html help_translation_parameters inherit_data + is_empty_callback label label_attr label_format diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.json index 5eaf65b863..9d1058b588 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.json +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.json @@ -22,6 +22,7 @@ "help_html", "help_translation_parameters", "inherit_data", + "is_empty_callback", "label", "label_attr", "label_format", diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.txt index 2007781f2d..e8f9b2660c 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.txt @@ -24,6 +24,7 @@ Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form") help_html help_translation_parameters inherit_data + is_empty_callback label label_attr label_format diff --git a/src/Symfony/Component/Form/Tests/SimpleFormTest.php b/src/Symfony/Component/Form/Tests/SimpleFormTest.php index 949885222e..bd71eebfc8 100644 --- a/src/Symfony/Component/Form/Tests/SimpleFormTest.php +++ b/src/Symfony/Component/Form/Tests/SimpleFormTest.php @@ -1097,6 +1097,21 @@ class SimpleFormTest extends AbstractFormTest $form->setData('foo'); } + public function testIsEmptyCallback() + { + $config = new FormConfigBuilder('foo', null, $this->dispatcher); + + $config->setIsEmptyCallback(function ($modelData): bool { return 'ccc' === $modelData; }); + $form = new Form($config); + $form->setData('ccc'); + $this->assertTrue($form->isEmpty()); + + $config->setIsEmptyCallback(function (): bool { return false; }); + $form = new Form($config); + $form->setData(null); + $this->assertFalse($form->isEmpty()); + } + protected function createForm(): FormInterface { return $this->getBuilder()->getForm();