From 2b6ebea73c9664055e4947232ef8ee09955a25ec Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Sat, 6 Apr 2019 13:49:33 +0200 Subject: [PATCH] [serializer] extract normalizer tests to traits --- .../Normalizer/AbstractNormalizer.php | 89 +- .../Normalizer/AbstractObjectNormalizer.php | 61 +- .../Fixtures/DeepObjectPopulateChildDummy.php | 16 + .../Tests/Fixtures/MaxDepthDummy.php | 5 + .../Normalizer/AbstractNormalizerTest.php | 13 - .../AbstractObjectNormalizerTest.php | 54 - .../Features/AttributesTestTrait.php | 108 ++ .../Normalizer/Features/CallbacksObject.php | 18 + .../Features/CallbacksTestTrait.php | 116 +++ .../Features/CircularReferenceTestTrait.php | 46 + .../Features/ConstructorArgumentsObject.php | 17 + .../ConstructorArgumentsTestTrait.php | 61 ++ .../Normalizer/Features/GroupsTestTrait.php | 81 ++ .../Features/IgnoredAttributesTestTrait.php | 89 ++ .../Normalizer/Features/MaxDepthTestTrait.php | 96 ++ .../Tests/Normalizer/Features/ObjectDummy.php | 62 ++ .../Tests/Normalizer/Features/ObjectInner.php | 29 + .../Tests/Normalizer/Features/ObjectOuter.php | 69 ++ .../Features/ObjectToPopulateTestTrait.php | 98 ++ .../Features/SkipNullValuesTestTrait.php | 23 + .../Features/TypeEnforcementNumberObject.php | 16 + .../Features/TypeEnforcementTestTrait.php | 46 + .../Normalizer/GetSetMethodNormalizerTest.php | 391 +++----- .../Tests/Normalizer/ObjectNormalizerTest.php | 940 ++++++------------ .../Normalizer/PropertyNormalizerTest.php | 356 +++---- 25 files changed, 1693 insertions(+), 1207 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/AttributesTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/CircularReferenceTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsObject.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/GroupsTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/IgnoredAttributesTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/MaxDepthTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectDummy.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectInner.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectToPopulateTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/SkipNullValuesTestTrait.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/TypeEnforcementNumberObject.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/Features/TypeEnforcementTestTrait.php diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index cae8593c1595..898963f5d2d9 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -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, diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index cb92412ecc03..92a113e2f125 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -33,13 +33,60 @@ */ 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 = []; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DeepObjectPopulateChildDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DeepObjectPopulateChildDummy.php index 339b25be3566..b72f88190b5a 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/DeepObjectPopulateChildDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DeepObjectPopulateChildDummy.php @@ -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; + } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/MaxDepthDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/MaxDepthDummy.php index aef6dda2966e..92a4045f1eaf 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/MaxDepthDummy.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/MaxDepthDummy.php @@ -42,4 +42,9 @@ public function getChild() { return $this->child; } + + public function getFoo() + { + return $this->foo; + } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php index fbf33aa7c639..72633cee5f4d 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php @@ -10,7 +10,6 @@ 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 @@ public function testGetAllowedAttributesAsObjects() $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(); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 9176f7f82a87..70939567f55c 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -23,8 +23,6 @@ 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 @@ public function testExtraAttributesException() '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 diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/AttributesTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/AttributesTestTrait.php new file mode 100644 index 000000000000..f54f1f280c64 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/AttributesTestTrait.php @@ -0,0 +1,108 @@ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php new file mode 100644 index 000000000000..d2290e6dda0e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php @@ -0,0 +1,18 @@ +bar = $bar; + } + + public function getBar() + { + return $this->bar; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksTestTrait.php new file mode 100644 index 000000000000..ccb54d9c6fd2 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksTestTrait.php @@ -0,0 +1,116 @@ +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']], + ]; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CircularReferenceTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CircularReferenceTestTrait.php new file mode 100644 index 000000000000..1f51cc26a940 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CircularReferenceTestTrait.php @@ -0,0 +1,46 @@ +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)); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsObject.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsObject.php new file mode 100644 index 000000000000..f290925a6eb5 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsObject.php @@ -0,0 +1,17 @@ +foo = $foo; + $this->bar = $bar; + $this->baz = $baz; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php new file mode 100644 index 000000000000..40cd02f19cca --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php @@ -0,0 +1,61 @@ + 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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/GroupsTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/GroupsTestTrait.php new file mode 100644 index 000000000000..1d9777d7a099 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/GroupsTestTrait.php @@ -0,0 +1,81 @@ +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']])); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/IgnoredAttributesTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/IgnoredAttributesTestTrait.php new file mode 100644 index 000000000000..8562ce6f1165 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/IgnoredAttributesTestTrait.php @@ -0,0 +1,89 @@ +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)); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/MaxDepthTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/MaxDepthTestTrait.php new file mode 100644 index 000000000000..229a1f822a99 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/MaxDepthTestTrait.php @@ -0,0 +1,96 @@ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectDummy.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectDummy.php new file mode 100644 index 000000000000..e127724572af --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectDummy.php @@ -0,0 +1,62 @@ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectInner.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectInner.php new file mode 100644 index 000000000000..d4086e93e3ba --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectInner.php @@ -0,0 +1,29 @@ +bar; + } + + public function setBar($bar): void + { + $this->bar = $bar; + } + + public function getFoo() + { + return $this->foo; + } + + public function setFoo($foo): void + { + $this->foo = $foo; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php new file mode 100644 index 000000000000..8193fa8ffe20 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php @@ -0,0 +1,69 @@ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectToPopulateTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectToPopulateTestTrait.php new file mode 100644 index 000000000000..c524c93ed8d8 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectToPopulateTestTrait.php @@ -0,0 +1,98 @@ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/SkipNullValuesTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/SkipNullValuesTestTrait.php new file mode 100644 index 000000000000..3ca0478c4314 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/SkipNullValuesTestTrait.php @@ -0,0 +1,23 @@ +bar = 'present'; + + $normalizer = $this->getNormalizerForSkipNullValues(); + $result = $normalizer->normalize($dummy, null, ['skip_null_values' => true]); + $this->assertSame(['fooBar' => 'present', 'bar' => 'present'], $result); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/TypeEnforcementNumberObject.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/TypeEnforcementNumberObject.php new file mode 100644 index 000000000000..98dcbc86d40d --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/TypeEnforcementNumberObject.php @@ -0,0 +1,16 @@ +number = $number; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/TypeEnforcementTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/TypeEnforcementTestTrait.php new file mode 100644 index 000000000000..598478ac8a81 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/TypeEnforcementTestTrait.php @@ -0,0 +1,46 @@ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 8d9b38312272..049e9cda6951 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -13,21 +13,45 @@ 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 @@ public function testDenormalize() { $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 @@ public function testDenormalizeWithObject() $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 @@ public function testConstructorWArgWithPrivateMutator() $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'); + return new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + } - $this->assertEquals([ - 'bar' => 'bar', - ], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['c']])); + /** + * @dataProvider provideCallbacks + */ + public function testLegacyCallbacks($callbacks, $value, $result) + { + $this->normalizer->setCallbacks($callbacks); - $this->assertEquals([ - 'symfony' => 'symfony', - 'foo' => 'foo', - 'fooBar' => 'fooBar', - 'bar' => 'bar', - 'kevin' => 'kevin', - 'coopTilleuls' => 'coopTilleuls', - ], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['a', 'c']])); + $obj = new CallbacksObject($value); + $this->assertEquals( + $result, + $this->normalizer->normalize($obj, 'any') + ); } - public function testGroupsDenormalize() + 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 @@ public function testGroupsDenormalizeWithNameConverter() ); } - /** - * @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); - /** - * @dataProvider provideCallbacks - */ - public function testLegacyCallbacks($callbacks, $value, $result, $message) - { - $this->doTestCallbacks($callbacks, $value, $result, $message, true); + return $normalizer; } - private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false) + protected function getDenormalizerForObjectToPopulate(): DenormalizerInterface { - $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('', $value, true); - $this->assertEquals( - $result, - $this->normalizer->normalize($obj, 'any'), - $message - ); + return $normalizer; } - /** - * @expectedException \InvalidArgumentException - */ - public function testUncallableCallbacks() + protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface { - $this->doTestUncallableCallbacks(); + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $normalizer = new GetSetMethodNormalizer(null, null, $extractor); + $serializer = new Serializer([new ArrayDenormalizer(), $normalizer]); + $normalizer->setSerializer($serializer); + + return $normalizer; } - /** - * @expectedException \InvalidArgumentException - */ - public function testLegacyUncallableCallbacks() + public function testRejectInvalidKey() { - $this->doTestUncallableCallbacks(true); + $this->markTestSkipped('This test makes no sense with the GetSetMethodNormalizer'); } - private function doTestUncallableCallbacks(bool $legacy = false) + protected function getNormalizerForIgnoredAttributes(): GetSetMethodNormalizer { - $callbacks = ['bar' => null]; - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([GetSetMethodNormalizer::CALLBACKS => $callbacks]); - - $obj = new GetConstructorDummy('baz', 'quux', true); + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor()); + new Serializer([$normalizer]); - $this->normalizer->normalize($obj, 'any'); + return $normalizer; } - public function testIgnoredAttributes() + protected function getDenormalizerForIgnoredAttributes(): GetSetMethodNormalizer { - $this->doTestIgnoredAttributes(); - } + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor()); + new Serializer([$normalizer]); - public function testLegacyIgnoredAttributes() - { - $this->doTestIgnoredAttributes(true); + return $normalizer; } - private function doTestIgnoredAttributes(bool $legacy = false) + public function testLegacyIgnoredAttributes() { $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 @@ private function doTestIgnoredAttributes(bool $legacy = false) ); } - 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 @@ public function testUnableToNormalizeObjectAttribute() $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 @@ public function testSiblingReference() $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 @@ public function testHasGetterNormalize() $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 diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 4c4448dfe4b0..002f9dc3d587 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -16,6 +16,7 @@ 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\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; @@ -31,14 +32,36 @@ 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\NotSerializedConstructorArgumentDummy; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; +use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait; +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\ObjectDummy; +use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectToPopulateTestTrait; +use Symfony\Component\Serializer\Tests\Normalizer\Features\SkipNullValuesTestTrait; +use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait; /** * @author Kévin Dunglas */ class ObjectNormalizerTest extends TestCase { + use AttributesTestTrait; + use CallbacksTestTrait; + use CircularReferenceTestTrait; + use ConstructorArgumentsTestTrait; + use GroupsTestTrait; + use IgnoredAttributesTestTrait; + use MaxDepthTestTrait; + use ObjectToPopulateTestTrait; + use SkipNullValuesTestTrait; + use TypeEnforcementTestTrait; + /** * @var ObjectNormalizer */ @@ -94,7 +117,7 @@ public function testDenormalize() { $obj = $this->normalizer->denormalize( ['foo' => 'foo', 'bar' => 'bar', 'baz' => true, 'fooBar' => 'foobar'], - __NAMESPACE__.'\ObjectDummy', + ObjectDummy::class, 'any' ); $this->assertEquals('foo', $obj->getFoo()); @@ -108,14 +131,14 @@ public function testDenormalizeWithObject() $data->foo = 'foo'; $data->bar = 'bar'; $data->fooBar = 'foobar'; - $obj = $this->normalizer->denormalize($data, __NAMESPACE__.'\ObjectDummy', 'any'); + $obj = $this->normalizer->denormalize($data, ObjectDummy::class, 'any'); $this->assertEquals('foo', $obj->getFoo()); $this->assertEquals('bar', $obj->bar); } public function testDenormalizeNull() { - $this->assertEquals(new ObjectDummy(), $this->normalizer->denormalize(null, __NAMESPACE__.'\ObjectDummy')); + $this->assertEquals(new ObjectDummy(), $this->normalizer->denormalize(null, ObjectDummy::class)); } public function testConstructorDenormalize() @@ -229,106 +252,193 @@ public function testConstructorWithUnknownObjectTypeHintDenormalize() $normalizer->denormalize($data, DummyWithConstructorInexistingObject::class); } - /** - * @expectedException \Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException - * @expectedExceptionMessage Cannot create an instance of Symfony\Component\Serializer\Tests\Normalizer\DummyValueObject from serialized data because its constructor requires parameter "bar" to be present. - */ - public function testConstructorWithMissingData() - { - $data = [ - 'foo' => 10, - ]; + // attributes + protected function getNormalizerForAttributes(): ObjectNormalizer + { $normalizer = new ObjectNormalizer(); + // instantiate a serializer with the normalizer to handle normalizing recursive structures + new Serializer([$normalizer]); - $normalizer->denormalize($data, DummyValueObject::class); + return $normalizer; } - public function testFillWithEmptyDataWhenMissingData() + protected function getDenormalizerForAttributes(): ObjectNormalizer { - $data = [ - 'foo' => 10, - ]; + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor()); + new Serializer([$normalizer]); - $normalizer = new ObjectNormalizer(); + return $normalizer; + } - $result = $normalizer->denormalize($data, DummyValueObject::class, 'json', [ - 'default_constructor_arguments' => [ - DummyValueObject::class => ['foo' => '', 'bar' => '', 'baz' => null], - ], - ]); + public function testAttributesContextDenormalizeConstructor() + { + $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); + $serializer = new Serializer([$normalizer]); + + $objectInner = new ObjectInner(); + $objectInner->bar = 'bar'; + + $obj = new DummyWithConstructorObjectAndDefaultValue('a', $objectInner); - $this->assertEquals(new DummyValueObject(10, '', null), $result); + $context = ['attributes' => ['inner' => ['bar']]]; + $this->assertEquals($obj, $serializer->denormalize([ + 'foo' => 'b', + 'inner' => ['foo' => 'foo', 'bar' => 'bar'], + ], DummyWithConstructorObjectAndDefaultValue::class, null, $context)); } - public function testGroupsNormalize() + public function testNormalizeSameObjectWithDifferentAttributes() { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + $serializer = new Serializer([$this->normalizer]); + $this->normalizer->setSerializer($serializer); - $obj = new GroupDummy(); - $obj->setFoo('foo'); - $obj->setBar('bar'); - $obj->setFooBar('fooBar'); - $obj->setSymfony('symfony'); - $obj->setKevin('kevin'); - $obj->setCoopTilleuls('coopTilleuls'); + $dummy = new ObjectOuter(); + $dummy->foo = new ObjectInner(); + $dummy->foo->foo = 'foo.foo'; + $dummy->foo->bar = 'foo.bar'; - $this->assertEquals([ - 'bar' => 'bar', - ], $this->normalizer->normalize($obj, null, [ObjectNormalizer::GROUPS => ['c']])); + $dummy->bar = new ObjectInner(); + $dummy->bar->foo = 'bar.foo'; + $dummy->bar->bar = 'bar.bar'; $this->assertEquals([ - 'symfony' => 'symfony', - 'foo' => 'foo', - 'fooBar' => 'fooBar', - 'bar' => 'bar', - 'kevin' => 'kevin', - 'coopTilleuls' => 'coopTilleuls', - ], $this->normalizer->normalize($obj, null, [ObjectNormalizer::GROUPS => ['a', 'c']])); + 'foo' => [ + 'bar' => 'foo.bar', + ], + 'bar' => [ + 'foo' => 'bar.foo', + ], + ], $this->normalizer->normalize($dummy, 'json', [ + 'attributes' => [ + 'foo' => ['bar'], + 'bar' => ['foo'], + ], + ])); } - public function testGroupsDenormalize() - { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + // callbacks - $obj = new GroupDummy(); - $obj->setFoo('foo'); + protected function getNormalizerForCallbacks(): ObjectNormalizer + { + return new ObjectNormalizer(); + } - $toNormalize = ['foo' => 'foo', 'bar' => 'bar']; + /** + * @dataProvider provideCallbacks + */ + public function testLegacyCallbacks($callbacks, $value, $result) + { + $this->normalizer->setCallbacks($callbacks); + $obj = new CallbacksObject($value); - $normalized = $this->normalizer->denormalize( - $toNormalize, - 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', - null, - [ObjectNormalizer::GROUPS => ['a']] + $this->assertEquals( + $result, + $this->normalizer->normalize($obj, 'any') ); - $this->assertEquals($obj, $normalized); + } - $obj->setBar('bar'); + /** + * @dataProvider provideInvalidCallbacks + */ + public function testLegacyUncallableCallbacks($callbacks) + { + $this->expectException(\InvalidArgumentException::class); - $normalized = $this->normalizer->denormalize( - $toNormalize, - 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', - null, - [ObjectNormalizer::GROUPS => ['a', 'b']] - ); - $this->assertEquals($obj, $normalized); + $this->normalizer->setCallbacks($callbacks); + } + + // circular reference + + protected function getNormalizerForCircularReference(): ObjectNormalizer + { + $normalizer = new ObjectNormalizer(); + new Serializer([$normalizer]); + + return $normalizer; + } + + protected function getSelfReferencingModel() + { + return new CircularReferenceDummy(); + } + + public function testLegacyUnableToNormalizeCircularReference() + { + $this->normalizer->setCircularReferenceLimit(2); + $serializer = new Serializer([$this->normalizer]); + $this->normalizer->setSerializer($serializer); + + $obj = new CircularReferenceDummy(); + + $this->expectException(CircularReferenceException::class); + $this->normalizer->normalize($obj); + } + + public function testSiblingReference() + { + $serializer = new Serializer([$this->normalizer]); + $this->normalizer->setSerializer($serializer); + + $siblingHolder = new SiblingHolder(); + + $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 testNormalizeNoPropertyInGroup() + public function testLegacyCircularReferenceHandler() + { + new Serializer([$this->normalizer]); + + $obj = new CircularReferenceDummy(); + $expected = ['me' => CircularReferenceDummy::class]; + + $this->normalizer->setCircularReferenceHandler(function ($obj, string $format, array $context) { + $this->assertInstanceOf(CircularReferenceDummy::class, $obj); + $this->assertSame('test', $format); + $this->assertArrayHasKey('foo', $context); + + return \get_class($obj); + }); + $this->assertEquals($expected, $this->normalizer->normalize($obj, 'test', ['foo' => 'bar'])); + } + + // constructor arguments + + protected function getDenormalizerForConstructArguments(): ObjectNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + $denormalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $serializer = new Serializer([$denormalizer]); + $denormalizer->setSerializer($serializer); - $obj = new GroupDummy(); - $obj->setFoo('foo'); + return $denormalizer; + } + + // groups + + protected function getNormalizerForGroups(): ObjectNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory); + // instantiate a serializer with the normalizer to handle normalizing recursive structures + new Serializer([$normalizer]); - $this->assertEquals([], $this->normalizer->normalize($obj, null, ['groups' => ['notExist']])); + return $normalizer; + } + + protected function getDenormalizerForGroups(): ObjectNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + + return new ObjectNormalizer($classMetadataFactory); } public function testGroupsNormalizeWithNameConverter() @@ -373,119 +483,30 @@ public function testGroupsDenormalizeWithNameConverter() ); } - public function testMetadataAwareNameConvertorWithNotSerializedConstructorParameter() - { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); - $this->normalizer->setSerializer($this->serializer); - - $obj = new NotSerializedConstructorArgumentDummy('buz'); - $obj->setBar('xyz'); - - $this->assertEquals( - $obj, - $this->normalizer->denormalize(['bar' => 'xyz'], - 'Symfony\Component\Serializer\Tests\Fixtures\NotSerializedConstructorArgumentDummy', - null, - [ObjectNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS => [ - 'Symfony\Component\Serializer\Tests\Fixtures\NotSerializedConstructorArgumentDummy' => ['foo' => 'buz'], - ]] - ) - ); - } - - public function testObjectToPopulateNoMatch() - { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor()); - new Serializer([$this->normalizer]); - - $objectToPopulate = new ObjectInner(); - $objectToPopulate->foo = 'foo'; - - $outer = $this->normalizer->denormalize([ - 'foo' => 'foo', - 'inner' => [ - 'bar' => 'bar', - ], - ], ObjectOuter::class, null, [ObjectNormalizer::OBJECT_TO_POPULATE => $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); - } - - /** - * @dataProvider provideCallbacks - */ - public function testCallbacks($callbacks, $value, $result, $message) - { - $this->doTestCallbacks($callbacks, $value, $result, $message); - } - - /** - * @dataProvider provideCallbacks - */ - public function testLegacyCallbacks($callbacks, $value, $result, $message) - { - $this->doTestCallbacks($callbacks, $value, $result, $message); - } - - private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false) - { - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([ObjectNormalizer::CALLBACKS => $callbacks]); - $obj = new ObjectConstructorDummy('', $value, true); + // ignored attributes - $this->assertEquals( - $result, - $this->normalizer->normalize($obj, 'any'), - $message - ); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testUncallableCallbacks() + protected function getNormalizerForIgnoredAttributes(): ObjectNormalizer { - $this->doTestUncallableCallbacks(); - } + $normalizer = new ObjectNormalizer(); + // instantiate a serializer with the normalizer to handle normalizing recursive structures + new Serializer([$normalizer]); - /** - * @expectedException \InvalidArgumentException - */ - public function testLegacyUncallableCallbacks() - { - $this->doTestUncallableCallbacks(true); + return $normalizer; } - private function doTestUncallableCallbacks(bool $legacy = false) + protected function getDenormalizerForIgnoredAttributes(): ObjectNormalizer { - $callbacks = ['bar' => null]; - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([ObjectNormalizer::CALLBACKS => $callbacks]); - - $obj = new ObjectConstructorDummy('baz', 'quux', true); - - $this->normalizer->normalize($obj, 'any'); - } + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor()); + new Serializer([$normalizer]); - 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([ObjectNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]); + $this->normalizer->setIgnoredAttributes($ignoredAttributes); $obj = new ObjectDummy(); $obj->setFoo('foo'); @@ -498,7 +519,7 @@ private function doTestIgnoredAttributes(bool $legacy = false) ); $ignoredAttributes = ['foo', 'baz', 'camelCase', 'object']; - $legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer([ObjectNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]); + $this->normalizer->setIgnoredAttributes($ignoredAttributes); $this->assertEquals( [ @@ -509,242 +530,33 @@ private function doTestIgnoredAttributes(bool $legacy = false) ); } - public function testIgnoredAttributesDenormalize() - { - $this->doTestIgnoredAttributesDenormalize(); - } - public function testLegacyIgnoredAttributesDenormalize() - { - $this->doTestIgnoredAttributesDenormalize(true); - } - - private function doTestIgnoredAttributesDenormalize(bool $legacy = false) { $ignoredAttributes = ['fooBar', 'bar', 'baz']; - $legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer([ObjectNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]); - - $obj = new ObjectDummy(); - $obj->setFoo('foo'); - - $this->assertEquals( - $obj, - $this->normalizer->denormalize(['fooBar' => 'fooBar', 'foo' => 'foo', 'baz' => 'baz'], __NAMESPACE__.'\ObjectDummy') - ); - } - - public function provideCallbacks() - { - return [ - [ - [ - 'bar' => function ($bar) { - $this->assertEquals('baz', $bar); - - return 'baz'; - }, - ], - 'baz', - ['foo' => '', 'bar' => 'baz', 'baz' => true], - 'Change a string', - ], - [ - [ - 'bar' => function ($value, $object, $attributeName, $format, $context) { - $this->assertSame('baz', $value); - $this->assertInstanceOf(ObjectConstructorDummy::class, $object); - $this->assertSame('bar', $attributeName); - $this->assertSame('any', $format); - $this->assertArrayHasKey('circular_reference_limit_counters', $context); - }, - ], - '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 ObjectConstructorDummy('baz', '', false), new ObjectConstructorDummy('quux', '', false)], - ['foo' => '', 'bar' => 'bazquux', 'baz' => true], - 'Collect a property', - ], - [ - [ - 'bar' => function ($bars) { - return \count($bars); - }, - ], - [new ObjectConstructorDummy('baz', '', false), new ObjectConstructorDummy('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 - */ - public function testUnableToNormalizeObjectAttribute() - { - $serializer = $this->getMockBuilder('Symfony\Component\Serializer\SerializerInterface')->getMock(); - $this->normalizer->setSerializer($serializer); - - $obj = new ObjectDummy(); - $object = new \stdClass(); - $obj->setObject($object); - - $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([ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT => 2]); - $serializer = new Serializer([$this->normalizer]); - $this->normalizer->setSerializer($serializer); - - $obj = new CircularReferenceDummy(); - - $this->normalizer->normalize($obj); - } - - public function testSiblingReference() - { - $serializer = new Serializer([$this->normalizer]); - $this->normalizer->setSerializer($serializer); - - $siblingHolder = new SiblingHolder(); - - $expected = [ - 'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'], - 'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'], - 'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'], - ]; - $this->assertEquals($expected, $this->normalizer->normalize($siblingHolder)); - } + $this->normalizer->setIgnoredAttributes($ignoredAttributes); - public function testCircularReferenceHandler() - { - $this->doTestCircularReferenceHandler(); - } - - public function testLegacyCircularReferenceHandler() - { - $this->doTestCircularReferenceHandler(true); - } - - private function doTestCircularReferenceHandler(bool $legacy = false) - { - $this->createNormalizerWithCircularReferenceHandler(function ($obj) { - return \get_class($obj); - }, $legacy); - - $obj = new CircularReferenceDummy(); - $expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy']; - $this->assertEquals($expected, $this->normalizer->normalize($obj)); - - $this->createNormalizerWithCircularReferenceHandler(function ($obj, string $format, array $context) { - $this->assertInstanceOf(CircularReferenceDummy::class, $obj); - $this->assertSame('test', $format); - $this->arrayHasKey('foo', $context); - - return \get_class($obj); - }, $legacy); - $this->assertEquals($expected, $this->normalizer->normalize($obj, 'test', ['foo' => 'bar'])); - } - - private function createNormalizerWithCircularReferenceHandler(callable $handler, bool $legacy) - { - $legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer([ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler]); - $this->serializer = new Serializer([$this->normalizer]); - $this->normalizer->setSerializer($this->serializer); - } - - public function testDenormalizeNonExistingAttribute() - { - $this->assertEquals( - new ObjectDummy(), - $this->normalizer->denormalize(['non_existing' => true], __NAMESPACE__.'\ObjectDummy') - ); - } - - public function testNoTraversableSupport() - { - $this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject())); - } - - public function testNormalizeStatic() - { - $this->assertEquals(['foo' => 'K'], $this->normalizer->normalize(new ObjectWithStaticPropertiesAndMethods())); - } - - public function testNormalizeUpperCaseAttributes() - { - $this->assertEquals(['Foo' => 'Foo', 'Bar' => 'BarBar'], $this->normalizer->normalize(new ObjectWithUpperCaseAttributeNames())); - } - - public function testNormalizeNotSerializableContext() - { - $objectDummy = new ObjectDummy(); - $expected = [ - 'foo' => null, - 'baz' => null, - 'fooBar' => '', - 'camelCase' => null, - 'object' => null, - 'bar' => null, - ]; + $obj = new ObjectDummy(); + $obj->setFoo('foo'); - $this->assertEquals($expected, $this->normalizer->normalize($objectDummy, null, ['not_serializable' => function () { - }])); + $this->assertEquals( + $obj, + $this->normalizer->denormalize(['fooBar' => 'fooBar', 'foo' => 'foo', 'baz' => 'baz'], ObjectDummy::class) + ); } - public function testMaxDepth() - { - $this->doTestMaxDepth(); - } + // max depth - public function testLegacyMaxDepth() + protected function getNormalizerForMaxDepth(): ObjectNormalizer { - $this->doTestMaxDepth(true); + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); + + return $normalizer; } - private function doTestMaxDepth(bool $legacy = false) + public function testLegacyMaxDepth() { $level1 = new MaxDepthDummy(); $level1->foo = 'level1'; @@ -757,20 +569,20 @@ private function doTestMaxDepth(bool $legacy = false) $level3->foo = 'level3'; $level2->child = $level3; - $this->createNormalizerWithMaxDepthHandler(null, $legacy); + $this->createNormalizerWithMaxDepthHandler(null); $result = $this->serializer->normalize($level1, null, [ObjectNormalizer::ENABLE_MAX_DEPTH => true]); $expected = [ 'bar' => null, 'foo' => 'level1', 'child' => [ + 'bar' => null, + 'foo' => 'level2', + 'child' => [ 'bar' => null, - 'foo' => 'level2', - 'child' => [ - 'bar' => null, - 'child' => null, - ], + 'child' => null, ], + ], ]; $this->assertEquals($expected, $result); @@ -791,7 +603,7 @@ private function doTestMaxDepth(bool $legacy = false) $this->createNormalizerWithMaxDepthHandler(function () { return 'handler'; - }, $legacy); + }); $result = $this->serializer->normalize($level1, null, [ObjectNormalizer::ENABLE_MAX_DEPTH => true]); $this->assertEquals($expected, $result); @@ -803,29 +615,105 @@ private function doTestMaxDepth(bool $legacy = false) $this->assertArrayHasKey(ObjectNormalizer::ENABLE_MAX_DEPTH, $context); return 'handler'; - }, $legacy); + }); $this->serializer->normalize($level1, 'test', [ObjectNormalizer::ENABLE_MAX_DEPTH => true]); } - private function createNormalizerWithMaxDepthHandler(callable $handler = null, bool $legacy = false) + private function createNormalizerWithMaxDepthHandler(callable $handler = null) { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - if ($legacy) { - $this->createNormalizer([], $classMetadataFactory); - if (null !== $handler) { - $this->normalizer->setMaxDepthHandler($handler); - } - } else { - $context = []; - if (null !== $handler) { - $context[ObjectNormalizer::MAX_DEPTH_HANDLER] = $handler; - } - $this->createNormalizer($context, $classMetadataFactory); + $this->createNormalizer([], $classMetadataFactory); + if (null !== $handler) { + $this->normalizer->setMaxDepthHandler($handler); } $this->serializer = new Serializer([$this->normalizer]); $this->normalizer->setSerializer($this->serializer); } + // object to populate + + protected function getDenormalizerForObjectToPopulate(): ObjectNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor()); + new Serializer([$normalizer]); + + return $normalizer; + } + + // skip null + + protected function getNormalizerForSkipNullValues(): ObjectNormalizer + { + return new ObjectNormalizer(); + } + + // type enforcement + + protected function getDenormalizerForTypeEnforcement(): ObjectNormalizer + { + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $normalizer = new ObjectNormalizer(null, null, null, $extractor); + new Serializer([new ArrayDenormalizer(), $normalizer]); + + return $normalizer; + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\LogicException + * @expectedExceptionMessage Cannot normalize attribute "object" because the injected serializer is not a normalizer + */ + public function testUnableToNormalizeObjectAttribute() + { + $serializer = $this->getMockBuilder('Symfony\Component\Serializer\SerializerInterface')->getMock(); + $this->normalizer->setSerializer($serializer); + + $obj = new ObjectDummy(); + $object = new \stdClass(); + $obj->setObject($object); + + $this->normalizer->normalize($obj, 'any'); + } + + public function testDenormalizeNonExistingAttribute() + { + $this->assertEquals( + new ObjectDummy(), + $this->normalizer->denormalize(['non_existing' => true], ObjectDummy::class) + ); + } + + public function testNoTraversableSupport() + { + $this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject())); + } + + public function testNormalizeStatic() + { + $this->assertEquals(['foo' => 'K'], $this->normalizer->normalize(new ObjectWithStaticPropertiesAndMethods())); + } + + public function testNormalizeUpperCaseAttributes() + { + $this->assertEquals(['Foo' => 'Foo', 'Bar' => 'BarBar'], $this->normalizer->normalize(new ObjectWithUpperCaseAttributeNames())); + } + + public function testNormalizeNotSerializableContext() + { + $objectDummy = new ObjectDummy(); + $expected = [ + 'foo' => null, + 'baz' => null, + 'fooBar' => '', + 'camelCase' => null, + 'object' => null, + 'bar' => null, + ]; + + $this->assertEquals($expected, $this->normalizer->normalize($objectDummy, null, ['not_serializable' => function () { + }])); + } + /** * @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException */ @@ -863,41 +751,6 @@ public function testAcceptJsonNumber() $this->assertSame(10.0, $serializer->denormalize(['number' => 10], JsonNumber::class, 'jsonld')->number); } - /** - * @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException - * @expectedExceptionMessage The type of the "date" attribute for class "Symfony\Component\Serializer\Tests\Normalizer\ObjectOuter" must be one of "DateTimeInterface" ("string" given). - */ - public function testRejectInvalidType() - { - $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); - $serializer = new Serializer([$normalizer]); - - $serializer->denormalize(['date' => 'foo'], ObjectOuter::class); - } - - /** - * @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException - * @expectedExceptionMessage The type of the key "a" must be "int" ("string" given). - */ - public function testRejectInvalidKey() - { - $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); - $normalizer = new ObjectNormalizer(null, null, null, $extractor); - $serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]); - - $serializer->denormalize(['inners' => ['a' => ['foo' => 1]]], ObjectOuter::class); - } - - public function testDoNotRejectInvalidTypeOnDisableTypeEnforcementContextOption() - { - $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor()]); - $normalizer = new ObjectNormalizer(null, null, null, $extractor); - $serializer = new Serializer([$normalizer]); - $context = [ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true]; - - $this->assertSame('foo', $serializer->denormalize(['number' => 'foo'], JsonNumber::class, null, $context)->number); - } - public function testExtractAttributesRespectsFormat() { $normalizer = new FormatAndContextAwareNormalizer(); @@ -920,111 +773,6 @@ public function testExtractAttributesRespectsContext() $this->assertSame(['foo' => 'bar', 'bar' => 'foo'], $normalizer->normalize($data, null, ['include_foo_and_bar' => true])); } - public function testAttributesContextNormalize() - { - $normalizer = new ObjectNormalizer(); - $serializer = new Serializer([$normalizer]); - - $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'], - ], - $serializer->normalize($objectDummy, null, $context) - ); - - $context = ['attributes' => ['foo', 'baz', 'object']]; - $this->assertEquals( - [ - 'foo' => 'foo', - 'baz' => true, - 'object' => ['foo' => 'innerFoo', 'bar' => 'innerBar'], - ], - $serializer->normalize($objectDummy, null, $context) - ); - } - - public function testAttributesContextDenormalize() - { - $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); - $serializer = new Serializer([$normalizer]); - - $objectInner = new ObjectInner(); - $objectInner->foo = 'innerFoo'; - - $objectOuter = new ObjectOuter(); - $objectOuter->bar = 'bar'; - $objectOuter->setInner($objectInner); - - $context = ['attributes' => ['bar', 'inner' => ['foo']]]; - $this->assertEquals($objectOuter, $serializer->denormalize( - [ - 'foo' => 'foo', - 'bar' => 'bar', - 'date' => '2017-02-03', - 'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'], - ], ObjectOuter::class, null, $context)); - } - - public function testAttributesContextDenormalizeConstructor() - { - $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); - $serializer = new Serializer([$normalizer]); - - $objectInner = new ObjectInner(); - $objectInner->bar = 'bar'; - - $obj = new DummyWithConstructorObjectAndDefaultValue('a', $objectInner); - - $context = ['attributes' => ['inner' => ['bar']]]; - $this->assertEquals($obj, $serializer->denormalize([ - 'foo' => 'b', - 'inner' => ['foo' => 'foo', 'bar' => 'bar'], - ], DummyWithConstructorObjectAndDefaultValue::class, null, $context)); - } - - public function testNormalizeSameObjectWithDifferentAttributes() - { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $serializer = new Serializer([$this->normalizer]); - $this->normalizer->setSerializer($serializer); - - $dummy = new ObjectOuter(); - $dummy->foo = new ObjectInner(); - $dummy->foo->foo = 'foo.foo'; - $dummy->foo->bar = 'foo.bar'; - - $dummy->bar = new ObjectInner(); - $dummy->bar->foo = 'bar.foo'; - $dummy->bar->bar = 'bar.bar'; - - $this->assertEquals([ - 'foo' => [ - 'bar' => 'foo.bar', - ], - 'bar' => [ - 'foo' => 'bar.foo', - ], - ], $this->normalizer->normalize($dummy, 'json', [ - 'attributes' => [ - 'foo' => ['bar'], - 'bar' => ['foo'], - ], - ])); - } - public function testAdvancedNameConverter() { $nameConverter = new class() implements AdvancedNameConverterInterface { @@ -1040,7 +788,7 @@ public function denormalize($propertyName, string $class = null, string $format }; $normalizer = new ObjectNormalizer(null, $nameConverter); - $this->assertArrayHasKey('foo-Symfony\Component\Serializer\Tests\Normalizer\ObjectDummy-json-bar', $normalizer->normalize(new ObjectDummy(), 'json', ['foo' => 'bar'])); + $this->assertArrayHasKey('foo-Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectDummy-json-bar', $normalizer->normalize(new ObjectDummy(), 'json', ['foo' => 'bar'])); } public function testDefaultObjectClassResolver() @@ -1096,65 +844,6 @@ public function testObjectClassResolver() } } -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; - } -} - class ProxyObjectDummy extends ObjectDummy { public $unwantedProperty; @@ -1363,19 +1052,6 @@ public function __construct($id, Unknown $unknown) { } } -class DummyValueObject -{ - private $foo; - private $bar; - private $baz; - - public function __construct($foo, $bar, $baz) - { - $this->foo = $foo; - $this->bar = $bar; - $this->baz = $baz; - } -} class JsonNumber { diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 26bccdfcad2e..b1c9fcef003b 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -13,24 +13,50 @@ 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 @@ public function testConstructorDenormalizeWithNullArgument() $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) - { - $this->doTestCallbacks($callbacks, $value, $result, $message, true); - } - - private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false) + public function testLegacyCallbacks($callbacks, $value, $result) { - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([PropertyNormalizer::CALLBACKS => $callbacks]); + $this->normalizer->setCallbacks($callbacks); - $obj = new PropertyConstructorDummy('', $value); + $obj = new CallbacksObject($value); $this->assertEquals( $result, - $this->normalizer->normalize($obj, 'any'), - $message + $this->normalizer->normalize($obj, 'any') ); } /** - * @expectedException \InvalidArgumentException + * @dataProvider provideInvalidCallbacks */ - public function testUncallableCallbacks() + public function testLegacyUncallableCallbacks($callbacks) { - $this->doTestUncallableCallbacks(); - } + $this->expectException(InvalidArgumentException::class); - /** - * @expectedException \InvalidArgumentException - */ - public function testLegacyUncallableCallbacks() - { - $this->doTestUncallableCallbacks(true); + $this->normalizer->setCallbacks($callbacks); } - /** - * @expectedException \InvalidArgumentException - */ - private function doTestUncallableCallbacks(bool $legacy = false) + protected function getNormalizerForCircularReference(): PropertyNormalizer { - $callbacks = ['bar' => null]; - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([PropertyNormalizer::CALLBACKS => $callbacks]); - - $obj = new PropertyConstructorDummy('baz', 'quux'); + $normalizer = new PropertyNormalizer(); + new Serializer([$normalizer]); - $this->normalizer->normalize($obj, 'any'); + return $normalizer; } - public function testIgnoredAttributes() + protected function getSelfReferencingModel() { - $this->doTestIgnoredAttributes(); + return new PropertyCircularReferenceDummy(); } - public function testLegacyIgnoredAttributes() + public function testLegacyUnableToNormalizeCircularReference() { - $this->doTestIgnoredAttributes(true); + $this->normalizer->setCircularReferenceLimit(2); + new Serializer([$this->normalizer]); + + $obj = new PropertyCircularReferenceDummy(); + + $this->expectException(CircularReferenceException::class); + $this->normalizer->normalize($obj); } - private function doTestIgnoredAttributes(bool $legacy = false) + public function testSiblingReference() { - $ignoredAttributes = ['foo', 'bar', 'camelCase']; - $legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer([PropertyNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]); + $serializer = new Serializer([$this->normalizer]); + $this->normalizer->setSerializer($serializer); - $obj = new PropertyDummy(); - $obj->foo = 'foo'; - $obj->setBar('bar'); + $siblingHolder = new PropertySiblingHolder(); - $this->assertEquals( - [], - $this->normalizer->normalize($obj, 'any') - ); + $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 testGroupsNormalize() + public function testLegacyCircularReferenceHandler() { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new PropertyNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + $this->normalizer->setCircularReferenceHandler(function ($obj) { + return \get_class($obj); + }); - $obj = new GroupDummy(); - $obj->setFoo('foo'); - $obj->setBar('bar'); - $obj->setFooBar('fooBar'); - $obj->setSymfony('symfony'); - $obj->setKevin('kevin'); - $obj->setCoopTilleuls('coopTilleuls'); + new Serializer([$this->normalizer]); - $this->assertEquals([ - 'bar' => 'bar', - ], $this->normalizer->normalize($obj, null, [PropertyNormalizer::GROUPS => ['c']])); + $obj = new PropertyCircularReferenceDummy(); - // 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']])); + $expected = ['me' => PropertyCircularReferenceDummy::class]; + $this->assertEquals($expected, $this->normalizer->normalize($obj)); } - public function testGroupsDenormalize() + 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 @@ public function testGroupsDenormalizeWithNameConverter() ); } - 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(); - } - - 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', - ], - ]; + $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 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); - - $siblingHolder = new PropertySiblingHolder(); + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new PropertyNormalizer($classMetadataFactory); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); - $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]); - public function testLegacyCircularReferenceHandler() - { - $this->doTestCircularReferenceHandler(true); + return $normalizer; } - private function doTestCircularReferenceHandler(bool $legacy = false) + protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface { - $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(); + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $normalizer = new PropertyNormalizer(null, null, $extractor); + $serializer = new Serializer([new ArrayDenormalizer(), $normalizer]); + $normalizer->setSerializer($serializer); - $expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy']; - $this->assertEquals($expected, $this->normalizer->normalize($obj)); + return $normalizer; } public function testDenormalizeNonExistingAttribute() @@ -475,42 +405,6 @@ public function testNoStaticPropertySupport() $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()));