Skip to content

Navigation Menu

Sign in
Appearance settings

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

[HttpKernel] allow boolean argument support for MapQueryString #54153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private function mapQueryString(Request $request, string $type, MapQueryString $
return null;
}

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

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

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

if ('' === $data = $request->getContent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,88 @@ public function testRequestPayloadValidationErrorCustomStatusCode()
$this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage());
}
}

/**
* @dataProvider provideBoolArgument
*/
public function testBoolArgumentInQueryString(mixed $expectedValue, ?string $parameterValue)
{
$serializer = new Serializer([new ObjectNormalizer()]);
$validator = $this->createMock(ValidatorInterface::class);
$resolver = new RequestPayloadValueResolver($serializer, $validator);

$argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [
MapQueryString::class => new MapQueryString(),
]);
$request = Request::create('/', 'GET', ['value' => $parameterValue]);

$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

$resolver->onKernelControllerArguments($event);

$this->assertSame($expectedValue, $event->getArguments()[0]->value);
}

/**
* @dataProvider provideBoolArgument
*/
public function testBoolArgumentInBody(mixed $expectedValue, ?string $parameterValue)
{
$serializer = new Serializer([new ObjectNormalizer()]);
$validator = $this->createMock(ValidatorInterface::class);
$resolver = new RequestPayloadValueResolver($serializer, $validator);

$argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [
MapRequestPayload::class => new MapRequestPayload(),
]);
$request = Request::create('/', 'POST', ['value' => $parameterValue], server: ['CONTENT_TYPE' => 'multipart/form-data']);

$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

$resolver->onKernelControllerArguments($event);

$this->assertSame($expectedValue, $event->getArguments()[0]->value);
}

public static function provideBoolArgument()
{
yield 'default value' => [null, null];
yield '"0"' => [false, '0'];
yield '"false"' => [false, 'false'];
yield '"no"' => [false, 'no'];
yield '"off"' => [false, 'off'];
yield '"1"' => [true, '1'];
yield '"true"' => [true, 'true'];
yield '"yes"' => [true, 'yes'];
yield '"on"' => [true, 'on'];
}

/**
* Boolean filtering must be disabled for content types other than form data.
*/
public function testBoolArgumentInJsonBody()
{
$serializer = new Serializer([new ObjectNormalizer()]);
$validator = $this->createMock(ValidatorInterface::class);
$resolver = new RequestPayloadValueResolver($serializer, $validator);

$argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [
MapRequestPayload::class => new MapRequestPayload(),
]);
$request = Request::create('/', 'POST', ['value' => 'off'], server: ['CONTENT_TYPE' => 'application/json']);

$kernel = $this->createMock(HttpKernelInterface::class);
$arguments = $resolver->resolve($request, $argument);
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);

$resolver->onKernelControllerArguments($event);

$this->assertTrue($event->getArguments()[0]->value);
}
}

class RequestPayload
Expand Down Expand Up @@ -765,3 +847,10 @@ public function getPassword(): string
return $this->password;
}
}

class ObjectWithBoolArgument
{
public function __construct(public readonly ?bool $value = null)
{
}
}
1 change: 1 addition & 0 deletions 1 src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Add `DateTimeNormalizer::CAST_KEY` context option
* Add `Default` and "class name" default groups
* Add `AbstractNormalizer::FILTER_BOOL` context option

7.0
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
*/
public const REQUIRE_ALL_PROPERTIES = 'require_all_properties';

/**
* Flag to control whether a non-boolean value should be filtered using the
* filter_var function with the {@see https://www.php.net/manual/fr/filter.filters.validate.php}
* \FILTER_VALIDATE_BOOL filter before casting it to a boolean.
*
* "0", "false", "off", "no" and "" will be cast to false.
* "1", "true", "on" and "yes" will be cast to true.
*/
public const FILTER_BOOL = 'filter_bool';

/**
* @internal
*/
Expand Down Expand Up @@ -436,12 +446,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
unset($context['has_constructor']);

if (!$reflectionClass->isInstantiable()) {
throw NotNormalizableValueException::createForUnexpectedDataType(
sprintf('Failed to create object because the class "%s" is not instantiable.', $class),
$data,
['unknown'],
$context['deserialization_path'] ?? null
);
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Failed to create object because the class "%s" is not instantiable.', $class), $data, ['unknown'], $context['deserialization_path'] ?? null);
}

return new $class();
Expand Down Expand Up @@ -473,7 +478,9 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara
return null;
}

return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
$parameterData = $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);

return $this->applyFilterBool($parameter, $parameterData, $context);
}

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

final protected function applyFilterBool(\ReflectionParameter $parameter, mixed $value, array $context): mixed
{
if (!($context[self::FILTER_BOOL] ?? false)) {
return $value;
}

if (!($parameterType = $parameter->getType()) instanceof \ReflectionNamedType || 'bool' !== $parameterType->getName()) {
return $value;
}

return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) ?? $value;
}

/**
* Computes the normalization context merged with current one. Metadata always wins over global context, as more specific.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,9 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara

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

return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
$parameterData = $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);

return $this->applyFilterBool($parameter, $parameterData, $context);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?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\Serializer\Tests\Normalizer\Features;

class FilterBoolObject
{
public function __construct(public ?bool $value)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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\Serializer\Tests\Normalizer\Features;

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

/**
* Test AbstractNormalizer::FILTER_BOOL.
*/
trait FilterBoolTestTrait
{
abstract protected function getNormalizerForFilterBool(): DenormalizerInterface;

/**
* @dataProvider provideObjectWithBoolArguments
*/
public function testObjectWithBoolArguments(?bool $expectedValue, ?string $parameterValue)
{
$normalizer = $this->getNormalizerForFilterBool();

$dummy = $normalizer->denormalize(['value' => $parameterValue], FilterBoolObject::class, context: ['filter_bool' => true]);

$this->assertSame($expectedValue, $dummy->value);
}

public static function provideObjectWithBoolArguments()
{
yield 'default value' => [null, null];
yield '0' => [false, '0'];
yield 'false' => [false, 'false'];
yield 'no' => [false, 'no'];
yield 'off' => [false, 'off'];
yield '1' => [true, '1'];
yield 'true' => [true, 'true'];
yield 'yes' => [true, 'yes'];
yield 'on' => [true, 'on'];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
Expand All @@ -53,6 +54,7 @@ class GetSetMethodNormalizerTest extends TestCase
use CallbacksTestTrait;
use CircularReferenceTestTrait;
use ConstructorArgumentsTestTrait;
use FilterBoolTestTrait;
use GroupsTestTrait;
use IgnoredAttributesTestTrait;
use MaxDepthTestTrait;
Expand Down Expand Up @@ -279,6 +281,11 @@ protected function getDenormalizerForGroups(): GetSetMethodNormalizer
return new GetSetMethodNormalizer($classMetadataFactory);
}

protected function getNormalizerForFilterBool(): GetSetMethodNormalizer
{
return new GetSetMethodNormalizer();
}

public function testGroupsNormalizeWithNameConverter()
{
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ContextMetadataTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
Expand All @@ -72,6 +73,7 @@ class ObjectNormalizerTest extends TestCase
use CircularReferenceTestTrait;
use ConstructorArgumentsTestTrait;
use ContextMetadataTestTrait;
use FilterBoolTestTrait;
use GroupsTestTrait;
use IgnoredAttributesTestTrait;
use MaxDepthTestTrait;
Expand Down Expand Up @@ -345,6 +347,11 @@ protected function getDenormalizerForAttributes(): ObjectNormalizer
return $normalizer;
}

protected function getNormalizerForFilterBool(): ObjectNormalizer
{
return new ObjectNormalizer();
}

public function testAttributesContextDenormalizeConstructor()
{
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
Expand All @@ -52,6 +53,7 @@ class PropertyNormalizerTest extends TestCase
use CallbacksTestTrait;
use CircularReferenceTestTrait;
use ConstructorArgumentsTestTrait;
use FilterBoolTestTrait;
use GroupsTestTrait;
use IgnoredAttributesTestTrait;
use MaxDepthTestTrait;
Expand Down Expand Up @@ -259,6 +261,11 @@ protected function getSelfReferencingModel()
return new PropertyCircularReferenceDummy();
}

protected function getNormalizerForFilterBool(): PropertyNormalizer
{
return new PropertyNormalizer();
}

public function testSiblingReference()
{
$serializer = new Serializer([$this->normalizer]);
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.