Skip to content

[Serializer] Added functionality to parse string boolean values into object boolean fields on denormalization #42716

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

Closed
wants to merge 2 commits into from
Closed
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 @@ -52,6 +52,15 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
public const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';

/**
* While denormalizing object type boolean fields try to parse string values
* into boolean values.
*
* ["true", "yes", "on", "1"] => true
* ["false", "no", "off", "0", ""] => false
*/
public const ENABLE_STRING_BOOL_VALUE = 'enable_string_bool_values';

/**
* Flag to control whether fields with the value `null` should be output
* when normalizing or omitted.
Expand Down Expand Up @@ -573,6 +582,14 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
return (float) $data;
}

if (($context[self::ENABLE_STRING_BOOL_VALUE] ?? $this->defaultContext[self::ENABLE_STRING_BOOL_VALUE] ?? false) && Type::BUILTIN_TYPE_BOOL === $builtinType && \is_string($data)) {
if (null !== $booleanValue = filter_var($data, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE)) {
return $booleanValue;
}

throw new NotNormalizableValueException(sprintf('The value of the "%s" attribute for class "%s" must be one of "true", "yes", "on", "1", "false", "no", "off", "0", "" ("%s" given).', $attribute, $currentClass, $data));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new NotNormalizableValueException(sprintf('The value of the "%s" attribute for class "%s" must be one of "true", "yes", "on", "1", "false", "no", "off", "0", "" ("%s" given).', $attribute, $currentClass, $data));
throw new NotNormalizableValueException(sprintf('The value of the "%s" attribute for class "%s" must be one of "true", "yes", "on", "1", "false", "no", "off", "0", ""(empty string) ("%s" given).', $attribute, $currentClass, $data));

Not sure if this could help and if it makes sense

Copy link
Contributor Author

@aurimasniekis aurimasniekis Aug 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but I was trying to follow Symfony style from exceptions around.

}

if (('is_'.$builtinType)($data)) {
return $data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,83 @@ private function getDenormalizerForObjectWithBasicProperties()
return $denormalizer;
}

public function testDenormalizeBasicTypePropertiesWithStringBoolValues()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better robuste a dataProvider here? The getDenormalizerForObject...() is tight coupled to the number of values provided by the on consecutive calls, while it's not supposed to be extended in the future I guess, it feels a bit weird.

While using a dataProvider it would be less complex and you don't the onConscutiveCalls anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this was my own library I would have written tests differently, but I tried to keep with the same style the all other tests are done in the file.

public function testDenormalizeBasicTypePropertiesFromXml()
{
$denormalizer = $this->getDenormalizerForObjectWithBasicProperties();
// bool
$objectWithBooleanProperties = $denormalizer->denormalize(
[
'boolTrue1' => 'true',
'boolFalse1' => 'false',
'boolTrue2' => '1',
'boolFalse2' => '0',
'int1' => '4711',
'int2' => '-4711',
'float1' => '123.456',
'float2' => '-1.2344e56',
'float3' => '45E-6',
'floatNaN' => 'NaN',
'floatInf' => 'INF',
'floatNegInf' => '-INF',
],
ObjectWithBasicProperties::class,
'xml'
);
$this->assertInstanceOf(ObjectWithBasicProperties::class, $objectWithBooleanProperties);
// Bool Properties
$this->assertTrue($objectWithBooleanProperties->boolTrue1);
$this->assertFalse($objectWithBooleanProperties->boolFalse1);
$this->assertTrue($objectWithBooleanProperties->boolTrue2);
$this->assertFalse($objectWithBooleanProperties->boolFalse2);
// Integer Properties
$this->assertEquals(4711, $objectWithBooleanProperties->int1);
$this->assertEquals(-4711, $objectWithBooleanProperties->int2);
// Float Properties
$this->assertEqualsWithDelta(123.456, $objectWithBooleanProperties->float1, 0.01);
$this->assertEqualsWithDelta(-1.2344e56, $objectWithBooleanProperties->float2, 1);
$this->assertEqualsWithDelta(45E-6, $objectWithBooleanProperties->float3, 1);
$this->assertNan($objectWithBooleanProperties->floatNaN);
$this->assertInfinite($objectWithBooleanProperties->floatInf);
$this->assertEquals(-\INF, $objectWithBooleanProperties->floatNegInf);
}
private function getDenormalizerForObjectWithBasicProperties()
{
$extractor = $this->createMock(PhpDocExtractor::class);
$extractor->method('getTypes')
->will($this->onConsecutiveCalls(
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('int')],
[new Type('int')],
[new Type('float')],
[new Type('float')],
[new Type('float')],
[new Type('float')],
[new Type('float')],
[new Type('float')]
));
$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
$arrayDenormalizer = new ArrayDenormalizerDummy();
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
$arrayDenormalizer->setSerializer($serializer);
$denormalizer->setSerializer($serializer);
return $denormalizer;
}
public function testDenormalizeBasicTypePropertiesWithStringBoolValues()
{
$denormalizer = $this->getDenormalizerForObjectWithStringBoolValues();
// bool
$objectWithBooleanProperties = $denormalizer->denormalize(
[
'true' => 'true',
'yes' => 'yEs',
'on' => 'ON',
'one' => '1',
'false' => 'falsE',
'no' => 'No',
'off' => 'OfF',
'zero' => '0',
'empty' => '',
],
ObjectWithStringBoolean::class,
null,
[AbstractObjectNormalizer::ENABLE_STRING_BOOL_VALUE => true]
);
$this->assertInstanceOf(ObjectWithStringBoolean::class, $objectWithBooleanProperties);
$this->assertTrue($objectWithBooleanProperties->true);
$this->assertTrue($objectWithBooleanProperties->yes);
$this->assertTrue($objectWithBooleanProperties->on);
$this->assertTrue($objectWithBooleanProperties->one);
$this->assertFalse($objectWithBooleanProperties->false);
$this->assertFalse($objectWithBooleanProperties->no);
$this->assertFalse($objectWithBooleanProperties->off);
$this->assertFalse($objectWithBooleanProperties->zero);
$this->assertFalse($objectWithBooleanProperties->empty);
}
public function testDenormalizeBasicTypePropertiesWithBadStringBoolValues()
{
$denormalizer = $this->getDenormalizerForObjectWithStringBoolValues();
$this->expectException(NotNormalizableValueException::class);
$this->expectExceptionMessage('The value of the "yes" attribute for class "Symfony\Component\Serializer\Tests\Normalizer\ObjectWithStringBoolean" must be one of "true", "yes", "on", "1", "false", "no", "off", "0", "" ("foo" given).');
$objectWithBooleanProperties = $denormalizer->denormalize(
[
'yes' => 'foo',
],
ObjectWithStringBoolean::class,
null,
[AbstractObjectNormalizer::ENABLE_STRING_BOOL_VALUE => true]
);
}
private function getDenormalizerForObjectWithStringBoolValues()
{
$extractor = $this->createMock(PhpDocExtractor::class);
$extractor->method('getTypes')
->will($this->onConsecutiveCalls(
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')]
));
$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
$arrayDenormalizer = new ArrayDenormalizerDummy();
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
$arrayDenormalizer->setSerializer($serializer);
$denormalizer->setSerializer($serializer);
return $denormalizer;
}

{
$denormalizer = $this->getDenormalizerForObjectWithStringBoolValues();

// bool
$objectWithBooleanProperties = $denormalizer->denormalize(
[
'true' => 'true',
'yes' => 'yEs',
'on' => 'ON',
'one' => '1',
'false' => 'falsE',
'no' => 'No',
'off' => 'OfF',
'zero' => '0',
'empty' => '',
],
ObjectWithStringBoolean::class,
null,
[AbstractObjectNormalizer::ENABLE_STRING_BOOL_VALUE => true]
);

$this->assertInstanceOf(ObjectWithStringBoolean::class, $objectWithBooleanProperties);

$this->assertTrue($objectWithBooleanProperties->true);
$this->assertTrue($objectWithBooleanProperties->yes);
$this->assertTrue($objectWithBooleanProperties->on);
$this->assertTrue($objectWithBooleanProperties->one);
$this->assertFalse($objectWithBooleanProperties->false);
$this->assertFalse($objectWithBooleanProperties->no);
$this->assertFalse($objectWithBooleanProperties->off);
$this->assertFalse($objectWithBooleanProperties->zero);
$this->assertFalse($objectWithBooleanProperties->empty);
}

public function testDenormalizeBasicTypePropertiesWithBadStringBoolValues()
{
$denormalizer = $this->getDenormalizerForObjectWithStringBoolValues();

$this->expectException(NotNormalizableValueException::class);
$this->expectExceptionMessage('The value of the "yes" attribute for class "Symfony\Component\Serializer\Tests\Normalizer\ObjectWithStringBoolean" must be one of "true", "yes", "on", "1", "false", "no", "off", "0", "" ("foo" given).');

$objectWithBooleanProperties = $denormalizer->denormalize(
[
'yes' => 'foo',
],
ObjectWithStringBoolean::class,
null,
[AbstractObjectNormalizer::ENABLE_STRING_BOOL_VALUE => true]
);
}

private function getDenormalizerForObjectWithStringBoolValues()
{
$extractor = $this->createMock(PhpDocExtractor::class);
$extractor->method('getTypes')
->will($this->onConsecutiveCalls(
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')],
[new Type('bool')]
));

$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
$arrayDenormalizer = new ArrayDenormalizerDummy();
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
$arrayDenormalizer->setSerializer($serializer);
$denormalizer->setSerializer($serializer);

return $denormalizer;
}

/**
* Test that additional attributes throw an exception if no metadata factory is specified.
*/
Expand Down Expand Up @@ -416,6 +493,36 @@ public function instantiateObject(array &$data, string $class, array &$context,
}
}

class ObjectWithStringBoolean
{
/** @var bool */
public $true;

/** @var bool */
public $yes;

/** @var bool */
public $on;

/** @var bool */
public $one;

/** @var bool */
public $false;

/** @var bool */
public $no;

/** @var bool */
public $off;

/** @var bool */
public $zero;

/** @var bool */
public $empty;
}

class Dummy
{
public $foo;
Expand Down