[serializer] extract normalizer tests to traits

This commit is contained in:
David Buchmann 2019-04-06 13:49:33 +02:00 committed by Fabien Potencier
parent 77f642ef39
commit 2b6ebea73c
25 changed files with 1901 additions and 1415 deletions

View File

@ -32,20 +32,89 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
use ObjectToPopulateTrait;
use SerializerAwareTrait;
const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
const OBJECT_TO_POPULATE = 'object_to_populate';
const GROUPS = 'groups';
const ATTRIBUTES = 'attributes';
const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
const CALLBACKS = 'callbacks';
const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler';
const IGNORED_ATTRIBUTES = 'ignored_attributes';
/* constants to configure the context */
/**
* How many loops of circular reference to allow while normalizing.
*
* The default value of 1 means that when we encounter the same object a
* second time, we consider that a circular reference.
*
* You can raise this value for special cases, e.g. in combination with the
* max depth setting of the object normalizer.
*/
public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
/**
* Instead of creating a new instance of an object, update the specified object.
*
* If you have a nested structure, child objects will be overwritten with
* new instances unless you set DEEP_OBJECT_TO_POPULATE to true.
*/
public const OBJECT_TO_POPULATE = 'object_to_populate';
/**
* Only (de)normalize attributes that are in the specified groups.
*/
public const GROUPS = 'groups';
/**
* Limit (de)normalize to the specified names.
*
* For nested structures, this list needs to reflect the object tree.
*/
public const ATTRIBUTES = 'attributes';
/**
* If ATTRIBUTES are specified, and the source has fields that are not part of that list,
* either ignore those attributes (true) or throw an ExtraAttributesException (false).
*/
public const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
/**
* Hashmap of default values for constructor arguments.
*
* The names need to match the parameter names in the constructor arguments.
*/
public const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
/**
* Hashmap of field name => callable to normalize this field.
*
* The callable is called if the field is encountered with the arguments:
*
* - mixed $attributeValue value of this field
* - object $object the whole object being normalized
* - string $attributeName name of the attribute being normalized
* - string $format the requested format
* - array $context the serialization context
*/
public const CALLBACKS = 'callbacks';
/**
* Handler to call when a circular reference has been detected.
*
* If you specify no handler, a CircularReferenceException is thrown.
*
* The method will be called with ($object, $format, $context) and its
* return value is returned as the result of the normalize call.
*/
public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler';
/**
* Skip the specified attributes when normalizing an object tree.
*
* This list is applied to each element of nested structures.
*
* Note: The behaviour for nested structures is different from ATTRIBUTES
* for historical reason. Aligning the behaviour would be a BC break.
*/
public const IGNORED_ATTRIBUTES = 'ignored_attributes';
/**
* @internal
*/
const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters';
protected const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters';
protected $defaultContext = [
self::ALLOW_EXTRA_ATTRIBUTES => true,

View File

@ -33,13 +33,60 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
*/
abstract class AbstractObjectNormalizer extends AbstractNormalizer
{
const ENABLE_MAX_DEPTH = 'enable_max_depth';
const DEPTH_KEY_PATTERN = 'depth_%s::%s';
const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
const SKIP_NULL_VALUES = 'skip_null_values';
const MAX_DEPTH_HANDLER = 'max_depth_handler';
const EXCLUDE_FROM_CACHE_KEY = 'exclude_from_cache_key';
const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
/**
* Set to true to respect the max depth metadata on fields.
*/
public const ENABLE_MAX_DEPTH = 'enable_max_depth';
/**
* How to track the current depth in the context.
*/
private const DEPTH_KEY_PATTERN = 'depth_%s::%s';
/**
* While denormalizing, we can verify that types match.
*
* You can disable this by setting this flag to true.
*/
public const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
/**
* Flag to control whether fields with the value `null` should be output
* when normalizing or omitted.
*/
public const SKIP_NULL_VALUES = 'skip_null_values';
/**
* Callback to allow to set a value for an attribute when the max depth has
* been reached.
*
* If no callback is given, the attribute is skipped. If a callable is
* given, its return value is used (even if null).
*
* The arguments are:
*
* - mixed $attributeValue value of this field
* - object $object the whole object being normalized
* - string $attributeName name of the attribute being normalized
* - string $format the requested format
* - array $context the serialization context
*/
public const MAX_DEPTH_HANDLER = 'max_depth_handler';
/**
* Specify which context key are not relevant to determine which attributes
* of an object to (de)normalize.
*/
public const EXCLUDE_FROM_CACHE_KEY = 'exclude_from_cache_key';
/**
* Flag to tell the denormalizer to also populate existing objects on
* attributes of the main object.
*
* Setting this to true is only useful if you also specify the root object
* in OBJECT_TO_POPULATE.
*/
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
private $propertyTypeExtractor;
private $typesCache = [];

View File

@ -19,4 +19,20 @@ class DeepObjectPopulateChildDummy
public $foo;
public $bar;
// needed to have GetSetNormalizer consider this class as supported
public function getFoo()
{
return $this->foo;
}
public function setFoo($foo)
{
$this->foo = $foo;
}
public function setBar($bar)
{
$this->bar = $bar;
}
}

View File

@ -42,4 +42,9 @@ class MaxDepthDummy
{
return $this->child;
}
public function getFoo()
{
return $this->foo;
}
}

View File

@ -10,7 +10,6 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractNormalizerDummy;
use Symfony\Component\Serializer\Tests\Fixtures\NullableConstructorArgumentDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ProxyDummy;
use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorDummy;
use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorNormalizer;
@ -99,18 +98,6 @@ class AbstractNormalizerTest extends TestCase
$this->assertEquals([$a3, $a4], $result);
}
public function testObjectToPopulateWithProxy()
{
$proxyDummy = new ProxyDummy();
$context = [AbstractNormalizer::OBJECT_TO_POPULATE => $proxyDummy];
$normalizer = new ObjectNormalizer();
$normalizer->denormalize(['foo' => 'bar'], 'Symfony\Component\Serializer\Tests\Fixtures\ToBeProxyfiedDummy', null, $context);
$this->assertSame('bar', $proxyDummy->getFoo());
}
public function testObjectWithStaticConstructor()
{
$normalizer = new StaticConstructorNormalizer();

View File

@ -23,8 +23,6 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateChildDummy;
use Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateParentDummy;
class AbstractObjectNormalizerTest extends TestCase
{
@ -163,58 +161,6 @@ class AbstractObjectNormalizerTest extends TestCase
'allow_extra_attributes' => false,
]);
}
public function testSkipNullValues()
{
$dummy = new Dummy();
$dummy->bar = 'present';
$normalizer = new ObjectNormalizer();
$result = $normalizer->normalize($dummy, null, [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
$this->assertSame(['bar' => 'present'], $result);
}
public function testDeepObjectToPopulate()
{
$child = new DeepObjectPopulateChildDummy();
$child->bar = 'bar-old';
$child->foo = 'foo-old';
$parent = new DeepObjectPopulateParentDummy();
$parent->setChild($child);
$context = [
AbstractObjectNormalizer::OBJECT_TO_POPULATE => $parent,
AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE => true,
];
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
$newChild = new DeepObjectPopulateChildDummy();
$newChild->bar = 'bar-new';
$newChild->foo = 'foo-old';
$serializer = $this->getMockBuilder(__NAMESPACE__.'\ObjectSerializerDenormalizer')->getMock();
$serializer
->method('supportsDenormalization')
->with($this->arrayHasKey('bar'),
$this->equalTo(DeepObjectPopulateChildDummy::class),
$this->isNull(),
$this->contains($child))
->willReturn(true);
$serializer->method('denormalize')->willReturn($newChild);
$normalizer->setSerializer($serializer);
$normalizer->denormalize([
'child' => [
'bar' => 'bar-new',
],
], 'Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateParentDummy', null, $context);
$this->assertSame('bar-new', $parent->getChild()->bar);
$this->assertSame('foo-old', $parent->getChild()->foo);
}
}
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer

View File

@ -0,0 +1,108 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractNormalizer::ATTRIBUTES and AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES.
*/
trait AttributesTestTrait
{
abstract protected function getNormalizerForAttributes(): NormalizerInterface;
abstract protected function getDenormalizerForAttributes(): DenormalizerInterface;
public function testAttributesNormalize()
{
$normalizer = $this->getNormalizerForAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectInner->bar = 'innerBar';
$objectDummy = new ObjectDummy();
$objectDummy->setFoo('foo');
$objectDummy->setBaz(true);
$objectDummy->setObject($objectInner);
$context = ['attributes' => ['foo', 'baz', 'object' => ['foo']]];
$this->assertEquals(
[
'foo' => 'foo',
'baz' => true,
'object' => ['foo' => 'innerFoo'],
],
$normalizer->normalize($objectDummy, null, $context)
);
$context = ['attributes' => ['foo', 'baz', 'object']];
$this->assertEquals(
[
'foo' => 'foo',
'baz' => true,
'object' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
],
$normalizer->normalize($objectDummy, null, $context)
);
}
public function testAttributesContextDenormalize()
{
$normalizer = $this->getDenormalizerForAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectOuter = new ObjectOuter();
$objectOuter->bar = 'bar';
$objectOuter->setInner($objectInner);
$context = ['attributes' => ['bar', 'inner' => ['foo']]];
$this->assertEquals($objectOuter, $normalizer->denormalize(
[
'foo' => 'foo',
'bar' => 'bar',
'date' => '2017-02-03',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context));
}
public function testAttributesContextDenormalizeIgnoreExtraAttributes()
{
$normalizer = $this->getDenormalizerForAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectOuter = new ObjectOuter();
$objectOuter->setInner($objectInner);
$context = ['attributes' => ['inner' => ['foo']]];
$this->assertEquals($objectOuter, $normalizer->denormalize(
[
'foo' => 'foo',
'bar' => 'changed',
'date' => '2017-02-03',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context));
}
public function testAttributesContextDenormalizeExceptionExtraAttributes()
{
$normalizer = $this->getDenormalizerForAttributes();
$context = [
'attributes' => ['bar', 'inner' => ['foo']],
'allow_extra_attributes' => false,
];
$this->expectException(ExtraAttributesException::class);
$normalizer->denormalize(
[
'bar' => 'bar',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class CallbacksObject
{
public $bar;
public function __construct($bar = null)
{
$this->bar = $bar;
}
public function getBar()
{
return $this->bar;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractNormalizer::CALLBACKS.
*/
trait CallbacksTestTrait
{
abstract protected function getNormalizerForCallbacks(): NormalizerInterface;
/**
* @dataProvider provideCallbacks
*/
public function testCallbacks($callbacks, $valueBar, $result)
{
$normalizer = $this->getNormalizerForCallbacks();
$obj = new CallbacksObject();
$obj->bar = $valueBar;
$this->assertEquals(
$result,
$normalizer->normalize($obj, 'any', ['callbacks' => $callbacks])
);
}
/**
* @dataProvider provideInvalidCallbacks()
*/
public function testUncallableCallbacks($callbacks)
{
$normalizer = $this->getNormalizerForCallbacks();
$obj = new CallbacksObject();
$this->markTestSkipped('Callback validation for callbacks in the context has been forgotten. See https://github.com/symfony/symfony/pull/30950');
$this->expectException(InvalidArgumentException::class);
$normalizer->normalize($obj, null, ['callbacks' => $callbacks]);
}
public function provideCallbacks()
{
return [
'Change a string' => [
[
'bar' => function ($bar) {
$this->assertEquals('baz', $bar);
return 'baz';
},
],
'baz',
['bar' => 'baz'],
],
'Null an item' => [
[
'bar' => function ($value, $object, $attributeName, $format, $context) {
$this->assertSame('baz', $value);
$this->assertInstanceOf(CallbacksObject::class, $object);
$this->assertSame('bar', $attributeName);
$this->assertSame('any', $format);
$this->assertArrayHasKey('circular_reference_limit_counters', $context);
},
],
'baz',
['bar' => null],
],
'Format a date' => [
[
'bar' => function ($bar) {
$this->assertInstanceOf(\DateTime::class, $bar);
return $bar->format('d-m-Y H:i:s');
},
],
new \DateTime('2011-09-10 06:30:00'),
['bar' => '10-09-2011 06:30:00'],
],
'Collect a property' => [
[
'bar' => function (array $bars) {
$result = '';
foreach ($bars as $bar) {
$result .= $bar->bar;
}
return $result;
},
],
[new CallbacksObject('baz'), new CallbacksObject('quux')],
['bar' => 'bazquux'],
],
'Count a property' => [
[
'bar' => function (array $bars) {
return \count($bars);
},
],
[new CallbacksObject(), new CallbacksObject()],
['bar' => 2],
],
];
}
public function provideInvalidCallbacks()
{
return [
[['bar' => null]],
[['bar' => 'thisisnotavalidfunction']],
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT and AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER.
*/
trait CircularReferenceTestTrait
{
abstract protected function getNormalizerForCircularReference(): NormalizerInterface;
abstract protected function getSelfReferencingModel();
public function testUnableToNormalizeCircularReference()
{
$normalizer = $this->getNormalizerForCircularReference();
$obj = $this->getSelfReferencingModel();
$this->expectException(CircularReferenceException::class);
$normalizer->normalize($obj, null, ['circular_reference_limit' => 2]);
}
public function testCircularReferenceHandler()
{
$normalizer = $this->getNormalizerForCircularReference();
$obj = $this->getSelfReferencingModel();
$expected = ['me' => \get_class($obj)];
$context = [
'circular_reference_handler' => function ($actualObj, string $format, array $context) use ($obj) {
$this->assertInstanceOf(\get_class($obj), $actualObj);
$this->assertSame('test', $format);
$this->assertArrayHasKey('foo', $context);
return \get_class($actualObj);
},
'foo' => 'bar',
];
$this->assertEquals($expected, $normalizer->normalize($obj, 'test', $context));
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class ConstructorArgumentsObject
{
private $foo;
private $bar;
private $baz;
public function __construct($foo, $bar, $baz)
{
$this->foo = $foo;
$this->bar = $bar;
$this->baz = $baz;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\NotSerializedConstructorArgumentDummy;
trait ConstructorArgumentsTestTrait
{
abstract protected function getDenormalizerForConstructArguments(): DenormalizerInterface;
public function testDefaultConstructorArguments()
{
$data = [
'foo' => 10,
];
$denormalizer = $this->getDenormalizerForConstructArguments();
$result = $denormalizer->denormalize($data, ConstructorArgumentsObject::class, 'json', [
'default_constructor_arguments' => [
ConstructorArgumentsObject::class => ['foo' => '', 'bar' => '', 'baz' => null],
],
]);
$this->assertEquals(new ConstructorArgumentsObject(10, '', null), $result);
}
public function testMetadataAwareNameConvertorWithNotSerializedConstructorParameter()
{
$denormalizer = $this->getDenormalizerForConstructArguments();
$obj = new NotSerializedConstructorArgumentDummy('buz');
$obj->setBar('xyz');
$this->assertEquals(
$obj,
$denormalizer->denormalize(['bar' => 'xyz'],
NotSerializedConstructorArgumentDummy::class,
null,
['default_constructor_arguments' => [
NotSerializedConstructorArgumentDummy::class => ['foo' => 'buz'],
]]
)
);
}
public function testConstructorWithMissingData()
{
$data = [
'foo' => 10,
];
$normalizer = $this->getDenormalizerForConstructArguments();
$this->expectException(MissingConstructorArgumentsException::class);
$this->expectExceptionMessage('Cannot create an instance of '.ConstructorArgumentsObject::class.' from serialized data because its constructor requires parameter "bar" to be present.');
$normalizer->denormalize($data, ConstructorArgumentsObject::class);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
/**
* Test AbstractNormalizer::GROUPS.
*/
trait GroupsTestTrait
{
abstract protected function getNormalizerForGroups(): NormalizerInterface;
abstract protected function getDenormalizerForGroups(): DenormalizerInterface;
public function testGroupsNormalize()
{
$normalizer = $this->getNormalizerForGroups();
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj->setBar('bar');
$obj->setFooBar('fooBar');
$obj->setSymfony('symfony');
$obj->setKevin('kevin');
$obj->setCoopTilleuls('coopTilleuls');
$this->assertEquals([
'bar' => 'bar',
], $normalizer->normalize($obj, null, ['groups' => ['c']]));
$this->assertEquals([
'symfony' => 'symfony',
'foo' => 'foo',
'fooBar' => 'fooBar',
'bar' => 'bar',
'kevin' => 'kevin',
'coopTilleuls' => 'coopTilleuls',
], $normalizer->normalize($obj, null, ['groups' => ['a', 'c']]));
}
public function testGroupsDenormalize()
{
$normalizer = $this->getDenormalizerForGroups();
$obj = new GroupDummy();
$obj->setFoo('foo');
$data = ['foo' => 'foo', 'bar' => 'bar'];
$normalized = $normalizer->denormalize(
$data,
GroupDummy::class,
null,
['groups' => ['a']]
);
$this->assertEquals($obj, $normalized);
$obj->setBar('bar');
$normalized = $normalizer->denormalize(
$data,
GroupDummy::class,
null,
['groups' => ['a', 'b']]
);
$this->assertEquals($obj, $normalized);
}
public function testNormalizeNoPropertyInGroup()
{
$normalizer = $this->getNormalizerForGroups();
$obj = new GroupDummy();
$obj->setFoo('foo');
$this->assertEquals([], $normalizer->normalize($obj, null, ['groups' => ['notExist']]));
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractNormalizer::IGNORED_ATTRIBUTES.
*/
trait IgnoredAttributesTestTrait
{
abstract protected function getNormalizerForIgnoredAttributes(): NormalizerInterface;
abstract protected function getDenormalizerForIgnoredAttributes(): DenormalizerInterface;
public function testIgnoredAttributesNormalize()
{
$normalizer = $this->getNormalizerForIgnoredAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectInner->bar = 'innerBar';
$objectOuter = new ObjectOuter();
$objectOuter->foo = 'foo';
$objectOuter->bar = 'bar';
$objectOuter->setInner($objectInner);
$context = ['ignored_attributes' => ['bar']];
$this->assertEquals(
[
'foo' => 'foo',
'inner' => ['foo' => 'innerFoo'],
'date' => null,
'inners' => null,
],
$normalizer->normalize($objectOuter, null, $context)
);
$this->markTestIncomplete('AbstractObjectNormalizer::getAttributes caches attributes by class instead of by class+context, reusing the normalizer with different config therefore fails. This is being fixed in https://github.com/symfony/symfony/pull/30907');
$context = ['ignored_attributes' => ['foo', 'inner']];
$this->assertEquals(
[
'bar' => 'bar',
'date' => null,
'inners' => null,
],
$normalizer->normalize($objectOuter, null, $context)
);
}
public function testIgnoredAttributesContextDenormalize()
{
$normalizer = $this->getDenormalizerForIgnoredAttributes();
$objectOuter = new ObjectOuter();
$objectOuter->bar = 'bar';
$context = ['ignored_attributes' => ['foo', 'inner']];
$this->assertEquals($objectOuter, $normalizer->denormalize(
[
'foo' => 'foo',
'bar' => 'bar',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context));
}
public function testIgnoredAttributesContextDenormalizeInherit()
{
$normalizer = $this->getDenormalizerForIgnoredAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectOuter = new ObjectOuter();
$objectOuter->foo = 'foo';
$objectOuter->setInner($objectInner);
$context = ['ignored_attributes' => ['bar']];
$this->assertEquals($objectOuter, $normalizer->denormalize(
[
'foo' => 'foo',
'bar' => 'bar',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context));
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
/**
* Covers AbstractObjectNormalizer::ENABLE_MAX_DEPTH and AbstractObjectNormalizer::MAX_DEPTH_HANDLER.
*/
trait MaxDepthTestTrait
{
abstract protected function getNormalizerForMaxDepth(): NormalizerInterface;
public function testMaxDepth()
{
$normalizer = $this->getNormalizerForMaxDepth();
$level1 = new MaxDepthDummy();
$level1->bar = 'level1';
$level2 = new MaxDepthDummy();
$level2->bar = 'level2';
$level1->child = $level2;
$level3 = new MaxDepthDummy();
$level3->bar = 'level3';
$level2->child = $level3;
$level4 = new MaxDepthDummy();
$level4->bar = 'level4';
$level3->child = $level4;
$result = $normalizer->normalize($level1, null, ['enable_max_depth' => true]);
$expected = [
'bar' => 'level1',
'child' => [
'bar' => 'level2',
'child' => [
'bar' => 'level3',
'child' => [
'child' => null,
],
],
'foo' => null,
],
'foo' => null,
];
$this->assertEquals($expected, $result);
}
public function testMaxDepthHandler()
{
$level1 = new MaxDepthDummy();
$level1->bar = 'level1';
$level2 = new MaxDepthDummy();
$level2->bar = 'level2';
$level1->child = $level2;
$level3 = new MaxDepthDummy();
$level3->bar = 'level3';
$level2->child = $level3;
$level4 = new MaxDepthDummy();
$level4->bar = 'level4';
$level3->child = $level4;
$handlerCalled = false;
$maxDepthHandler = function ($object, $parentObject, $attributeName, $format, $context) use (&$handlerCalled) {
if ('foo' === $attributeName) {
return null;
}
$this->assertSame('level4', $object);
$this->assertInstanceOf(MaxDepthDummy::class, $parentObject);
$this->assertSame('bar', $attributeName);
$this->assertSame('test', $format);
$this->assertArrayHasKey('enable_max_depth', $context);
$handlerCalled = true;
return 'handler';
};
$normalizer = $this->getNormalizerForMaxDepth();
$normalizer->normalize($level1, 'test', [
'enable_max_depth' => true,
'max_depth_handler' => $maxDepthHandler,
]);
$this->assertTrue($handlerCalled);
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class ObjectDummy
{
protected $foo;
public $bar;
private $baz;
protected $camelCase;
protected $object;
public function getFoo()
{
return $this->foo;
}
public function setFoo($foo)
{
$this->foo = $foo;
}
public function isBaz()
{
return $this->baz;
}
public function setBaz($baz)
{
$this->baz = $baz;
}
public function getFooBar()
{
return $this->foo.$this->bar;
}
public function getCamelCase()
{
return $this->camelCase;
}
public function setCamelCase($camelCase)
{
$this->camelCase = $camelCase;
}
public function otherMethod()
{
throw new \RuntimeException('Dummy::otherMethod() should not be called');
}
public function setObject($object)
{
$this->object = $object;
}
public function getObject()
{
return $this->object;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class ObjectInner
{
public $foo;
public $bar;
public function getBar()
{
return $this->bar;
}
public function setBar($bar): void
{
$this->bar = $bar;
}
public function getFoo()
{
return $this->foo;
}
public function setFoo($foo): void
{
$this->foo = $foo;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class ObjectOuter
{
public $foo;
public $bar;
private $inner;
private $date;
/**
* @var ObjectInner[]
*/
private $inners;
public function getFoo()
{
return $this->foo;
}
public function setFoo($foo): void
{
$this->foo = $foo;
}
public function getBar()
{
return $this->bar;
}
public function setBar($bar): void
{
$this->bar = $bar;
}
/**
* @return ObjectInner
*/
public function getInner()
{
return $this->inner;
}
public function setInner(ObjectInner $inner)
{
$this->inner = $inner;
}
public function setDate(\DateTimeInterface $date)
{
$this->date = $date;
}
public function getDate()
{
return $this->date;
}
public function setInners(array $inners)
{
$this->inners = $inners;
}
public function getInners()
{
return $this->inners;
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateChildDummy;
use Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateParentDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ProxyDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ToBeProxyfiedDummy;
trait ObjectToPopulateTestTrait
{
abstract protected function getDenormalizerForObjectToPopulate(): DenormalizerInterface;
public function testObjectToPopulate()
{
$dummy = new ObjectDummy();
$dummy->bar = 'bar';
$denormalizer = $this->getDenormalizerForObjectToPopulate();
$obj = $denormalizer->denormalize(
['foo' => 'foo'],
ObjectDummy::class,
null,
['object_to_populate' => $dummy]
);
$this->assertEquals($dummy, $obj);
$this->assertEquals('foo', $obj->getFoo());
$this->assertEquals('bar', $obj->bar);
}
public function testObjectToPopulateWithProxy()
{
$proxyDummy = new ProxyDummy();
$context = ['object_to_populate' => $proxyDummy];
$denormalizer = $this->getDenormalizerForObjectToPopulate();
$denormalizer->denormalize(['foo' => 'bar'], ToBeProxyfiedDummy::class, null, $context);
$this->assertSame('bar', $proxyDummy->getFoo());
}
public function testObjectToPopulateNoMatch()
{
$this->markTestSkipped('something broken here!');
$denormalizer = $this->getDenormalizerForObjectToPopulate();
$objectToPopulate = new ObjectInner();
$objectToPopulate->foo = 'foo';
$outer = $denormalizer->denormalize([
'foo' => 'foo',
'inner' => [
'bar' => 'bar',
],
], ObjectOuter::class, null, ['object_to_popuplate' => $objectToPopulate]);
$this->assertInstanceOf(ObjectOuter::class, $outer);
$inner = $outer->getInner();
$this->assertInstanceOf(ObjectInner::class, $inner);
$this->assertNotSame($objectToPopulate, $inner);
$this->assertSame('bar', $inner->bar);
$this->assertNull($inner->foo);
}
public function testDeepObjectToPopulate()
{
$child = new DeepObjectPopulateChildDummy();
$child->bar = 'bar-old';
$child->foo = 'foo-old';
$parent = new DeepObjectPopulateParentDummy();
$parent->setChild($child);
$context = [
'object_to_populate' => $parent,
'deep_object_to_populate' => true,
];
$normalizer = $this->getDenormalizerForObjectToPopulate();
$newChild = new DeepObjectPopulateChildDummy();
$newChild->bar = 'bar-new';
$newChild->foo = 'foo-old';
$normalizer->denormalize([
'child' => [
'bar' => 'bar-new',
],
], DeepObjectPopulateParentDummy::class, null, $context);
$this->assertSame('bar-new', $parent->getChild()->bar);
$this->assertSame('foo-old', $parent->getChild()->foo);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractObjectNormalizer::SKIP_NULL_VALUES.
*/
trait SkipNullValuesTestTrait
{
abstract protected function getNormalizerForSkipNullValues(): NormalizerInterface;
public function testSkipNullValues()
{
$dummy = new ObjectDummy();
$dummy->bar = 'present';
$normalizer = $this->getNormalizerForSkipNullValues();
$result = $normalizer->normalize($dummy, null, ['skip_null_values' => true]);
$this->assertSame(['fooBar' => 'present', 'bar' => 'present'], $result);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class TypeEnforcementNumberObject
{
/**
* @var float
*/
public $number;
public function setNumber($number)
{
$this->number = $number;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Test type mismatches with a denormalizer that is aware of types.
* Covers AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT.
*/
trait TypeEnforcementTestTrait
{
abstract protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface;
public function testRejectInvalidType()
{
$denormalizer = $this->getDenormalizerForTypeEnforcement();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('The type of the "date" attribute for class "'.ObjectOuter::class.'" must be one of "DateTimeInterface" ("string" given).');
$denormalizer->denormalize(['date' => 'foo'], ObjectOuter::class);
}
public function testRejectInvalidKey()
{
$denormalizer = $this->getDenormalizerForTypeEnforcement();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('The type of the key "a" must be "int" ("string" given).');
$denormalizer->denormalize(['inners' => ['a' => ['foo' => 1]]], ObjectOuter::class);
}
public function testDoNotRejectInvalidTypeOnDisableTypeEnforcementContextOption()
{
$denormalizer = $this->getDenormalizerForTypeEnforcement();
$this->assertSame('foo', $denormalizer->denormalize(
['number' => 'foo'],
TypeEnforcementNumberObject::class,
null,
[AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true]
)->number);
}
}

View File

@ -13,21 +13,45 @@ namespace Symfony\Component\Serializer\Tests\Normalizer;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksObject;
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\GroupsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectToPopulateTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait;
class GetSetMethodNormalizerTest extends TestCase
{
use CallbacksTestTrait;
use CircularReferenceTestTrait;
use ConstructorArgumentsTestTrait;
use GroupsTestTrait;
use IgnoredAttributesTestTrait;
use MaxDepthTestTrait;
use ObjectToPopulateTestTrait;
use TypeEnforcementTestTrait;
/**
* @var GetSetMethodNormalizer
*/
@ -89,7 +113,7 @@ class GetSetMethodNormalizerTest extends TestCase
{
$obj = $this->normalizer->denormalize(
['foo' => 'foo', 'bar' => 'bar', 'baz' => true, 'fooBar' => 'foobar'],
__NAMESPACE__.'\GetSetDummy',
GetSetDummy::class,
'any'
);
$this->assertEquals('foo', $obj->getFoo());
@ -119,14 +143,14 @@ class GetSetMethodNormalizerTest extends TestCase
$data->foo = 'foo';
$data->bar = 'bar';
$data->fooBar = 'foobar';
$obj = $this->normalizer->denormalize($data, __NAMESPACE__.'\GetSetDummy', 'any');
$obj = $this->normalizer->denormalize($data, GetSetDummy::class, 'any');
$this->assertEquals('foo', $obj->getFoo());
$this->assertEquals('bar', $obj->getBar());
}
public function testDenormalizeNull()
{
$this->assertEquals(new GetSetDummy(), $this->normalizer->denormalize(null, __NAMESPACE__.'\GetSetDummy'));
$this->assertEquals(new GetSetDummy(), $this->normalizer->denormalize(null, GetSetDummy::class));
}
public function testConstructorDenormalize()
@ -202,62 +226,90 @@ class GetSetMethodNormalizerTest extends TestCase
$this->assertEquals('bar', $obj->getFoo());
}
public function testGroupsNormalize()
protected function getNormalizerForCallbacks(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj->setBar('bar');
$obj->setFooBar('fooBar');
$obj->setSymfony('symfony');
$obj->setKevin('kevin');
$obj->setCoopTilleuls('coopTilleuls');
$this->assertEquals([
'bar' => 'bar',
], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['c']]));
$this->assertEquals([
'symfony' => 'symfony',
'foo' => 'foo',
'fooBar' => 'fooBar',
'bar' => 'bar',
'kevin' => 'kevin',
'coopTilleuls' => 'coopTilleuls',
], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['a', 'c']]));
return new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
}
public function testGroupsDenormalize()
/**
* @dataProvider provideCallbacks
*/
public function testLegacyCallbacks($callbacks, $value, $result)
{
$this->normalizer->setCallbacks($callbacks);
$obj = new CallbacksObject($value);
$this->assertEquals(
$result,
$this->normalizer->normalize($obj, 'any')
);
}
protected function getNormalizerForCircularReference(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
new Serializer([$normalizer]);
return $normalizer;
}
protected function getSelfReferencingModel()
{
return new CircularReferenceDummy();
}
public function testLegacyUnableToNormalizeCircularReference()
{
$this->normalizer->setCircularReferenceLimit(2);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj = new CircularReferenceDummy();
$toNormalize = ['foo' => 'foo', 'bar' => 'bar'];
$this->expectException(CircularReferenceException::class);
$this->normalizer->normalize($obj);
}
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
[GetSetMethodNormalizer::GROUPS => ['a']]
);
$this->assertEquals($obj, $normalized);
public function testLegacyCircularReferenceHandler()
{
$handler = function ($obj) {
return \get_class($obj);
};
$obj->setBar('bar');
$this->normalizer->setCircularReferenceHandler($handler);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
[GetSetMethodNormalizer::GROUPS => ['a', 'b']]
);
$this->assertEquals($obj, $normalized);
$obj = new CircularReferenceDummy();
$expected = ['me' => CircularReferenceDummy::class];
$this->assertEquals($expected, $this->normalizer->normalize($obj));
}
protected function getDenormalizerForConstructArguments(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$denormalizer = new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
new Serializer([$denormalizer]);
return $denormalizer;
}
protected function getNormalizerForGroups(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
return new GetSetMethodNormalizer($classMetadataFactory);
}
protected function getDenormalizerForGroups(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
return new GetSetMethodNormalizer($classMetadataFactory);
}
public function testGroupsNormalizeWithNameConverter()
@ -302,74 +354,62 @@ class GetSetMethodNormalizerTest extends TestCase
);
}
/**
* @dataProvider provideCallbacks
*/
public function testCallbacks($callbacks, $value, $result, $message)
protected function getNormalizerForMaxDepth(): NormalizerInterface
{
$this->doTestCallbacks($callbacks, $value, $result, $message);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$serializer = new Serializer([$normalizer]);
$normalizer->setSerializer($serializer);
return $normalizer;
}
/**
* @dataProvider provideCallbacks
*/
public function testLegacyCallbacks($callbacks, $value, $result, $message)
protected function getDenormalizerForObjectToPopulate(): DenormalizerInterface
{
$this->doTestCallbacks($callbacks, $value, $result, $message, true);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor());
new Serializer([$normalizer]);
return $normalizer;
}
private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false)
protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface
{
$legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([GetSetMethodNormalizer::CALLBACKS => $callbacks]);
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = new GetSetMethodNormalizer(null, null, $extractor);
$serializer = new Serializer([new ArrayDenormalizer(), $normalizer]);
$normalizer->setSerializer($serializer);
$obj = new GetConstructorDummy('', $value, true);
$this->assertEquals(
$result,
$this->normalizer->normalize($obj, 'any'),
$message
);
return $normalizer;
}
/**
* @expectedException \InvalidArgumentException
*/
public function testUncallableCallbacks()
public function testRejectInvalidKey()
{
$this->doTestUncallableCallbacks();
$this->markTestSkipped('This test makes no sense with the GetSetMethodNormalizer');
}
/**
* @expectedException \InvalidArgumentException
*/
public function testLegacyUncallableCallbacks()
protected function getNormalizerForIgnoredAttributes(): GetSetMethodNormalizer
{
$this->doTestUncallableCallbacks(true);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor());
new Serializer([$normalizer]);
return $normalizer;
}
private function doTestUncallableCallbacks(bool $legacy = false)
protected function getDenormalizerForIgnoredAttributes(): GetSetMethodNormalizer
{
$callbacks = ['bar' => null];
$legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([GetSetMethodNormalizer::CALLBACKS => $callbacks]);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor());
new Serializer([$normalizer]);
$obj = new GetConstructorDummy('baz', 'quux', true);
$this->normalizer->normalize($obj, 'any');
}
public function testIgnoredAttributes()
{
$this->doTestIgnoredAttributes();
return $normalizer;
}
public function testLegacyIgnoredAttributes()
{
$this->doTestIgnoredAttributes(true);
}
private function doTestIgnoredAttributes(bool $legacy = false)
{
$ignoredAttributes = ['foo', 'bar', 'baz', 'camelCase', 'object'];
$legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer([GetSetMethodNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]);
$this->normalizer->setIgnoredAttributes($ignoredAttributes);
$obj = new GetSetDummy();
$obj->setFoo('foo');
@ -382,66 +422,6 @@ class GetSetMethodNormalizerTest extends TestCase
);
}
public function provideCallbacks()
{
return [
[
[
'bar' => function ($bar) {
return 'baz';
},
],
'baz',
['foo' => '', 'bar' => 'baz', 'baz' => true],
'Change a string',
],
[
[
'bar' => function ($bar) {
},
],
'baz',
['foo' => '', 'bar' => null, 'baz' => true],
'Null an item',
],
[
[
'bar' => function ($bar) {
return $bar->format('d-m-Y H:i:s');
},
],
new \DateTime('2011-09-10 06:30:00'),
['foo' => '', 'bar' => '10-09-2011 06:30:00', 'baz' => true],
'Format a date',
],
[
[
'bar' => function ($bars) {
$foos = '';
foreach ($bars as $bar) {
$foos .= $bar->getFoo();
}
return $foos;
},
],
[new GetConstructorDummy('baz', '', false), new GetConstructorDummy('quux', '', false)],
['foo' => '', 'bar' => 'bazquux', 'baz' => true],
'Collect a property',
],
[
[
'bar' => function ($bars) {
return \count($bars);
},
],
[new GetConstructorDummy('baz', '', false), new GetConstructorDummy('quux', '', false)],
['foo' => '', 'bar' => 2, 'baz' => true],
'Count a property',
],
];
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\LogicException
* @expectedExceptionMessage Cannot normalize attribute "object" because the injected serializer is not a normalizer
@ -458,32 +438,6 @@ class GetSetMethodNormalizerTest extends TestCase
$this->normalizer->normalize($obj, 'any');
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
*/
public function testUnableToNormalizeCircularReference()
{
$this->doTestUnableToNormalizeCircularReference();
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
*/
public function testLegacyUnableToNormalizeCircularReference()
{
$this->doTestUnableToNormalizeCircularReference(true);
}
private function doTestUnableToNormalizeCircularReference(bool $legacy = false)
{
$legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer([GetSetMethodNormalizer::CIRCULAR_REFERENCE_LIMIT => 2]);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$obj = new CircularReferenceDummy();
$this->normalizer->normalize($obj);
}
public function testSiblingReference()
{
$serializer = new Serializer([$this->normalizer]);
@ -499,60 +453,17 @@ class GetSetMethodNormalizerTest extends TestCase
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
}
public function testCircularReferenceHandler()
{
$this->doTestCircularReferenceHandler();
}
public function testLegacyCircularReferenceHandler()
{
$this->doTestCircularReferenceHandler(true);
}
private function doTestCircularReferenceHandler(bool $legacy = false)
{
$handler = function ($obj) {
return \get_class($obj);
};
$legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer([GetSetMethodNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler]);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$obj = new CircularReferenceDummy();
$expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy'];
$this->assertEquals($expected, $this->normalizer->normalize($obj));
}
public function testObjectToPopulate()
{
$dummy = new GetSetDummy();
$dummy->setFoo('foo');
$obj = $this->normalizer->denormalize(
['bar' => 'bar'],
__NAMESPACE__.'\GetSetDummy',
null,
[GetSetMethodNormalizer::OBJECT_TO_POPULATE => $dummy]
);
$this->assertEquals($dummy, $obj);
$this->assertEquals('foo', $obj->getFoo());
$this->assertEquals('bar', $obj->getBar());
}
public function testDenormalizeNonExistingAttribute()
{
$this->assertEquals(
new GetSetDummy(),
$this->normalizer->denormalize(['non_existing' => true], __NAMESPACE__.'\GetSetDummy')
$this->normalizer->denormalize(['non_existing' => true], GetSetDummy::class)
);
}
public function testDenormalizeShouldNotSetStaticAttribute()
{
$obj = $this->normalizer->denormalize(['staticObject' => true], __NAMESPACE__.'\GetSetDummy');
$obj = $this->normalizer->denormalize(['staticObject' => true], GetSetDummy::class);
$this->assertEquals(new GetSetDummy(), $obj);
$this->assertNull(GetSetDummy::getStaticObject());
@ -590,46 +501,6 @@ class GetSetMethodNormalizerTest extends TestCase
$this->normalizer->normalize($obj, 'any')
);
}
public function testMaxDepth()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($serializer);
$level1 = new MaxDepthDummy();
$level1->bar = 'level1';
$level2 = new MaxDepthDummy();
$level2->bar = 'level2';
$level1->child = $level2;
$level3 = new MaxDepthDummy();
$level3->bar = 'level3';
$level2->child = $level3;
$level4 = new MaxDepthDummy();
$level4->bar = 'level4';
$level3->child = $level4;
$result = $serializer->normalize($level1, null, [GetSetMethodNormalizer::ENABLE_MAX_DEPTH => true]);
$expected = [
'bar' => 'level1',
'child' => [
'bar' => 'level2',
'child' => [
'bar' => 'level3',
'child' => [
'child' => null,
],
],
],
];
$this->assertEquals($expected, $result);
}
}
class GetSetDummy

View File

@ -13,24 +13,50 @@ namespace Symfony\Component\Serializer\Tests\Normalizer;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummyChild;
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksObject;
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\GroupsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectToPopulateTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait;
class PropertyNormalizerTest extends TestCase
{
use CallbacksTestTrait;
use CircularReferenceTestTrait;
use ConstructorArgumentsTestTrait;
use GroupsTestTrait;
use IgnoredAttributesTestTrait;
use MaxDepthTestTrait;
use ObjectToPopulateTestTrait;
use TypeEnforcementTestTrait;
/**
* @var PropertyNormalizer
*/
private $normalizer;
/**
* @var SerializerInterface
*/
@ -121,146 +147,111 @@ class PropertyNormalizerTest extends TestCase
$this->assertEquals('bar', $obj->getBar());
}
/**
* @dataProvider provideCallbacks
*/
public function testCallbacks($callbacks, $value, $result, $message)
protected function getNormalizerForCallbacks(): PropertyNormalizer
{
$this->doTestCallbacks($callbacks, $value, $result, $message);
return new PropertyNormalizer();
}
/**
* @dataProvider provideCallbacks
*/
public function testLegacyCallbacks($callbacks, $value, $result, $message)
public function testLegacyCallbacks($callbacks, $value, $result)
{
$this->doTestCallbacks($callbacks, $value, $result, $message, true);
}
$this->normalizer->setCallbacks($callbacks);
private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false)
{
$legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([PropertyNormalizer::CALLBACKS => $callbacks]);
$obj = new PropertyConstructorDummy('', $value);
$obj = new CallbacksObject($value);
$this->assertEquals(
$result,
$this->normalizer->normalize($obj, 'any'),
$message
);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testUncallableCallbacks()
{
$this->doTestUncallableCallbacks();
}
/**
* @expectedException \InvalidArgumentException
*/
public function testLegacyUncallableCallbacks()
{
$this->doTestUncallableCallbacks(true);
}
/**
* @expectedException \InvalidArgumentException
*/
private function doTestUncallableCallbacks(bool $legacy = false)
{
$callbacks = ['bar' => null];
$legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([PropertyNormalizer::CALLBACKS => $callbacks]);
$obj = new PropertyConstructorDummy('baz', 'quux');
$this->normalizer->normalize($obj, 'any');
}
public function testIgnoredAttributes()
{
$this->doTestIgnoredAttributes();
}
public function testLegacyIgnoredAttributes()
{
$this->doTestIgnoredAttributes(true);
}
private function doTestIgnoredAttributes(bool $legacy = false)
{
$ignoredAttributes = ['foo', 'bar', 'camelCase'];
$legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer([PropertyNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]);
$obj = new PropertyDummy();
$obj->foo = 'foo';
$obj->setBar('bar');
$this->assertEquals(
[],
$this->normalizer->normalize($obj, 'any')
);
}
public function testGroupsNormalize()
/**
* @dataProvider provideInvalidCallbacks
*/
public function testLegacyUncallableCallbacks($callbacks)
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new PropertyNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$this->expectException(InvalidArgumentException::class);
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj->setBar('bar');
$obj->setFooBar('fooBar');
$obj->setSymfony('symfony');
$obj->setKevin('kevin');
$obj->setCoopTilleuls('coopTilleuls');
$this->assertEquals([
'bar' => 'bar',
], $this->normalizer->normalize($obj, null, [PropertyNormalizer::GROUPS => ['c']]));
// The PropertyNormalizer is also able to hydrate properties from parent classes
$this->assertEquals([
'symfony' => 'symfony',
'foo' => 'foo',
'fooBar' => 'fooBar',
'bar' => 'bar',
'kevin' => 'kevin',
'coopTilleuls' => 'coopTilleuls',
], $this->normalizer->normalize($obj, null, [PropertyNormalizer::GROUPS => ['a', 'c']]));
$this->normalizer->setCallbacks($callbacks);
}
public function testGroupsDenormalize()
protected function getNormalizerForCircularReference(): PropertyNormalizer
{
$normalizer = new PropertyNormalizer();
new Serializer([$normalizer]);
return $normalizer;
}
protected function getSelfReferencingModel()
{
return new PropertyCircularReferenceDummy();
}
public function testLegacyUnableToNormalizeCircularReference()
{
$this->normalizer->setCircularReferenceLimit(2);
new Serializer([$this->normalizer]);
$obj = new PropertyCircularReferenceDummy();
$this->expectException(CircularReferenceException::class);
$this->normalizer->normalize($obj);
}
public function testSiblingReference()
{
$serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($serializer);
$siblingHolder = new PropertySiblingHolder();
$expected = [
'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
];
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
}
public function testLegacyCircularReferenceHandler()
{
$this->normalizer->setCircularReferenceHandler(function ($obj) {
return \get_class($obj);
});
new Serializer([$this->normalizer]);
$obj = new PropertyCircularReferenceDummy();
$expected = ['me' => PropertyCircularReferenceDummy::class];
$this->assertEquals($expected, $this->normalizer->normalize($obj));
}
protected function getDenormalizerForConstructArguments(): PropertyNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new PropertyNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$denormalizer = new PropertyNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
$serializer = new Serializer([$denormalizer]);
$denormalizer->setSerializer($serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
return $denormalizer;
}
$toNormalize = ['foo' => 'foo', 'bar' => 'bar'];
protected function getNormalizerForGroups(): PropertyNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
[PropertyNormalizer::GROUPS => ['a']]
);
$this->assertEquals($obj, $normalized);
return new PropertyNormalizer($classMetadataFactory);
}
$obj->setBar('bar');
protected function getDenormalizerForGroups(): PropertyNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
[PropertyNormalizer::GROUPS => ['a', 'b']]
);
$this->assertEquals($obj, $normalized);
return new PropertyNormalizer($classMetadataFactory);
}
public function testGroupsNormalizeWithNameConverter()
@ -305,132 +296,71 @@ class PropertyNormalizerTest extends TestCase
);
}
public function provideCallbacks()
protected function getDenormalizerForIgnoredAttributes(): PropertyNormalizer
{
return [
[
[
'bar' => function ($bar) {
return 'baz';
},
],
'baz',
['foo' => '', 'bar' => 'baz'],
'Change a string',
],
[
[
'bar' => function ($bar) {
},
],
'baz',
['foo' => '', 'bar' => null],
'Null an item',
],
[
[
'bar' => function ($bar) {
return $bar->format('d-m-Y H:i:s');
},
],
new \DateTime('2011-09-10 06:30:00'),
['foo' => '', 'bar' => '10-09-2011 06:30:00'],
'Format a date',
],
[
[
'bar' => function ($bars) {
$foos = '';
foreach ($bars as $bar) {
$foos .= $bar->getFoo();
}
$normalizer = new PropertyNormalizer();
// instantiate a serializer with the normalizer to handle normalizing recursive structures
new Serializer([$normalizer]);
return $foos;
},
],
[new PropertyConstructorDummy('baz', ''), new PropertyConstructorDummy('quux', '')],
['foo' => '', 'bar' => 'bazquux'],
'Collect a property',
],
[
[
'bar' => function ($bars) {
return \count($bars);
},
],
[new PropertyConstructorDummy('baz', ''), new PropertyConstructorDummy('quux', '')],
['foo' => '', 'bar' => 2],
'Count a property',
],
];
return $normalizer;
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
*/
public function testUnableToNormalizeCircularReference()
protected function getNormalizerForIgnoredAttributes(): PropertyNormalizer
{
$this->doTestUnableToNormalizeCircularReference();
$normalizer = new PropertyNormalizer();
// instantiate a serializer with the normalizer to handle normalizing recursive structures
new Serializer([$normalizer]);
return $normalizer;
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
*/
public function testLegacyUnableToNormalizeCircularReference()
public function testIgnoredAttributesContextDenormalizeInherit()
{
$this->doTestUnableToNormalizeCircularReference(true);
$this->markTestSkipped('This has not been tested previously - did not manage to make the test work');
}
private function doTestUnableToNormalizeCircularReference(bool $legacy = false)
public function testLegacyIgnoredAttributes()
{
$legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer([PropertyNormalizer::CIRCULAR_REFERENCE_LIMIT => 2]);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$ignoredAttributes = ['foo', 'bar', 'camelCase'];
$this->normalizer->setIgnoredAttributes($ignoredAttributes);
$obj = new PropertyCircularReferenceDummy();
$obj = new PropertyDummy();
$obj->foo = 'foo';
$obj->setBar('bar');
$this->normalizer->normalize($obj);
$this->assertEquals(
[],
$this->normalizer->normalize($obj, 'any')
);
}
public function testSiblingReference()
protected function getNormalizerForMaxDepth(): PropertyNormalizer
{
$serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($serializer);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new PropertyNormalizer($classMetadataFactory);
$serializer = new Serializer([$normalizer]);
$normalizer->setSerializer($serializer);
$siblingHolder = new PropertySiblingHolder();
$expected = [
'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
];
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
return $normalizer;
}
public function testCircularReferenceHandler()
protected function getDenormalizerForObjectToPopulate(): PropertyNormalizer
{
$this->doTestCircularReferenceHandler();
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new PropertyNormalizer($classMetadataFactory, null, new PhpDocExtractor());
new Serializer([$normalizer]);
return $normalizer;
}
public function testLegacyCircularReferenceHandler()
protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface
{
$this->doTestCircularReferenceHandler(true);
}
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = new PropertyNormalizer(null, null, $extractor);
$serializer = new Serializer([new ArrayDenormalizer(), $normalizer]);
$normalizer->setSerializer($serializer);
private function doTestCircularReferenceHandler(bool $legacy = false)
{
$handler = function ($obj) {
return \get_class($obj);
};
$legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer([PropertyNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler]);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$obj = new PropertyCircularReferenceDummy();
$expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy'];
$this->assertEquals($expected, $this->normalizer->normalize($obj));
return $normalizer;
}
public function testDenormalizeNonExistingAttribute()
@ -475,42 +405,6 @@ class PropertyNormalizerTest extends TestCase
$this->assertFalse($this->normalizer->supportsNormalization(new StaticPropertyDummy()));
}
public function testMaxDepth()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new PropertyNormalizer($classMetadataFactory);
$serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($serializer);
$level1 = new MaxDepthDummy();
$level1->foo = 'level1';
$level2 = new MaxDepthDummy();
$level2->foo = 'level2';
$level1->child = $level2;
$level3 = new MaxDepthDummy();
$level3->foo = 'level3';
$level2->child = $level3;
$result = $serializer->normalize($level1, null, [PropertyNormalizer::ENABLE_MAX_DEPTH => true]);
$expected = [
'foo' => 'level1',
'child' => [
'foo' => 'level2',
'child' => [
'child' => null,
'bar' => null,
],
'bar' => null,
],
'bar' => null,
];
$this->assertEquals($expected, $result);
}
public function testInheritedPropertiesSupport()
{
$this->assertTrue($this->normalizer->supportsNormalization(new PropertyChildDummy()));