Skip to content

Commit 88b2da3

Browse files
feature #32747 [Form] Add "is empty callback" to form config (fancyweb)
This PR was merged into the 5.1-dev branch. Discussion ---------- [Form] Add "is empty callback" to form config | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | symfony/symfony#31572 for 4.4+ | License | MIT | Doc PR | - This PR introduces a new feature that allow to resolve a bug. Currently, the `isEmpty()` behavior of the `Form` class is the same whatever its configuration. That prevents us to specify a different behavior by form type. But I think that some form types should have dedicated empty values. For example, the `CheckboxType` model data either resolves to `true` (checked) or `false` (unchecked). But `false` is not an empty value in the `Form::isEmpty()` method, so a `CheckboxType` form can never be empty. `false` should not be in that list because for other form types, it's perfectly fine that it's not considered as an empty value. The problem is better seen in symfony/symfony#31572 with a `ChoiceType` that is never considered as empty (when no radio button is checked). Being able to specify the "is empty" behavior by form type would also allow users to define their own logic in their custom form types + probably define it ourselves in all our form types in order to get rid of the default common behavior. Commits ------- 7bfc27e7cf [Form] Add "is empty callback" to form config
2 parents c209864 + 2085474 commit 88b2da3

14 files changed

+133
-0
lines changed

ButtonBuilder.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,16 @@ public function getFormConfig()
466466
return $config;
467467
}
468468

469+
/**
470+
* Unsupported method.
471+
*
472+
* @throws BadMethodCallException
473+
*/
474+
public function setIsEmptyCallback(?callable $isEmptyCallback)
475+
{
476+
throw new BadMethodCallException('Buttons do not support "is empty" callback.');
477+
}
478+
469479
/**
470480
* Unsupported method.
471481
*/
@@ -738,6 +748,16 @@ public function getOption(string $name, $default = null)
738748
return \array_key_exists($name, $this->options) ? $this->options[$name] : $default;
739749
}
740750

751+
/**
752+
* Unsupported method.
753+
*
754+
* @throws BadMethodCallException
755+
*/
756+
public function getIsEmptyCallback(): ?callable
757+
{
758+
throw new BadMethodCallException('Buttons do not support "is empty" callback.');
759+
}
760+
741761
/**
742762
* Unsupported method.
743763
*

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ CHANGELOG
66

77
* The `view_timezone` option defaults to the `model_timezone` if no `reference_date` is configured.
88
* Added default `inputmode` attribute to Search, Email and Tel form types.
9+
* Implementing the `FormConfigInterface` without implementing the `getIsEmptyCallback()` method
10+
is deprecated. The method will be added to the interface in 6.0.
11+
* Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method
12+
is deprecated. The method will be added to the interface in 6.0.
913

1014
5.0.0
1115
-----

Extension/Core/Type/CheckboxType.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ public function configureOptions(OptionsResolver $resolver)
6060
'empty_data' => $emptyData,
6161
'compound' => false,
6262
'false_values' => [null],
63+
'is_empty_callback' => static function ($modelData): bool {
64+
return false === $modelData;
65+
},
6366
]);
6467

6568
$resolver->setAllowedTypes('false_values', 'array');

Extension/Core/Type/FormType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
1616
use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
1717
use Symfony\Component\Form\FormBuilderInterface;
18+
use Symfony\Component\Form\FormConfigBuilderInterface;
1819
use Symfony\Component\Form\FormInterface;
1920
use Symfony\Component\Form\FormView;
2021
use Symfony\Component\OptionsResolver\Options;
@@ -58,6 +59,14 @@ public function buildForm(FormBuilderInterface $builder, array $options)
5859
if ($options['trim']) {
5960
$builder->addEventSubscriber(new TrimListener());
6061
}
62+
63+
if (!method_exists($builder, 'setIsEmptyCallback')) {
64+
@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);
65+
66+
return;
67+
}
68+
69+
$builder->setIsEmptyCallback($options['is_empty_callback']);
6170
}
6271

6372
/**
@@ -190,13 +199,15 @@ public function configureOptions(OptionsResolver $resolver)
190199
'help_attr' => [],
191200
'help_html' => false,
192201
'help_translation_parameters' => [],
202+
'is_empty_callback' => null,
193203
]);
194204

195205
$resolver->setAllowedTypes('label_attr', 'array');
196206
$resolver->setAllowedTypes('upload_max_size_message', ['callable']);
197207
$resolver->setAllowedTypes('help', ['string', 'null']);
198208
$resolver->setAllowedTypes('help_attr', 'array');
199209
$resolver->setAllowedTypes('help_html', 'bool');
210+
$resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']);
200211
}
201212

202213
/**

Form.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,18 @@ public function isEmpty()
726726
}
727727
}
728728

729+
if (!method_exists($this->config, 'getIsEmptyCallback')) {
730+
@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);
731+
732+
$isEmptyCallback = null;
733+
} else {
734+
$isEmptyCallback = $this->config->getIsEmptyCallback();
735+
}
736+
737+
if (null !== $isEmptyCallback) {
738+
return $isEmptyCallback($this->modelData);
739+
}
740+
729741
return FormUtil::isEmpty($this->modelData) ||
730742
// arrays, countables
731743
((\is_array($this->modelData) || $this->modelData instanceof \Countable) && 0 === \count($this->modelData)) ||

FormConfigBuilder.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class FormConfigBuilder implements FormConfigBuilderInterface
102102

103103
private $autoInitialize = false;
104104
private $options;
105+
private $isEmptyCallback;
105106

106107
/**
107108
* Creates an empty form configuration.
@@ -461,6 +462,14 @@ public function getOption(string $name, $default = null)
461462
return \array_key_exists($name, $this->options) ? $this->options[$name] : $default;
462463
}
463464

465+
/**
466+
* {@inheritdoc}
467+
*/
468+
public function getIsEmptyCallback(): ?callable
469+
{
470+
return $this->isEmptyCallback;
471+
}
472+
464473
/**
465474
* {@inheritdoc}
466475
*/
@@ -761,6 +770,16 @@ public function getFormConfig()
761770
return $config;
762771
}
763772

773+
/**
774+
* {@inheritdoc}
775+
*/
776+
public function setIsEmptyCallback(?callable $isEmptyCallback)
777+
{
778+
$this->isEmptyCallback = $isEmptyCallback;
779+
780+
return $this;
781+
}
782+
764783
/**
765784
* Validates whether the given variable is a valid form name.
766785
*

FormConfigBuilderInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
/**
1818
* @author Bernhard Schussek <[email protected]>
19+
*
20+
* @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
1921
*/
2022
interface FormConfigBuilderInterface extends FormConfigInterface
2123
{

FormConfigInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
* The configuration of a {@link Form} object.
1919
*
2020
* @author Bernhard Schussek <[email protected]>
21+
*
22+
* @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
2123
*/
2224
interface FormConfigInterface
2325
{

Tests/Extension/Core/Type/ChoiceTypeTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,4 +2049,45 @@ public function provideTrimCases()
20492049
'Multiple expanded' => [true, true],
20502050
];
20512051
}
2052+
2053+
/**
2054+
* @dataProvider expandedIsEmptyWhenNoRealChoiceIsSelectedProvider
2055+
*/
2056+
public function testExpandedIsEmptyWhenNoRealChoiceIsSelected(bool $expected, $submittedData, bool $multiple, bool $required, $placeholder)
2057+
{
2058+
$options = [
2059+
'expanded' => true,
2060+
'choices' => [
2061+
'foo' => 'bar',
2062+
],
2063+
'multiple' => $multiple,
2064+
'required' => $required,
2065+
];
2066+
2067+
if (!$multiple) {
2068+
$options['placeholder'] = $placeholder;
2069+
}
2070+
2071+
$form = $this->factory->create(static::TESTED_TYPE, null, $options);
2072+
2073+
$form->submit($submittedData);
2074+
2075+
$this->assertSame($expected, $form->isEmpty());
2076+
}
2077+
2078+
public function expandedIsEmptyWhenNoRealChoiceIsSelectedProvider()
2079+
{
2080+
// Some invalid cases are voluntarily not tested:
2081+
// - multiple with placeholder
2082+
// - required with placeholder
2083+
return [
2084+
'Nothing submitted / single / not required / without a placeholder -> should be empty' => [true, null, false, false, null],
2085+
'Nothing submitted / single / not required / with a placeholder -> should not be empty' => [false, null, false, false, 'ccc'], // It falls back on the placeholder
2086+
'Nothing submitted / single / required / without a placeholder -> should be empty' => [true, null, false, true, null],
2087+
'Nothing submitted / single / required / with a placeholder -> should be empty' => [true, null, false, true, 'ccc'],
2088+
'Nothing submitted / multiple / not required / without a placeholder -> should be empty' => [true, null, true, false, null],
2089+
'Nothing submitted / multiple / required / without a placeholder -> should be empty' => [true, null, true, true, null],
2090+
'Placeholder submitted / single / not required / with a placeholder -> should not be empty' => [false, '', false, false, 'ccc'], // The placeholder is a selected value
2091+
];
2092+
}
20522093
}

Tests/Fixtures/Descriptor/resolved_form_type_1.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"help_html",
4343
"help_translation_parameters",
4444
"inherit_data",
45+
"is_empty_callback",
4546
"label",
4647
"label_attr",
4748
"label_format",

Tests/Fixtures/Descriptor/resolved_form_type_1.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice")
2222
help_html
2323
help_translation_parameters
2424
inherit_data
25+
is_empty_callback
2526
label
2627
label_attr
2728
label_format

Tests/Fixtures/Descriptor/resolved_form_type_2.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"help_html",
2323
"help_translation_parameters",
2424
"inherit_data",
25+
"is_empty_callback",
2526
"label",
2627
"label_attr",
2728
"label_format",

Tests/Fixtures/Descriptor/resolved_form_type_2.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form")
2424
help_html
2525
help_translation_parameters
2626
inherit_data
27+
is_empty_callback
2728
label
2829
label_attr
2930
label_format

Tests/SimpleFormTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,21 @@ public function testCannotCallGetViewDataInPreSetDataListener()
10971097
$form->setData('foo');
10981098
}
10991099

1100+
public function testIsEmptyCallback()
1101+
{
1102+
$config = new FormConfigBuilder('foo', null, $this->dispatcher);
1103+
1104+
$config->setIsEmptyCallback(function ($modelData): bool { return 'ccc' === $modelData; });
1105+
$form = new Form($config);
1106+
$form->setData('ccc');
1107+
$this->assertTrue($form->isEmpty());
1108+
1109+
$config->setIsEmptyCallback(function (): bool { return false; });
1110+
$form = new Form($config);
1111+
$form->setData(null);
1112+
$this->assertFalse($form->isEmpty());
1113+
}
1114+
11001115
protected function createForm(): FormInterface
11011116
{
11021117
return $this->getBuilder()->getForm();

0 commit comments

Comments
 (0)