Skip to content

Commit adb9016

Browse files
filiplikavcannicolas-grekas
authored andcommitted
[Form] Missing Data Handling (checkbox)
1 parent 9f5238d commit adb9016

File tree

4 files changed

+192
-10
lines changed

4 files changed

+192
-10
lines changed

src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Form\Exception\UnexpectedTypeException;
1515
use Symfony\Component\Form\FormError;
1616
use Symfony\Component\Form\FormInterface;
17+
use Symfony\Component\Form\MissingDataHandler;
1718
use Symfony\Component\Form\RequestHandlerInterface;
1819
use Symfony\Component\Form\Util\ServerParams;
1920
use Symfony\Component\HttpFoundation\File\File;
@@ -29,10 +30,12 @@
2930
class HttpFoundationRequestHandler implements RequestHandlerInterface
3031
{
3132
private $serverParams;
33+
private MissingDataHandler $missingDataHandler;
3234

3335
public function __construct(ServerParams $serverParams = null)
3436
{
3537
$this->serverParams = $serverParams ?? new ServerParams();
38+
$this->missingDataHandler = new MissingDataHandler();
3639
}
3740

3841
/**
@@ -57,13 +60,13 @@ public function handleRequest(FormInterface $form, $request = null)
5760
if ('' === $name) {
5861
$data = $request->query->all();
5962
} else {
60-
// Don't submit GET requests if the form's name does not exist
61-
// in the request
62-
if (!$request->query->has($name)) {
63+
$missingData = $this->missingDataHandler->missingData;
64+
65+
if ($missingData === $data = $request->query->get($name) ?? $missingData) {
66+
// Don't submit GET requests if the form's name does not exist
67+
// in the request
6368
return;
6469
}
65-
66-
$data = $request->query->get($name);
6770
}
6871
} else {
6972
// Mark the form with an error if the uploaded size was too large
@@ -90,6 +93,15 @@ public function handleRequest(FormInterface $form, $request = null)
9093
$params = $request->request->get($name, $default);
9194
$files = $request->files->get($name, $default);
9295
} else {
96+
$params = $this->missingDataHandler->missingData;
97+
$files = null;
98+
}
99+
100+
if ('PATCH' !== $method) {
101+
$params = $this->missingDataHandler->handle($form, $params);
102+
}
103+
104+
if ($this->missingDataHandler->missingData === $params) {
93105
// Don't submit the form if it is not present in the request
94106
return;
95107
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form;
13+
14+
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
15+
use Symfony\Component\Form\FormInterface;
16+
use Symfony\Component\Form\ResolvedFormTypeInterface;
17+
18+
class MissingDataHandler
19+
{
20+
public readonly \stdClass $missingData;
21+
22+
public function __construct()
23+
{
24+
$this->missingData = new \stdClass();
25+
}
26+
27+
public function handle(FormInterface $form, mixed $data): mixed
28+
{
29+
$processedData = $this->handleMissingData($form, $data);
30+
31+
return $processedData === $this->missingData ? $data : $processedData;
32+
}
33+
34+
private function handleMissingData(FormInterface $form, mixed $data): mixed
35+
{
36+
if ($form->getConfig()->getType() instanceof ResolvedFormTypeInterface && $form->getConfig()->getType()->getInnerType() instanceof CheckboxType) {
37+
$falseValues = $form->getConfig()->getOption('false_values');
38+
39+
if ($data === $this->missingData) {
40+
return $falseValues[0];
41+
}
42+
43+
if (\in_array($data, $falseValues)) {
44+
return $data;
45+
}
46+
}
47+
48+
if (null === $data || $this->missingData === $data) {
49+
$data = $form->getConfig()->getCompound() ? [] : $data;
50+
}
51+
52+
if (\is_array($data)) {
53+
$children = $form->getConfig()->getCompound() ? $form->all() : [$form];
54+
55+
foreach ($children as $child) {
56+
$value = $this->handleMissingData($child, \array_key_exists($child->getName(), $data) ? $data[$child->getName()] : $this->missingData);
57+
58+
if ($this->missingData !== $value) {
59+
$data[$child->getName()] = $value;
60+
}
61+
}
62+
63+
return $data ?: $this->missingData;
64+
}
65+
66+
return $data;
67+
}
68+
}

src/Symfony/Component/Form/NativeRequestHandler.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Form;
1313

1414
use Symfony\Component\Form\Exception\UnexpectedTypeException;
15+
use Symfony\Component\Form\MissingDataHandler;
1516
use Symfony\Component\Form\Util\ServerParams;
1617

1718
/**
@@ -22,6 +23,7 @@
2223
class NativeRequestHandler implements RequestHandlerInterface
2324
{
2425
private $serverParams;
26+
private MissingDataHandler $missingDataHandler;
2527

2628
/**
2729
* The allowed keys of the $_FILES array.
@@ -37,6 +39,7 @@ class NativeRequestHandler implements RequestHandlerInterface
3739
public function __construct(ServerParams $params = null)
3840
{
3941
$this->serverParams = $params ?? new ServerParams();
42+
$this->missingDataHandler = new MissingDataHandler();
4043
}
4144

4245
/**
@@ -63,13 +66,13 @@ public function handleRequest(FormInterface $form, $request = null)
6366
if ('' === $name) {
6467
$data = $_GET;
6568
} else {
66-
// Don't submit GET requests if the form's name does not exist
67-
// in the request
68-
if (!isset($_GET[$name])) {
69+
$missingData = $this->missingDataHandler->missingData;
70+
71+
if ($missingData === $data = $this->missingDataHandler->handle($form, $_GET[$name] ?? $missingData)) {
72+
// Don't submit GET requests if the form's name does not exist
73+
// in the request
6974
return;
7075
}
71-
72-
$data = $_GET[$name];
7376
}
7477
} else {
7578
// Mark the form with an error if the uploaded size was too large
@@ -101,6 +104,15 @@ public function handleRequest(FormInterface $form, $request = null)
101104
$params = \array_key_exists($name, $_POST) ? $_POST[$name] : $default;
102105
$files = \array_key_exists($name, $fixedFiles) ? $fixedFiles[$name] : $default;
103106
} else {
107+
$params = $this->missingDataHandler->missingData;
108+
$files = null;
109+
}
110+
111+
if ('PATCH' !== $method) {
112+
$params = $this->missingDataHandler->handle($form, $params);
113+
}
114+
115+
if ($this->missingDataHandler->missingData === $params) {
104116
// Don't submit the form if it is not present in the request
105117
return;
106118
}

src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\EventDispatcher\EventDispatcher;
1616
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
17+
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
18+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
19+
use Symfony\Component\Form\Extension\Core\Type\FormType;
1720
use Symfony\Component\Form\Form;
1821
use Symfony\Component\Form\FormBuilder;
1922
use Symfony\Component\Form\FormError;
@@ -64,6 +67,93 @@ public function getNormalizedIniPostMaxSize(): string
6467
$this->request = null;
6568
}
6669

70+
/**
71+
* @dataProvider methodExceptPatchProvider
72+
*/
73+
public function testSubmitCheckboxInCollectionFormWithEmptyData($method)
74+
{
75+
$form = $this->factory->create(CollectionType::class, [true, false, true], [
76+
'entry_type' => CheckboxType::class,
77+
'method' => $method,
78+
]);
79+
80+
$this->setRequestData($method, [
81+
]);
82+
83+
$this->requestHandler->handleRequest($form, $this->request);
84+
85+
$this->assertEqualsCanonicalizing([false, false, false], $form->getData());
86+
}
87+
88+
/**
89+
* @dataProvider methodExceptPatchProvider
90+
*/
91+
public function testSubmitCheckboxInCollectionFormWithPartialData($method)
92+
{
93+
$form = $this->factory->create(CollectionType::class, [true, false, true], [
94+
'entry_type' => CheckboxType::class,
95+
'method' => $method,
96+
]);
97+
98+
$this->setRequestData($method, [
99+
'collection' => [
100+
1 => true,
101+
],
102+
]);
103+
104+
$this->requestHandler->handleRequest($form, $this->request);
105+
106+
$this->assertEqualsCanonicalizing([false, true, false], $form->getData());
107+
}
108+
109+
/**
110+
* @dataProvider methodExceptPatchProvider
111+
*/
112+
public function testSubmitCheckboxFormWithEmptyData($method)
113+
{
114+
$form = $this->factory->create(FormType::class, ['subform' => ['checkbox' => true]], [
115+
'method' => $method,
116+
])
117+
->add('subform', FormType::class, [
118+
'compound' => true,
119+
]);
120+
121+
$form->get('subform')
122+
->add('checkbox', CheckboxType::class);
123+
124+
$this->setRequestData($method, []);
125+
126+
$this->requestHandler->handleRequest($form, $this->request);
127+
128+
$this->assertEquals(['subform' => ['checkbox' => false]], $form->getData());
129+
}
130+
131+
/**
132+
* @dataProvider methodExceptPatchProvider
133+
*/
134+
public function testSubmitSimpleCheckboxFormWithEmptyData($method)
135+
{
136+
$form = $this->factory->createNamed('checkbox', CheckboxType::class, true, [
137+
'method' => $method,
138+
]);
139+
140+
$this->setRequestData($method, []);
141+
142+
$this->requestHandler->handleRequest($form, $this->request);
143+
144+
$this->assertFalse($form->getData());
145+
}
146+
147+
public function methodExceptPatchProvider()
148+
{
149+
return [
150+
['POST'],
151+
['PUT'],
152+
['DELETE'],
153+
['GET'],
154+
];
155+
}
156+
67157
public function methodExceptGetProvider()
68158
{
69159
return [

0 commit comments

Comments
 (0)