[Form] Add support for submitting forms with unchecked checkboxes in request handlers

This commit is contained in:
“Filip
2022-01-19 16:18:06 +01:00
committed by Nicolas Grekas
parent a4113f1e30
commit ec1b37b8f0
5 changed files with 195 additions and 10 deletions

View File

@@ -4,6 +4,7 @@ CHANGELOG
8.1
---
* Add support for submitting forms with unchecked checkboxes in request handlers
* Add `ResetFlowType` button in `NavigatorFlowType` that you can display with `with_reset` option
* Allow injecting a `ViolationMapperInterface` into `FormTypeValidatorExtension`
* Deprecate passing boolean as the second argument of `ValidatorExtension` and `FormTypeValidatorExtension`'s constructors; pass a `ViolationMapperInterface` instead

View File

@@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\HttpFoundation;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\MissingDataHandler;
use Symfony\Component\Form\RequestHandlerInterface;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\Util\ServerParams;
@@ -30,10 +31,12 @@ use Symfony\Component\HttpFoundation\Request;
class HttpFoundationRequestHandler implements RequestHandlerInterface
{
private ServerParams $serverParams;
private MissingDataHandler $missingDataHandler;
public function __construct(?ServerParams $serverParams = null)
{
$this->serverParams = $serverParams ?? new ServerParams();
$this->missingDataHandler = new MissingDataHandler();
}
public function handleRequest(FormInterface $form, mixed $request = null): void
@@ -44,6 +47,7 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
$name = $form->getName();
$method = $form->getConfig()->getMethod();
$missingData = $this->missingDataHandler->missingData;
if ($method !== $request->getMethod()) {
return;
@@ -55,13 +59,15 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
if ('' === $name) {
$data = $request->query->all();
} else {
// Don't submit GET requests if the form's name does not exist
// in the request
if (!$request->query->has($name)) {
$queryData = $request->query->all()[$name] ?? $missingData;
$data = $this->missingDataHandler->handle($form, $queryData);
if ($missingData === $data) {
// Don't submit GET requests if the form's name does not exist
// in the request
return;
}
$data = $request->query->all()[$name];
}
} else {
// Mark the form with an error if the uploaded size was too large
@@ -88,6 +94,15 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
$params = $request->request->all()[$name] ?? $default;
$files = $request->files->get($name, $default);
} else {
$params = $missingData;
$files = null;
}
if ('PATCH' !== $method) {
$params = $this->missingDataHandler->handle($form, $params);
}
if ($missingData === $params) {
// Don't submit the form if it is not present in the request
return;
}

76
MissingDataHandler.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
/**
* @internal
*/
class MissingDataHandler
{
public readonly \stdClass $missingData;
public function __construct()
{
$this->missingData = new \stdClass();
}
public function handle(FormInterface $form, mixed $data): mixed
{
$processedData = $this->handleMissingData($form, $data);
return $processedData === $this->missingData ? $data : $processedData;
}
private function handleMissingData(FormInterface $form, mixed $data): mixed
{
$config = $form->getConfig();
$missingData = $this->missingData;
$falseValues = $config->getOption('false_values', null);
if (\is_array($falseValues)) {
if ($data === $missingData) {
return $falseValues[0] ?? null;
}
if (\in_array($data, $falseValues)) {
return $data;
}
}
if (null === $data || $missingData === $data) {
$data = $config->getCompound() ? [] : $data;
}
if (\is_array($data)) {
$children = $config->getCompound() ? $form->all() : [$form];
foreach ($children as $child) {
$name = $child->getName();
$childData = $missingData;
if (\array_key_exists($name, $data)) {
$childData = $data[$name];
}
$value = $this->handleMissingData($child, $childData);
if ($missingData !== $value) {
$data[$name] = $value;
}
}
return $data ?: $missingData;
}
return $data;
}
}

View File

@@ -23,6 +23,7 @@ use Symfony\Component\Form\Util\ServerParams;
class NativeRequestHandler implements RequestHandlerInterface
{
private ServerParams $serverParams;
private MissingDataHandler $missingDataHandler;
/**
* The allowed keys of the $_FILES array.
@@ -39,6 +40,7 @@ class NativeRequestHandler implements RequestHandlerInterface
public function __construct(?ServerParams $params = null)
{
$this->serverParams = $params ?? new ServerParams();
$this->missingDataHandler = new MissingDataHandler();
}
/**
@@ -52,6 +54,7 @@ class NativeRequestHandler implements RequestHandlerInterface
$name = $form->getName();
$method = $form->getConfig()->getMethod();
$missingData = $this->missingDataHandler->missingData;
if ($method !== self::getRequestMethod()) {
return;
@@ -63,13 +66,14 @@ class NativeRequestHandler implements RequestHandlerInterface
if ('' === $name) {
$data = $_GET;
} else {
// Don't submit GET requests if the form's name does not exist
// in the request
if (!isset($_GET[$name])) {
$queryData = $_GET[$name] ?? $missingData;
$data = $this->missingDataHandler->handle($form, $queryData);
if ($missingData === $data) {
// Don't submit GET requests if the form's name does not exist
// in the request
return;
}
$data = $_GET[$name];
}
} else {
// Mark the form with an error if the uploaded size was too large
@@ -101,6 +105,15 @@ class NativeRequestHandler implements RequestHandlerInterface
$params = \array_key_exists($name, $_POST) ? $_POST[$name] : $default;
$files = \array_key_exists($name, $fixedFiles) ? $fixedFiles[$name] : $default;
} else {
$params = $missingData;
$files = null;
}
if ('PATCH' !== $method) {
$params = $this->missingDataHandler->handle($form, $params);
}
if ($missingData === $params) {
// Don't submit the form if it is not present in the request
return;
}

View File

@@ -15,7 +15,9 @@ use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilder;
@@ -60,6 +62,84 @@ abstract class AbstractRequestHandlerTestCase extends TestCase
$this->request = null;
}
#[DataProvider('methodExceptPatchProvider')]
public function testSubmitCheckboxInCollectionFormWithEmptyData($method)
{
$form = $this->factory->create(CollectionType::class, [true, false, true], [
'entry_type' => CheckboxType::class,
'method' => $method,
]);
$this->setRequestData($method, []);
$this->requestHandler->handleRequest($form, $this->request);
$this->assertEqualsCanonicalizing([false, false, false], $form->getData());
}
#[DataProvider('methodExceptPatchProvider')]
public function testSubmitCheckboxInCollectionFormWithPartialData($method)
{
$form = $this->factory->create(CollectionType::class, [true, false, true], [
'entry_type' => CheckboxType::class,
'method' => $method,
]);
$this->setRequestData($method, [
'collection' => [
1 => true,
],
]);
$this->requestHandler->handleRequest($form, $this->request);
$this->assertEqualsCanonicalizing([false, true, false], $form->getData());
}
#[DataProvider('methodExceptPatchProvider')]
public function testSubmitCheckboxFormWithEmptyData($method)
{
$form = $this->factory->create(FormType::class, ['subform' => ['checkbox' => true]], [
'method' => $method,
])
->add('subform', FormType::class, [
'compound' => true,
]);
$form->get('subform')
->add('checkbox', CheckboxType::class);
$this->setRequestData($method, []);
$this->requestHandler->handleRequest($form, $this->request);
$this->assertEquals(['subform' => ['checkbox' => false]], $form->getData());
}
#[DataProvider('methodExceptPatchProvider')]
public function testSubmitSimpleCheckboxFormWithEmptyData($method)
{
$form = $this->factory->createNamed('checkbox', CheckboxType::class, true, [
'method' => $method,
]);
$this->setRequestData($method, []);
$this->requestHandler->handleRequest($form, $this->request);
$this->assertFalse($form->getData());
}
public static function methodExceptPatchProvider(): array
{
return [
['POST'],
['PUT'],
['DELETE'],
['GET'],
];
}
public static function methodExceptGetProvider(): array
{
return [