mirror of
https://github.com/symfony/form.git
synced 2026-03-24 00:02:23 +01:00
[Form] Add support for submitting forms with unchecked checkboxes in request handlers
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
76
MissingDataHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user