From cbbda0b4ad90ed24bf87963e594da8e3538e3d6a Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Sat, 6 Apr 2019 13:49:33 +0200 Subject: [PATCH] extract normalizer tests for the context options into traits to reuse them for all normalizers --- .../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 | 13 + .../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 | 9 + .../Tests/Normalizer/Features/ObjectOuter.php | 46 ++ .../Features/ObjectToPopulateTestTrait.php | 76 +++ .../Features/SkipNullValuesTestTrait.php | 23 + .../Features/TypeEnforcementNumberObject.php | 16 + .../Features/TypeEnforcementTestTrait.php | 46 ++ .../Normalizer/GetSetMethodNormalizerTest.php | 169 ++--- .../Tests/Normalizer/ObjectNormalizerTest.php | 615 ++++-------------- .../Normalizer/PropertyNormalizerTest.php | 292 +++------ 25 files changed, 1320 insertions(+), 899 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 aac72010da601..a97dc25ecd2dd 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 599f503e42421..49aa0f8351267 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 339b25be3566a..b72f88190b5ac 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 aef6dda2966eb..92a4045f1eaf2 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 fbf33aa7c6393..72633cee5f4d1 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 9176f7f82a87d..70939567f55c7 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 0000000000000..f54f1f280c645 --- /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 0000000000000..c81e23f05633c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php @@ -0,0 +1,13 @@ +bar = $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 0000000000000..ccb54d9c6fd25 --- /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 0000000000000..1f51cc26a9407 --- /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 0000000000000..f290925a6eb5e --- /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 0000000000000..40cd02f19cca8 --- /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 0000000000000..1d9777d7a099e --- /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 0000000000000..8562ce6f11657 --- /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 0000000000000..229a1f822a99c --- /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 0000000000000..e127724572afd --- /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 0000000000000..be4f5485f244c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectInner.php @@ -0,0 +1,9 @@ +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 0000000000000..ade03c264692f --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectToPopulateTestTrait.php @@ -0,0 +1,76 @@ +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 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, + ]; + + $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 0000000000000..3ca0478c43149 --- /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 0000000000000..98dcbc86d40d1 --- /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 0000000000000..598478ac8a811 --- /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 8d9b38312272b..1a171a0298b22 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -13,21 +13,37 @@ 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\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\ConstructorArgumentsTestTrait; +use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait; +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 GroupsTestTrait; + use ObjectToPopulateTestTrait; + use MaxDepthTestTrait; + use ConstructorArgumentsTestTrait; + use TypeEnforcementTestTrait; + /** * @var GetSetMethodNormalizer */ @@ -89,7 +105,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 +135,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 +218,62 @@ public function testConstructorWArgWithPrivateMutator() $this->assertEquals('bar', $obj->getFoo()); } - public function testGroupsNormalize() + protected function getNormalizerForGroups(): 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); + } - $this->assertEquals([ - 'bar' => 'bar', - ], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['c']])); + protected function getDenormalizerForGroups(): GetSetMethodNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->assertEquals([ - 'symfony' => 'symfony', - 'foo' => 'foo', - 'fooBar' => 'fooBar', - 'bar' => 'bar', - 'kevin' => 'kevin', - 'coopTilleuls' => 'coopTilleuls', - ], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['a', 'c']])); + return new GetSetMethodNormalizer($classMetadataFactory); } - public function testGroupsDenormalize() + protected function getDenormalizerForObjectToPopulate(): DenormalizerInterface { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new GetSetMethodNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor()); + new Serializer([$normalizer]); - $obj = new GroupDummy(); - $obj->setFoo('foo'); + return $normalizer; + } + + protected function getNormalizerForMaxDepth(): NormalizerInterface + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); - $toNormalize = ['foo' => 'foo', 'bar' => 'bar']; + return $normalizer; + } - $normalized = $this->normalizer->denormalize( - $toNormalize, - 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', - null, - [GetSetMethodNormalizer::GROUPS => ['a']] - ); - $this->assertEquals($obj, $normalized); + protected function getDenormalizerForConstructArguments(): DenormalizerInterface + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $denormalizer = new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $serializer = new Serializer([$denormalizer]); + $denormalizer->setSerializer($serializer); - $obj->setBar('bar'); + return $denormalizer; + } - $normalized = $this->normalizer->denormalize( - $toNormalize, - 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', - null, - [GetSetMethodNormalizer::GROUPS => ['a', 'b']] - ); - $this->assertEquals($obj, $normalized); + protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface + { + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $normalizer = new GetSetMethodNormalizer(null, null, $extractor); + $serializer = new Serializer([new ArrayDenormalizer(), $normalizer]); + $normalizer->setSerializer($serializer); + + return $normalizer; + } + + public function testRejectInvalidKey() + { + $this->markTestSkipped('This test makes no sense with the GetSetMethodNormalizer'); } public function testGroupsNormalizeWithNameConverter() @@ -525,34 +541,17 @@ private function doTestCircularReferenceHandler(bool $legacy = false) $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 +589,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 b4e080528165f..69124a85316a7 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; @@ -30,15 +31,36 @@ 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\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 IgnoredAttributesTestTrait; + use GroupsTestTrait; + use CircularReferenceTestTrait; + use ObjectToPopulateTestTrait; + use MaxDepthTestTrait; + use ConstructorArgumentsTestTrait; + use TypeEnforcementTestTrait; + use CallbacksTestTrait; + use SkipNullValuesTestTrait; + /** * @var ObjectNormalizer */ @@ -94,7 +116,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 +130,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 +251,64 @@ 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() + protected function getNormalizerForGroups(): ObjectNormalizer { - $data = [ - 'foo' => 10, - ]; - - $normalizer = new 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]); - $normalizer->denormalize($data, DummyValueObject::class); + return $normalizer; } - public function testFillWithEmptyDataWhenMissingData() + protected function getDenormalizerForGroups(): ObjectNormalizer { - $data = [ - 'foo' => 10, - ]; - - $normalizer = new ObjectNormalizer(); - - $result = $normalizer->denormalize($data, DummyValueObject::class, 'json', [ - 'default_constructor_arguments' => [ - DummyValueObject::class => ['foo' => '', 'bar' => '', 'baz' => null], - ], - ]); + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->assertEquals(new DummyValueObject(10, '', null), $result); + return new ObjectNormalizer($classMetadataFactory); } - public function testGroupsNormalize() + protected function getDenormalizerForObjectToPopulate(): ObjectNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($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'); + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor()); + new Serializer([$normalizer]); - $this->assertEquals([ - 'bar' => 'bar', - ], $this->normalizer->normalize($obj, null, [ObjectNormalizer::GROUPS => ['c']])); - - $this->assertEquals([ - 'symfony' => 'symfony', - 'foo' => 'foo', - 'fooBar' => 'fooBar', - 'bar' => 'bar', - 'kevin' => 'kevin', - 'coopTilleuls' => 'coopTilleuls', - ], $this->normalizer->normalize($obj, null, [ObjectNormalizer::GROUPS => ['a', 'c']])); + return $normalizer; } - public function testGroupsDenormalize() + protected function getNormalizerForMaxDepth(): ObjectNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); - - $obj = new GroupDummy(); - $obj->setFoo('foo'); - - $toNormalize = ['foo' => 'foo', 'bar' => 'bar']; + $normalizer = new ObjectNormalizer($classMetadataFactory); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); - $normalized = $this->normalizer->denormalize( - $toNormalize, - 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', - null, - [ObjectNormalizer::GROUPS => ['a']] - ); - $this->assertEquals($obj, $normalized); + return $normalizer; + } - $obj->setBar('bar'); + protected function getDenormalizerForConstructArguments(): ObjectNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $denormalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $serializer = new Serializer([$denormalizer]); + $denormalizer->setSerializer($serializer); - $normalized = $this->normalizer->denormalize( - $toNormalize, - 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', - null, - [ObjectNormalizer::GROUPS => ['a', 'b']] - ); - $this->assertEquals($obj, $normalized); + return $denormalizer; } - public function testNormalizeNoPropertyInGroup() + protected function getDenormalizerForTypeEnforcement(): ObjectNormalizer { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $normalizer = new ObjectNormalizer(null, null, null, $extractor); + new Serializer([new ArrayDenormalizer(), $normalizer]); - $obj = new GroupDummy(); - $obj->setFoo('foo'); + return $normalizer; + } - $this->assertEquals([], $this->normalizer->normalize($obj, null, ['groups' => ['notExist']])); + protected function getNormalizerForSkipNullValues(): ObjectNormalizer + { + return new ObjectNormalizer(); } public function testGroupsNormalizeWithNameConverter() @@ -373,95 +353,57 @@ 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'], - ]] - ) - ); - } - - /** - * @dataProvider provideCallbacks - */ - public function testCallbacks($callbacks, $value, $result, $message) + protected function getNormalizerForCallbacks(): ObjectNormalizer { - $this->doTestCallbacks($callbacks, $value, $result, $message); + return new ObjectNormalizer(); } /** * @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) + public function testLegacyCallbacks($callbacks, $value, $result) { - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([ObjectNormalizer::CALLBACKS => $callbacks]); - $obj = new ObjectConstructorDummy('', $value, true); + $this->normalizer->setCallbacks($callbacks); + $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); } - private function doTestUncallableCallbacks(bool $legacy = false) + protected function getNormalizerForIgnoredAttributes(): ObjectNormalizer { - $callbacks = ['bar' => null]; - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([ObjectNormalizer::CALLBACKS => $callbacks]); - - $obj = new ObjectConstructorDummy('baz', 'quux', true); + $normalizer = new ObjectNormalizer(); + // instantiate a serializer with the normalizer to handle normalizing recursive structures + new Serializer([$normalizer]); - $this->normalizer->normalize($obj, 'any'); + return $normalizer; } - public function testIgnoredAttributes() + protected function getDenormalizerForIgnoredAttributes(): ObjectNormalizer { - $this->doTestIgnoredAttributes(); - } + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor()); + 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([ObjectNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]); + $this->normalizer->setIgnoredAttributes($ignoredAttributes); $obj = new ObjectDummy(); $obj->setFoo('foo'); @@ -474,97 +416,20 @@ 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]); + $this->normalizer->setIgnoredAttributes($ignoredAttributes); $obj = new ObjectDummy(); $obj->setFoo('foo'); $this->assertEquals( $obj, - $this->normalizer->denormalize(['fooBar' => 'fooBar', 'foo' => 'foo', 'baz' => 'baz'], __NAMESPACE__.'\ObjectDummy') + $this->normalizer->denormalize(['fooBar' => 'fooBar', 'foo' => 'foo', 'baz' => 'baz'], ObjectDummy::class) ); } - 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 @@ -581,30 +446,28 @@ public function testUnableToNormalizeObjectAttribute() $this->normalizer->normalize($obj, 'any'); } - /** - * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException - */ - public function testUnableToNormalizeCircularReference() + protected function getNormalizerForCircularReference(): ObjectNormalizer { - $this->doTestUnableToNormalizeCircularReference(); + $normalizer = new ObjectNormalizer(); + new Serializer([$normalizer]); + + return $normalizer; } - /** - * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException - */ - public function testLegacyUnableToNormalizeCircularReference() + protected function getSelfReferencingModel() { - $this->doTestUnableToNormalizeCircularReference(true); + return new CircularReferenceDummy(); } - private function doTestUnableToNormalizeCircularReference(bool $legacy = false) + public function testLegacyUnableToNormalizeCircularReference() { - $legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer([ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT => 2]); + $this->normalizer->setCircularReferenceLimit(2); $serializer = new Serializer([$this->normalizer]); $this->normalizer->setSerializer($serializer); $obj = new CircularReferenceDummy(); + $this->expectException(CircularReferenceException::class); $this->normalizer->normalize($obj); } @@ -623,48 +486,28 @@ 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) - { - $this->createNormalizerWithCircularReferenceHandler(function ($obj) { - return \get_class($obj); - }, $legacy); + new Serializer([$this->normalizer]); $obj = new CircularReferenceDummy(); - $expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy']; - $this->assertEquals($expected, $this->normalizer->normalize($obj)); + $expected = ['me' => CircularReferenceDummy::class]; - $this->createNormalizerWithCircularReferenceHandler(function ($obj, string $format, array $context) { + $this->normalizer->setCircularReferenceHandler(function ($obj, string $format, array $context) { $this->assertInstanceOf(CircularReferenceDummy::class, $obj); $this->assertSame('test', $format); - $this->arrayHasKey('foo', $context); + $this->assertArrayHasKey('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') + $this->normalizer->denormalize(['non_existing' => true], ObjectDummy::class) ); } @@ -699,94 +542,6 @@ public function testNormalizeNotSerializableContext() }])); } - public function testMaxDepth() - { - $this->doTestMaxDepth(); - } - - public function testLegacyMaxDepth() - { - $this->doTestMaxDepth(true); - } - - private function doTestMaxDepth(bool $legacy = false) - { - $level1 = new MaxDepthDummy(); - $level1->foo = 'level1'; - - $level2 = new MaxDepthDummy(); - $level2->foo = 'level2'; - $level1->child = $level2; - - $level3 = new MaxDepthDummy(); - $level3->foo = 'level3'; - $level2->child = $level3; - - $this->createNormalizerWithMaxDepthHandler(null, $legacy); - $result = $this->serializer->normalize($level1, null, [ObjectNormalizer::ENABLE_MAX_DEPTH => true]); - - $expected = [ - 'bar' => null, - 'foo' => 'level1', - 'child' => [ - 'bar' => null, - 'foo' => 'level2', - 'child' => [ - 'bar' => null, - 'child' => null, - ], - ], - ]; - - $this->assertEquals($expected, $result); - - $expected = [ - 'bar' => null, - 'foo' => 'level1', - 'child' => [ - 'bar' => null, - 'foo' => 'level2', - 'child' => [ - 'bar' => null, - 'child' => null, - 'foo' => 'handler', - ], - ], - ]; - - $this->createNormalizerWithMaxDepthHandler(function () { - return 'handler'; - }, $legacy); - $result = $this->serializer->normalize($level1, null, [ObjectNormalizer::ENABLE_MAX_DEPTH => true]); - $this->assertEquals($expected, $result); - - $this->createNormalizerWithMaxDepthHandler(function ($object, $parentObject, $attributeName, $format, $context) { - $this->assertSame('level3', $object); - $this->assertInstanceOf(MaxDepthDummy::class, $parentObject); - $this->assertSame('foo', $attributeName); - $this->assertSame('test', $format); - $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) - { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - if ($legacy) { - $this->createNormalizer([], $classMetadataFactory); - if (null !== $handler) { - $this->normalizer->setMaxDepthHandler($handler); - } - } else { - $this->createNormalizer([ObjectNormalizer::MAX_DEPTH_HANDLER => $handler], $classMetadataFactory); - } - $this->serializer = new Serializer([$this->normalizer]); - $this->normalizer->setSerializer($this->serializer); - } - /** * @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException */ @@ -824,41 +579,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(); @@ -881,61 +601,22 @@ public function testExtractAttributesRespectsContext() $this->assertSame(['foo' => 'bar', 'bar' => 'foo'], $normalizer->normalize($data, null, ['include_foo_and_bar' => true])); } - public function testAttributesContextNormalize() + protected function getNormalizerForAttributes(): ObjectNormalizer { $normalizer = new ObjectNormalizer(); - $serializer = new Serializer([$normalizer]); - - $objectInner = new ObjectInner(); - $objectInner->foo = 'innerFoo'; - $objectInner->bar = 'innerBar'; + // instantiate a serializer with the normalizer to handle normalizing recursive structures + new Serializer([$normalizer]); - $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) - ); + return $normalizer; } - public function testAttributesContextDenormalize() + protected function getDenormalizerForAttributes(): ObjectNormalizer { - $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); + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor()); + new Serializer([$normalizer]); - $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)); + return $normalizer; } public function testAttributesContextDenormalizeConstructor() @@ -1001,7 +682,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 testObjectClassResolver() @@ -1033,65 +714,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; @@ -1297,19 +919,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 26bccdfcad2e8..35eb9eeabb1a0 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -13,20 +13,43 @@ 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\MaxDepthTestTrait; +use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectToPopulateTestTrait; +use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait; class PropertyNormalizerTest extends TestCase { + use GroupsTestTrait; + use CircularReferenceTestTrait; + use ObjectToPopulateTestTrait; + use MaxDepthTestTrait; + use ConstructorArgumentsTestTrait; + use TypeEnforcementTestTrait; + use CallbacksTestTrait; + /** * @var PropertyNormalizer */ @@ -121,62 +144,34 @@ 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) + public function testLegacyCallbacks($callbacks, $value, $result) { - $this->doTestCallbacks($callbacks, $value, $result, $message, true); - } + $this->normalizer->setCallbacks($callbacks); - private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false) - { - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([PropertyNormalizer::CALLBACKS => $callbacks]); - - $obj = new PropertyConstructorDummy('', $value); + $obj = new CallbacksObject($value); $this->assertEquals( $result, - $this->normalizer->normalize($obj, 'any'), - $message + $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); - } - - /** - * @expectedException \InvalidArgumentException - */ - private function doTestUncallableCallbacks(bool $legacy = false) - { - $callbacks = ['bar' => null]; - $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([PropertyNormalizer::CALLBACKS => $callbacks]); - - $obj = new PropertyConstructorDummy('baz', 'quux'); - - $this->normalizer->normalize($obj, 'any'); + $this->normalizer->setCallbacks($callbacks); } public function testIgnoredAttributes() @@ -204,63 +199,57 @@ private function doTestIgnoredAttributes(bool $legacy = false) ); } - public function testGroupsNormalize() + protected function getNormalizerForGroups(): PropertyNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new PropertyNormalizer($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 PropertyNormalizer($classMetadataFactory); + } - $this->assertEquals([ - 'bar' => 'bar', - ], $this->normalizer->normalize($obj, null, [PropertyNormalizer::GROUPS => ['c']])); + protected function getDenormalizerForGroups(): PropertyNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - // 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']])); + return new PropertyNormalizer($classMetadataFactory); } - public function testGroupsDenormalize() + protected function getDenormalizerForObjectToPopulate(): PropertyNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new PropertyNormalizer($classMetadataFactory); - $this->normalizer->setSerializer($this->serializer); + $normalizer = new PropertyNormalizer($classMetadataFactory, null, new PhpDocExtractor()); + new Serializer([$normalizer]); - $obj = new GroupDummy(); - $obj->setFoo('foo'); + return $normalizer; + } + + protected function getNormalizerForMaxDepth(): PropertyNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new PropertyNormalizer($classMetadataFactory); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); - $toNormalize = ['foo' => 'foo', 'bar' => 'bar']; + return $normalizer; + } - $normalized = $this->normalizer->denormalize( - $toNormalize, - 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', - null, - [PropertyNormalizer::GROUPS => ['a']] - ); - $this->assertEquals($obj, $normalized); + protected function getDenormalizerForConstructArguments(): PropertyNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $denormalizer = new PropertyNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $serializer = new Serializer([$denormalizer]); + $denormalizer->setSerializer($serializer); - $obj->setBar('bar'); + return $denormalizer; + } - $normalized = $this->normalizer->denormalize( - $toNormalize, - 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', - null, - [PropertyNormalizer::GROUPS => ['a', 'b']] - ); - $this->assertEquals($obj, $normalized); + protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface + { + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $normalizer = new PropertyNormalizer(null, null, $extractor); + $serializer = new Serializer([new ArrayDenormalizer(), $normalizer]); + $normalizer->setSerializer($serializer); + + return $normalizer; } public function testGroupsNormalizeWithNameConverter() @@ -305,90 +294,27 @@ public function testGroupsDenormalizeWithNameConverter() ); } - public function provideCallbacks() + protected function getNormalizerForCircularReference(): 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(); + new Serializer([$normalizer]); - /** - * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException - */ - public function testUnableToNormalizeCircularReference() - { - $this->doTestUnableToNormalizeCircularReference(); + return $normalizer; } - /** - * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException - */ - public function testLegacyUnableToNormalizeCircularReference() + protected function getSelfReferencingModel() { - $this->doTestUnableToNormalizeCircularReference(true); + return new PropertyCircularReferenceDummy(); } - private function doTestUnableToNormalizeCircularReference(bool $legacy = false) + public function testLegacyUnableToNormalizeCircularReference() { - $legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer([PropertyNormalizer::CIRCULAR_REFERENCE_LIMIT => 2]); - $this->serializer = new Serializer([$this->normalizer]); - $this->normalizer->setSerializer($this->serializer); + $this->normalizer->setCircularReferenceLimit(2); + new Serializer([$this->normalizer]); $obj = new PropertyCircularReferenceDummy(); + $this->expectException(CircularReferenceException::class); $this->normalizer->normalize($obj); } @@ -407,29 +333,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) { + $this->normalizer->setCircularReferenceHandler(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); + new Serializer([$this->normalizer]); $obj = new PropertyCircularReferenceDummy(); - $expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy']; + $expected = ['me' => PropertyCircularReferenceDummy::class]; $this->assertEquals($expected, $this->normalizer->normalize($obj)); } @@ -475,42 +389,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()));