Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5c20295

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

File tree

10 files changed

+209
-10
lines changed

10 files changed

+209
-10
lines changed

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

Lines 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

Lines 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

Lines 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

Lines 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

Lines 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
/**
Lines 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 <[email protected]>
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+
}
Lines 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 <[email protected]>
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

Lines 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

Lines 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

Lines 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)