Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5c20295

Browse filesBrowse files
Jean-Berufabpot
authored andcommitted
[HttpKernel] allow boolean argument support for MapQueryString
1 parent 9d6ee30 commit 5c20295
Copy full SHA for 5c20295

File tree

10 files changed

+209
-10
lines changed
Filter options

10 files changed

+209
-10
lines changed

‎src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ private function mapQueryString(Request $request, string $type, MapQueryString $
166166
return null;
167167
}
168168

169-
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE);
169+
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
170170
}
171171

172172
private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object
@@ -180,7 +180,7 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
180180
}
181181

182182
if ($data = $request->request->all()) {
183-
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE);
183+
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
184184
}
185185

186186
if ('' === $data = $request->getContent()) {

‎src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
+89Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,88 @@ public function testRequestPayloadValidationErrorCustomStatusCode()
722722
$this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage());
723723
}
724724
}
725+
726+
/**
727+
* @dataProvider provideBoolArgument
728+
*/
729+
public function testBoolArgumentInQueryString(mixed $expectedValue, ?string $parameterValue)
730+
{
731+
$serializer = new Serializer([new ObjectNormalizer()]);
732+
$validator = $this->createMock(ValidatorInterface::class);
733+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
734+
735+
$argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [
736+
MapQueryString::class => new MapQueryString(),
737+
]);
738+
$request = Request::create('/', 'GET', ['value' => $parameterValue]);
739+
740+
$kernel = $this->createMock(HttpKernelInterface::class);
741+
$arguments = $resolver->resolve($request, $argument);
742+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
743+
744+
$resolver->onKernelControllerArguments($event);
745+
746+
$this->assertSame($expectedValue, $event->getArguments()[0]->value);
747+
}
748+
749+
/**
750+
* @dataProvider provideBoolArgument
751+
*/
752+
public function testBoolArgumentInBody(mixed $expectedValue, ?string $parameterValue)
753+
{
754+
$serializer = new Serializer([new ObjectNormalizer()]);
755+
$validator = $this->createMock(ValidatorInterface::class);
756+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
757+
758+
$argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [
759+
MapRequestPayload::class => new MapRequestPayload(),
760+
]);
761+
$request = Request::create('/', 'POST', ['value' => $parameterValue], server: ['CONTENT_TYPE' => 'multipart/form-data']);
762+
763+
$kernel = $this->createMock(HttpKernelInterface::class);
764+
$arguments = $resolver->resolve($request, $argument);
765+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
766+
767+
$resolver->onKernelControllerArguments($event);
768+
769+
$this->assertSame($expectedValue, $event->getArguments()[0]->value);
770+
}
771+
772+
public static function provideBoolArgument()
773+
{
774+
yield 'default value' => [null, null];
775+
yield '"0"' => [false, '0'];
776+
yield '"false"' => [false, 'false'];
777+
yield '"no"' => [false, 'no'];
778+
yield '"off"' => [false, 'off'];
779+
yield '"1"' => [true, '1'];
780+
yield '"true"' => [true, 'true'];
781+
yield '"yes"' => [true, 'yes'];
782+
yield '"on"' => [true, 'on'];
783+
}
784+
785+
/**
786+
* Boolean filtering must be disabled for content types other than form data.
787+
*/
788+
public function testBoolArgumentInJsonBody()
789+
{
790+
$serializer = new Serializer([new ObjectNormalizer()]);
791+
$validator = $this->createMock(ValidatorInterface::class);
792+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
793+
794+
$argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [
795+
MapRequestPayload::class => new MapRequestPayload(),
796+
]);
797+
$request = Request::create('/', 'POST', ['value' => 'off'], server: ['CONTENT_TYPE' => 'application/json']);
798+
799+
$kernel = $this->createMock(HttpKernelInterface::class);
800+
$arguments = $resolver->resolve($request, $argument);
801+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
802+
803+
$resolver->onKernelControllerArguments($event);
804+
805+
$this->assertTrue($event->getArguments()[0]->value);
806+
}
725807
}
726808

727809
class RequestPayload
@@ -765,3 +847,10 @@ public function getPassword(): string
765847
return $this->password;
766848
}
767849
}
850+
851+
class ObjectWithBoolArgument
852+
{
853+
public function __construct(public readonly ?bool $value = null)
854+
{
855+
}
856+
}

‎src/Symfony/Component/Serializer/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `DateTimeNormalizer::CAST_KEY` context option
88
* Add `Default` and "class name" default groups
9+
* Add `AbstractNormalizer::FILTER_BOOL` context option
910

1011
7.0
1112
---

‎src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
+27-7Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
118118
*/
119119
public const REQUIRE_ALL_PROPERTIES = 'require_all_properties';
120120

121+
/**
122+
* Flag to control whether a non-boolean value should be filtered using the
123+
* filter_var function with the {@see https://www.php.net/manual/fr/filter.filters.validate.php}
124+
* \FILTER_VALIDATE_BOOL filter before casting it to a boolean.
125+
*
126+
* "0", "false", "off", "no" and "" will be cast to false.
127+
* "1", "true", "on" and "yes" will be cast to true.
128+
*/
129+
public const FILTER_BOOL = 'filter_bool';
130+
121131
/**
122132
* @internal
123133
*/
@@ -436,12 +446,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
436446
unset($context['has_constructor']);
437447

438448
if (!$reflectionClass->isInstantiable()) {
439-
throw NotNormalizableValueException::createForUnexpectedDataType(
440-
sprintf('Failed to create object because the class "%s" is not instantiable.', $class),
441-
$data,
442-
['unknown'],
443-
$context['deserialization_path'] ?? null
444-
);
449+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Failed to create object because the class "%s" is not instantiable.', $class), $data, ['unknown'], $context['deserialization_path'] ?? null);
445450
}
446451

447452
return new $class();
@@ -473,7 +478,9 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara
473478
return null;
474479
}
475480

476-
return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
481+
$parameterData = $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
482+
483+
return $this->applyFilterBool($parameter, $parameterData, $context);
477484
}
478485

479486
/**
@@ -524,6 +531,19 @@ final protected function applyCallbacks(mixed $value, object|string $object, str
524531
return $callback ? $callback($value, $object, $attribute, $format, $context) : $value;
525532
}
526533

534+
final protected function applyFilterBool(\ReflectionParameter $parameter, mixed $value, array $context): mixed
535+
{
536+
if (!($context[self::FILTER_BOOL] ?? false)) {
537+
return $value;
538+
}
539+
540+
if (!($parameterType = $parameter->getType()) instanceof \ReflectionNamedType || 'bool' !== $parameterType->getName()) {
541+
return $value;
542+
}
543+
544+
return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) ?? $value;
545+
}
546+
527547
/**
528548
* Computes the normalization context merged with current one. Metadata always wins over global context, as more specific.
529549
*

‎src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,9 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara
601601

602602
$parameterData = $this->validateAndDenormalize($types, $class->getName(), $parameterName, $parameterData, $format, $context);
603603

604-
return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
604+
$parameterData = $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
605+
606+
return $this->applyFilterBool($parameter, $parameterData, $context);
605607
}
606608

607609
/**
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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\Serializer\Tests\Normalizer\Features;
13+
14+
class FilterBoolObject
15+
{
16+
public function __construct(public ?bool $value)
17+
{
18+
}
19+
}
+47Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Serializer\Tests\Normalizer\Features;
13+
14+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
15+
16+
/**
17+
* Test AbstractNormalizer::FILTER_BOOL.
18+
*/
19+
trait FilterBoolTestTrait
20+
{
21+
abstract protected function getNormalizerForFilterBool(): DenormalizerInterface;
22+
23+
/**
24+
* @dataProvider provideObjectWithBoolArguments
25+
*/
26+
public function testObjectWithBoolArguments(?bool $expectedValue, ?string $parameterValue)
27+
{
28+
$normalizer = $this->getNormalizerForFilterBool();
29+
30+
$dummy = $normalizer->denormalize(['value' => $parameterValue], FilterBoolObject::class, context: ['filter_bool' => true]);
31+
32+
$this->assertSame($expectedValue, $dummy->value);
33+
}
34+
35+
public static function provideObjectWithBoolArguments()
36+
{
37+
yield 'default value' => [null, null];
38+
yield '0' => [false, '0'];
39+
yield 'false' => [false, 'false'];
40+
yield 'no' => [false, 'no'];
41+
yield 'off' => [false, 'off'];
42+
yield '1' => [true, '1'];
43+
yield 'true' => [true, 'true'];
44+
yield 'yes' => [true, 'yes'];
45+
yield 'on' => [true, 'on'];
46+
}
47+
}

‎src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
4040
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
4141
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
42+
use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait;
4243
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
4344
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
4445
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
@@ -53,6 +54,7 @@ class GetSetMethodNormalizerTest extends TestCase
5354
use CallbacksTestTrait;
5455
use CircularReferenceTestTrait;
5556
use ConstructorArgumentsTestTrait;
57+
use FilterBoolTestTrait;
5658
use GroupsTestTrait;
5759
use IgnoredAttributesTestTrait;
5860
use MaxDepthTestTrait;
@@ -279,6 +281,11 @@ protected function getDenormalizerForGroups(): GetSetMethodNormalizer
279281
return new GetSetMethodNormalizer($classMetadataFactory);
280282
}
281283

284+
protected function getNormalizerForFilterBool(): GetSetMethodNormalizer
285+
{
286+
return new GetSetMethodNormalizer();
287+
}
288+
282289
public function testGroupsNormalizeWithNameConverter()
283290
{
284291
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());

‎src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
5151
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
5252
use Symfony\Component\Serializer\Tests\Normalizer\Features\ContextMetadataTestTrait;
53+
use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait;
5354
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
5455
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
5556
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
@@ -72,6 +73,7 @@ class ObjectNormalizerTest extends TestCase
7273
use CircularReferenceTestTrait;
7374
use ConstructorArgumentsTestTrait;
7475
use ContextMetadataTestTrait;
76+
use FilterBoolTestTrait;
7577
use GroupsTestTrait;
7678
use IgnoredAttributesTestTrait;
7779
use MaxDepthTestTrait;
@@ -345,6 +347,11 @@ protected function getDenormalizerForAttributes(): ObjectNormalizer
345347
return $normalizer;
346348
}
347349

350+
protected function getNormalizerForFilterBool(): ObjectNormalizer
351+
{
352+
return new ObjectNormalizer();
353+
}
354+
348355
public function testAttributesContextDenormalizeConstructor()
349356
{
350357
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());

‎src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
3939
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
4040
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
41+
use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait;
4142
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
4243
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
4344
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
@@ -52,6 +53,7 @@ class PropertyNormalizerTest extends TestCase
5253
use CallbacksTestTrait;
5354
use CircularReferenceTestTrait;
5455
use ConstructorArgumentsTestTrait;
56+
use FilterBoolTestTrait;
5557
use GroupsTestTrait;
5658
use IgnoredAttributesTestTrait;
5759
use MaxDepthTestTrait;
@@ -259,6 +261,11 @@ protected function getSelfReferencingModel()
259261
return new PropertyCircularReferenceDummy();
260262
}
261263

264+
protected function getNormalizerForFilterBool(): PropertyNormalizer
265+
{
266+
return new PropertyNormalizer();
267+
}
268+
262269
public function testSiblingReference()
263270
{
264271
$serializer = new Serializer([$this->normalizer]);

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.