From 74dda9b9e67bc86cdbd868e797398c30d1379447 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Sun, 7 Apr 2019 11:40:42 +0200 Subject: [PATCH 1/6] Add allowed and ignored implementations --- .../AllowedPropertyListExtractor.php | 51 +++++++++++++ .../IgnoredPropertyListExtractor.php | 51 +++++++++++++ .../AllowedPropertyListExtractorTest.php | 73 +++++++++++++++++++ .../IgnoredPropertyListExtractorTest.php | 66 +++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php create mode 100644 src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php create mode 100644 src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php create mode 100644 src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php diff --git a/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php new file mode 100644 index 0000000000000..13c0677e05bde --- /dev/null +++ b/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Extractor; + +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; + +/** + * Allow properties given an allowed list of properties in the context. + * + * @author Joel Wurtz + */ +final class AllowedPropertyListExtractor implements PropertyListExtractorInterface +{ + public const ATTRIBUTES = 'attributes'; + + private $extractor; + + public function __construct(PropertyListExtractorInterface $extractor) + { + $this->extractor = $extractor; + } + + /** + * {@inheritdoc} + */ + public function getProperties($class, array $context = []) + { + $properties = $this->extractor->getProperties($class, $context); + + if (null === $properties) { + return null; + } + + $allowed = $context[self::ATTRIBUTES] ?? null; + + if (null === $allowed) { + return $properties; + } + + return array_intersect($properties, $allowed); + } +} diff --git a/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php new file mode 100644 index 0000000000000..d06a81c5e1f6e --- /dev/null +++ b/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Extractor; + +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; + +/** + * Remove properties given a ignore list of attributes in the context. + * + * @author Joel Wurtz + */ +final class IgnoredPropertyListExtractor implements PropertyListExtractorInterface +{ + public const ATTRIBUTES = 'ignored_attributes'; + + private $extractor; + + public function __construct(PropertyListExtractorInterface $extractor) + { + $this->extractor = $extractor; + } + + /** + * {@inheritdoc} + */ + public function getProperties($class, array $context = []) + { + $properties = $this->extractor->getProperties($class, $context); + + if (null === $properties) { + return null; + } + + $ignored = $context[self::ATTRIBUTES] ?? null; + + if (null === $ignored) { + return $properties; + } + + return array_diff($properties, $ignored); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php b/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php new file mode 100644 index 0000000000000..703a74fea6ff7 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Extractor; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Extractor\AllowedPropertyListExtractor; + +class AllowedPropertyListExtractorTest extends TestCase +{ + public function testAllowAttributes() + { + $extractor = $this + ->getMockBuilder(PropertyListExtractorInterface::class) + ->getMock() + ; + + $extractor->method('getProperties')->willReturn([ + 'foo', + 'bar', + 'baz' + ]); + + $allowed = new AllowedPropertyListExtractor($extractor); + $properties = $allowed->getProperties('SomeClass'); + + $this->assertInternalType('array', $properties); + $this->assertContains('foo', $properties); + $this->assertContains('bar', $properties); + $this->assertContains('baz', $properties); + + $properties = $allowed->getProperties('SomeClass', [ + AllowedPropertyListExtractor::ATTRIBUTES => ['foo', 'bar', 'dummy'], + ]); + + $this->assertInternalType('array', $properties); + $this->assertContains('foo', $properties); + $this->assertContains('bar', $properties); + $this->assertNotContains('dummy', $properties); + $this->assertNotContains('baz', $properties); + + $properties = $allowed->getProperties('SomeClass', [ + AllowedPropertyListExtractor::ATTRIBUTES => [], + ]); + + $this->assertInternalType('array', $properties); + $this->assertEmpty($properties); + } + + public function testNullProperties() + { + $extractor = $this + ->getMockBuilder(PropertyListExtractorInterface::class) + ->getMock() + ; + + $extractor->method('getProperties')->willReturn(null); + + $allowed = new AllowedPropertyListExtractor($extractor); + $properties = $allowed->getProperties('SomeClass'); + + $this->assertNull($properties); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php b/src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php new file mode 100644 index 0000000000000..27faad1a59f9e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Extractor; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Extractor\IgnoredPropertyListExtractor; + +class IgnoredPropertyListExtractorTest extends TestCase +{ + public function testRemoveAttributes() + { + $extractor = $this + ->getMockBuilder(PropertyListExtractorInterface::class) + ->getMock() + ; + + $extractor->method('getProperties')->willReturn([ + 'foo', + 'bar', + 'baz' + ]); + + $ignored = new IgnoredPropertyListExtractor($extractor); + $properties = $ignored->getProperties('SomeClass'); + + $this->assertInternalType('array', $properties); + $this->assertContains('foo', $properties); + $this->assertContains('bar', $properties); + $this->assertContains('baz', $properties); + + $properties = $ignored->getProperties('SomeClass', [ + IgnoredPropertyListExtractor::ATTRIBUTES => ['foo', 'bar', 'dummy'], + ]); + + $this->assertInternalType('array', $properties); + $this->assertNotContains('foo', $properties); + $this->assertNotContains('bar', $properties); + $this->assertNotContains('dummy', $properties); + $this->assertContains('baz', $properties); + } + + public function testNullProperties() + { + $extractor = $this + ->getMockBuilder(PropertyListExtractorInterface::class) + ->getMock() + ; + + $extractor->method('getProperties')->willReturn(null); + + $ignored = new IgnoredPropertyListExtractor($extractor); + $properties = $ignored->getProperties('SomeClass'); + + $this->assertNull($properties); + } +} From fc2508c84db23bf9baacb8373eaa1980b87e438d Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Mon, 8 Apr 2019 11:23:24 +0200 Subject: [PATCH 2/6] Add a group property list extractor --- .../Extractor/GroupPropertyListExtractor.php | 66 ++++++++ .../GroupPropertyListExtractorTest.php | 157 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php create mode 100644 src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php diff --git a/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php new file mode 100644 index 0000000000000..f1f630274b41f --- /dev/null +++ b/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Extractor; + +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +/** + * Filter properties given a specific set of groups + * + * @author Joel Wurtz + */ +class GroupPropertyListExtractor implements PropertyListExtractorInterface +{ + public const GROUPS = 'groups'; + + private $classMetadataFactory; + + private $extractor; + + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory, PropertyListExtractorInterface $extractor = null) + { + $this->extractor = $extractor; + $this->classMetadataFactory = $classMetadataFactory; + } + + public function getProperties($class, array $context = []) + { + $properties = null; + + if (null !== $this->extractor) { + $properties = $this->extractor->getProperties($class, $context); + + if (null === $properties) { + return null; + } + } + + $groups = $context[self::GROUPS] ?? null; + $groups = (\is_array($groups) || is_scalar($groups)) ? (array) $groups : false; + $groupProperties = []; + + foreach ($this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() as $attributeMetadata) { + $name = $attributeMetadata->getName(); + + if (false === $groups || array_intersect($attributeMetadata->getGroups(), $groups)) { + $groupProperties[] = $name; + } + } + + if (null === $properties) { + return $groupProperties; + } + + return array_intersect($properties, $groupProperties); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php b/src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php new file mode 100644 index 0000000000000..1321316750556 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Extractor; + +use Doctrine\Common\Annotations\AnnotationReader; +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Extractor\GroupPropertyListExtractor; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + +class GroupPropertyListExtractorTest extends TestCase +{ + public function testNoGroups() + { + $extractor = new ReflectionExtractor(); + $factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $groupExtractor = new GroupPropertyListExtractor($factory, $extractor); + + $properties = $groupExtractor->getProperties(DummyNoGroups::class); + + $this->assertInternalType('array', $properties); + $this->assertContains('foo', $properties); + $this->assertContains('bar', $properties); + $this->assertContains('baz', $properties); + + $properties = $groupExtractor->getProperties(DummyNoGroups::class, [ + GroupPropertyListExtractor::GROUPS => ['dummy'] + ]); + + $this->assertInternalType('array', $properties); + $this->assertEmpty($properties); + } + + public function testGroups() + { + $extractor = new ReflectionExtractor(); + $factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $groupExtractor = new GroupPropertyListExtractor($factory, $extractor); + + $properties = $groupExtractor->getProperties(DummyGroups::class); + + $this->assertInternalType('array', $properties); + $this->assertContains('foo', $properties); + $this->assertContains('bar', $properties); + $this->assertContains('baz', $properties); + + $properties = $groupExtractor->getProperties(DummyGroups::class, [ + GroupPropertyListExtractor::GROUPS => ['dummy'] + ]); + + $this->assertInternalType('array', $properties); + $this->assertEmpty($properties); + + $properties = $groupExtractor->getProperties(DummyGroups::class, [ + GroupPropertyListExtractor::GROUPS => ['foo'] + ]); + + $this->assertInternalType('array', $properties); + $this->assertContains('foo', $properties); + $this->assertNotContains('bar', $properties); + $this->assertNotContains('baz', $properties); + + $properties = $groupExtractor->getProperties(DummyGroups::class, [ + GroupPropertyListExtractor::GROUPS => ['foo', 'bar'] + ]); + + $this->assertInternalType('array', $properties); + $this->assertContains('foo', $properties); + $this->assertContains('bar', $properties); + $this->assertContains('baz', $properties); + + $properties = $groupExtractor->getProperties(DummyGroups::class, [ + GroupPropertyListExtractor::GROUPS => ['bar'] + ]); + + $this->assertInternalType('array', $properties); + $this->assertNotContains('foo', $properties); + $this->assertContains('bar', $properties); + $this->assertContains('baz', $properties); + + $properties = $groupExtractor->getProperties(DummyGroups::class, [ + GroupPropertyListExtractor::GROUPS => ['baz'] + ]); + + $this->assertInternalType('array', $properties); + $this->assertNotContains('foo', $properties); + $this->assertNotContains('bar', $properties); + $this->assertContains('baz', $properties); + } + + public function testNullProperties() + { + $extractor = $this + ->getMockBuilder(PropertyListExtractorInterface::class) + ->getMock() + ; + $factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + + $extractor->method('getProperties')->willReturn(null); + + $ignored = new GroupPropertyListExtractor($factory, $extractor); + $properties = $ignored->getProperties(DummyNoGroups::class); + + $this->assertNull($properties); + } + + public function testNoExtractor() + { + $factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $ignored = new GroupPropertyListExtractor($factory); + $properties = $ignored->getProperties(DummyNoGroups::class); + + $this->assertInternalType('array', $properties); + $this->assertContains('foo', $properties); + $this->assertContains('bar', $properties); + $this->assertContains('baz', $properties); + } +} + +class DummyNoGroups +{ + public $foo; + + public $bar; + + public $baz; +} + +class DummyGroups +{ + /** + * @Groups("foo") + */ + public $foo; + + /** + * @Groups({"bar"}) + */ + public $bar; + + /** + * @Groups({"bar", "baz"}) + */ + public $baz; +} From 64458eb3e594a4b8863a9f830ad60e56c5abda2f Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Mon, 8 Apr 2019 12:59:33 +0200 Subject: [PATCH 3/6] Add final and experimental --- .../Serializer/Extractor/AllowedPropertyListExtractor.php | 2 ++ .../Serializer/Extractor/GroupPropertyListExtractor.php | 4 +++- .../Serializer/Extractor/IgnoredPropertyListExtractor.php | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php index 13c0677e05bde..0f1859c0dc513 100644 --- a/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php +++ b/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php @@ -17,6 +17,8 @@ * Allow properties given an allowed list of properties in the context. * * @author Joel Wurtz + * + * @experimental in 4.3 */ final class AllowedPropertyListExtractor implements PropertyListExtractorInterface { diff --git a/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php index f1f630274b41f..ae100c572256c 100644 --- a/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php +++ b/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php @@ -18,8 +18,10 @@ * Filter properties given a specific set of groups * * @author Joel Wurtz + * + * @experimental in 4.3 */ -class GroupPropertyListExtractor implements PropertyListExtractorInterface +final class GroupPropertyListExtractor implements PropertyListExtractorInterface { public const GROUPS = 'groups'; diff --git a/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php index d06a81c5e1f6e..6b046d3e64cc5 100644 --- a/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php +++ b/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php @@ -17,6 +17,8 @@ * Remove properties given a ignore list of attributes in the context. * * @author Joel Wurtz + * + * @experimental in 4.3 */ final class IgnoredPropertyListExtractor implements PropertyListExtractorInterface { From f2ad9d9d7af6de9e6baea5b56868d703ba0064f3 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Mon, 8 Apr 2019 15:01:29 +0200 Subject: [PATCH 4/6] Add child context builder concept, allow nested allowed attributes --- .../Context/ChildContextBuilderInterface.php | 25 ++++++++++ .../AllowedPropertyListExtractor.php | 21 +++++++- .../DecorateChildContextBuilderTrait.php | 31 ++++++++++++ .../Extractor/GroupPropertyListExtractor.php | 9 ++-- .../IgnoredPropertyListExtractor.php | 7 +-- .../Extractor/ObjectPropertyListExtractor.php | 15 +++++- .../AllowedPropertyListExtractorTest.php | 49 +++++++++++++++++++ .../GroupPropertyListExtractorTest.php | 25 ++++++++-- .../IgnoredPropertyListExtractorTest.php | 16 ++++++ .../ObjectPropertyListExtractorTest.php | 16 ++++++ 10 files changed, 201 insertions(+), 13 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Context/ChildContextBuilderInterface.php create mode 100644 src/Symfony/Component/Serializer/Extractor/DecorateChildContextBuilderTrait.php diff --git a/src/Symfony/Component/Serializer/Context/ChildContextBuilderInterface.php b/src/Symfony/Component/Serializer/Context/ChildContextBuilderInterface.php new file mode 100644 index 0000000000000..b5e27ddbd0350 --- /dev/null +++ b/src/Symfony/Component/Serializer/Context/ChildContextBuilderInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Context; + +/** + * @author Joel Wurtz + * + * @experimental in 4.3 + */ +interface ChildContextBuilderInterface +{ + /** + * Update the context for a sub level given a specific attribute. + */ + public function createChildContextForAttribute(array $context, string $attribute): array; +} diff --git a/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php index 0f1859c0dc513..20ebe4798914c 100644 --- a/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php +++ b/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Extractor; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; /** * Allow properties given an allowed list of properties in the context. @@ -20,7 +21,7 @@ * * @experimental in 4.3 */ -final class AllowedPropertyListExtractor implements PropertyListExtractorInterface +final class AllowedPropertyListExtractor implements PropertyListExtractorInterface, ChildContextBuilderInterface { public const ATTRIBUTES = 'attributes'; @@ -50,4 +51,22 @@ public function getProperties($class, array $context = []) return array_intersect($properties, $allowed); } + + /** + * {@inheritdoc} + */ + public function createChildContextForAttribute(array $context, string $attribute): array + { + if ($this->extractor instanceof ChildContextBuilderInterface) { + $context = $this->extractor->createChildContextForAttribute($context, $attribute); + } + + if (isset($context[self::ATTRIBUTES][$attribute])) { + $context[self::ATTRIBUTES] = $context[self::ATTRIBUTES][$attribute]; + } else { + unset($context[self::ATTRIBUTES]); + } + + return $context; + } } diff --git a/src/Symfony/Component/Serializer/Extractor/DecorateChildContextBuilderTrait.php b/src/Symfony/Component/Serializer/Extractor/DecorateChildContextBuilderTrait.php new file mode 100644 index 0000000000000..89d5b7c7309d2 --- /dev/null +++ b/src/Symfony/Component/Serializer/Extractor/DecorateChildContextBuilderTrait.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Extractor; + +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; + +trait DecorateChildContextBuilderTrait +{ + private $extractor; + + /** + * {@inheritdoc} + */ + public function createChildContextForAttribute(array $context, string $attribute): array + { + if ($this->extractor instanceof ChildContextBuilderInterface) { + return $this->extractor->createChildContextForAttribute($context, $attribute); + } + + return $context; + } +} diff --git a/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php index ae100c572256c..e4950e1da2f0a 100644 --- a/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php +++ b/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php @@ -12,23 +12,24 @@ namespace Symfony\Component\Serializer\Extractor; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; /** - * Filter properties given a specific set of groups + * Filter properties given a specific set of groups. * * @author Joel Wurtz * * @experimental in 4.3 */ -final class GroupPropertyListExtractor implements PropertyListExtractorInterface +final class GroupPropertyListExtractor implements PropertyListExtractorInterface, ChildContextBuilderInterface { + use DecorateChildContextBuilderTrait; + public const GROUPS = 'groups'; private $classMetadataFactory; - private $extractor; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory, PropertyListExtractorInterface $extractor = null) { $this->extractor = $extractor; diff --git a/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php index 6b046d3e64cc5..00f22b4504da4 100644 --- a/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php +++ b/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Extractor; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; /** * Remove properties given a ignore list of attributes in the context. @@ -20,11 +21,11 @@ * * @experimental in 4.3 */ -final class IgnoredPropertyListExtractor implements PropertyListExtractorInterface +final class IgnoredPropertyListExtractor implements PropertyListExtractorInterface, ChildContextBuilderInterface { - public const ATTRIBUTES = 'ignored_attributes'; + use DecorateChildContextBuilderTrait; - private $extractor; + public const ATTRIBUTES = 'ignored_attributes'; public function __construct(PropertyListExtractorInterface $extractor) { diff --git a/src/Symfony/Component/Serializer/Extractor/ObjectPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/ObjectPropertyListExtractor.php index 2ea19d28faa20..117a92c9f3bd5 100644 --- a/src/Symfony/Component/Serializer/Extractor/ObjectPropertyListExtractor.php +++ b/src/Symfony/Component/Serializer/Extractor/ObjectPropertyListExtractor.php @@ -12,13 +12,14 @@ namespace Symfony\Component\Serializer\Extractor; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; /** * @author David Maicher * * @experimental in 4.3 */ -final class ObjectPropertyListExtractor implements ObjectPropertyListExtractorInterface +final class ObjectPropertyListExtractor implements ObjectPropertyListExtractorInterface, ChildContextBuilderInterface { private $propertyListExtractor; private $objectClassResolver; @@ -38,4 +39,16 @@ public function getProperties($object, array $context = []): ?array return $this->propertyListExtractor->getProperties($class, $context); } + + /** + * {@inheritdoc} + */ + public function createChildContextForAttribute(array $context, string $attribute): array + { + if ($this->propertyListExtractor instanceof ChildContextBuilderInterface) { + return $this->propertyListExtractor->createChildContextForAttribute($context, $attribute); + } + + return $context; + } } diff --git a/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php b/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php index 703a74fea6ff7..a8decc6be01eb 100644 --- a/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php +++ b/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; use Symfony\Component\Serializer\Extractor\AllowedPropertyListExtractor; class AllowedPropertyListExtractorTest extends TestCase @@ -70,4 +71,52 @@ public function testNullProperties() $this->assertNull($properties); } + + public function testDecorateChildContext() + { + $extractor = $this + ->getMockBuilder([PropertyListExtractorInterface::class, ChildContextBuilderInterface::class]) + ->getMock() + ; + + $extractor->method('createChildContextForAttribute')->willReturn([]); + + $allowed = new AllowedPropertyListExtractor($extractor); + $childContext = $allowed->createChildContextForAttribute(['test'], 'some_attribute'); + + $this->assertSame([], $childContext); + } + + public function testCreateAttributesForNested() + { + $extractor = $this + ->getMockBuilder([PropertyListExtractorInterface::class, ChildContextBuilderInterface::class]) + ->getMock() + ; + + $extractor->method('createChildContextForAttribute')->willReturnArgument(0); + + $allowed = new AllowedPropertyListExtractor($extractor); + $childContext = $allowed->createChildContextForAttribute([ + AllowedPropertyListExtractor::ATTRIBUTES => [ + 'foo', + 'bar' => [ + 'baz' + ] + ], + ], 'foo'); + + $this->assertSame([], $childContext); + + $childContext = $allowed->createChildContextForAttribute([ + AllowedPropertyListExtractor::ATTRIBUTES => [ + 'foo', + 'bar' => [ + 'baz' + ] + ], + ], 'bar'); + + $this->assertSame([AllowedPropertyListExtractor::ATTRIBUTES => ['baz']], $childContext); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php b/src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php index 1321316750556..cbe4732be0820 100644 --- a/src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php +++ b/src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php @@ -16,6 +16,7 @@ use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; use Symfony\Component\Serializer\Extractor\GroupPropertyListExtractor; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; @@ -110,8 +111,8 @@ public function testNullProperties() $extractor->method('getProperties')->willReturn(null); - $ignored = new GroupPropertyListExtractor($factory, $extractor); - $properties = $ignored->getProperties(DummyNoGroups::class); + $groupExtractor = new GroupPropertyListExtractor($factory, $extractor); + $properties = $groupExtractor->getProperties(DummyNoGroups::class); $this->assertNull($properties); } @@ -119,14 +120,30 @@ public function testNullProperties() public function testNoExtractor() { $factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $ignored = new GroupPropertyListExtractor($factory); - $properties = $ignored->getProperties(DummyNoGroups::class); + $groupExtractor = new GroupPropertyListExtractor($factory); + $properties = $groupExtractor->getProperties(DummyNoGroups::class); $this->assertInternalType('array', $properties); $this->assertContains('foo', $properties); $this->assertContains('bar', $properties); $this->assertContains('baz', $properties); } + + public function testDecorateChildContext() + { + $extractor = $this + ->getMockBuilder([PropertyListExtractorInterface::class, ChildContextBuilderInterface::class]) + ->getMock() + ; + + $extractor->method('createChildContextForAttribute')->willReturn([]); + $factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $groupExtractor = new GroupPropertyListExtractor($factory, $extractor); + + $childContext = $groupExtractor->createChildContextForAttribute(['test'], 'some_attribute'); + + $this->assertSame([], $childContext); + } } class DummyNoGroups diff --git a/src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php b/src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php index 27faad1a59f9e..2b163165ae915 100644 --- a/src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php +++ b/src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; use Symfony\Component\Serializer\Extractor\IgnoredPropertyListExtractor; class IgnoredPropertyListExtractorTest extends TestCase @@ -63,4 +64,19 @@ public function testNullProperties() $this->assertNull($properties); } + + public function testDecorateChildContext() + { + $extractor = $this + ->getMockBuilder([PropertyListExtractorInterface::class, ChildContextBuilderInterface::class]) + ->getMock() + ; + + $extractor->method('createChildContextForAttribute')->willReturn([]); + $ignored = new IgnoredPropertyListExtractor($extractor); + + $childContext = $ignored->createChildContextForAttribute(['test'], 'some_attribute'); + + $this->assertSame([], $childContext); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Extractor/ObjectPropertyListExtractorTest.php b/src/Symfony/Component/Serializer/Tests/Extractor/ObjectPropertyListExtractorTest.php index 9701628e9008e..1caf49494f41b 100644 --- a/src/Symfony/Component/Serializer/Tests/Extractor/ObjectPropertyListExtractorTest.php +++ b/src/Symfony/Component/Serializer/Tests/Extractor/ObjectPropertyListExtractorTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Context\ChildContextBuilderInterface; use Symfony\Component\Serializer\Extractor\ObjectPropertyListExtractor; class ObjectPropertyListExtractorTest extends TestCase @@ -58,4 +59,19 @@ public function testGetPropertiesWithObjectClassResolver(): void (new ObjectPropertyListExtractor($propertyListExtractor, $classResolver))->getProperties($object, $context) ); } + + public function testDecorateChildContext() + { + $extractor = $this + ->getMockBuilder([PropertyListExtractorInterface::class, ChildContextBuilderInterface::class]) + ->getMock() + ; + + $extractor->method('createChildContextForAttribute')->willReturn([]); + $objectExtractor = new ObjectPropertyListExtractor($extractor); + + $childContext = $objectExtractor->createChildContextForAttribute(['test'], 'some_attribute'); + + $this->assertSame([], $childContext); + } } From a3c388a2a3c90f2bda9c27ce09ca6f21b6d4a9fa Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 9 Apr 2019 09:31:30 +0200 Subject: [PATCH 5/6] Add circular reference normalizer --- .../CheckCircularReferenceNormalizer.php | 147 ++++++++++++ .../CheckCircularReferenceNormalizerTest.php | 211 ++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Normalizer/CheckCircularReferenceNormalizer.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/CheckCircularReferenceNormalizerTest.php diff --git a/src/Symfony/Component/Serializer/Normalizer/CheckCircularReferenceNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/CheckCircularReferenceNormalizer.php new file mode 100644 index 0000000000000..069e304e6f0b0 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/CheckCircularReferenceNormalizer.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use Symfony\Component\Serializer\Exception\CircularReferenceException; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * Handle circular references. + * + * @author Joel Wurtz + */ +final class CheckCircularReferenceNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface, DenormalizerAwareInterface, SerializerAwareInterface +{ + public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit'; + public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler'; + private const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters'; + + private $normalizer; + + public function __construct(NormalizerInterface $normalizer) + { + $this->normalizer = $normalizer; + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $class, $format = null, array $context = []) + { + if (!$this->normalizer instanceof DenormalizerInterface) { + return null; + } + + return $this->normalizer->denormalize($data, $class, $format, $context); + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null) + { + return ($this->normalizer instanceof DenormalizerInterface && $this->normalizer->supportsDenormalization($data, $type, $format)); + } + + /** + * {@inheritdoc} + */ + public function normalize($object, $format = null, array $context = []) + { + if ($this->isCircularReference($object, $context)) { + return $this->handleCircularReference($object, $format, $context); + } + + return $this->normalizer->normalize($object, $format, $context); + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return $this->normalizer->supportsNormalization($data, $format); + } + + /** + * {@inheritdoc} + */ + public function hasCacheableSupportsMethod(): bool + { + return $this->normalizer instanceof CacheableSupportsMethodInterface && $this->normalizer->hasCacheableSupportsMethod(); + } + + /** + * {@inheritdoc} + */ + public function setDenormalizer(DenormalizerInterface $denormalizer) + { + if ($this->normalizer instanceof DenormalizerAwareInterface) { + $this->normalizer->setDenormalizer($denormalizer); + } + } + + /** + * {@inheritdoc} + */ + public function setNormalizer(NormalizerInterface $normalizer) + { + if ($this->normalizer instanceof NormalizerAwareInterface) { + $this->normalizer->setNormalizer($normalizer); + } + } + + /** + * {@inheritdoc} + */ + public function setSerializer(SerializerInterface $serializer) + { + if ($this->normalizer instanceof SerializerAwareInterface) { + $this->normalizer->setSerializer($serializer); + } + } + + private function isCircularReference($object, &$context) + { + $objectHash = spl_object_hash($object); + + $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? 1; + + if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) { + if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) { + unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]); + + return true; + } + + ++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]; + } else { + $context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1; + } + + return false; + } + + protected function handleCircularReference($object, string $format = null, array $context = []) + { + $circularReferenceHandler = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? null; + + if ($circularReferenceHandler) { + return $circularReferenceHandler($object, $format, $context); + } + + $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? 1; + + throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d)', \get_class($object), $circularReferenceLimit)); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/CheckCircularReferenceNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/CheckCircularReferenceNormalizerTest.php new file mode 100644 index 0000000000000..d47a114587efc --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/CheckCircularReferenceNormalizerTest.php @@ -0,0 +1,211 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Normalizer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Exception\CircularReferenceException; +use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; +use Symfony\Component\Serializer\Normalizer\CheckCircularReferenceNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; + +/** + * @author Jérôme Desjardins + */ +class CheckCircularReferenceNormalizerTest extends TestCase +{ + public function testNormalize() + { + $subNormalizer = $this + ->getMockBuilder(NormalizerInterface::class) + ->getMock() + ; + + $subNormalizer->method('normalize')->willReturn(['foo' => 'foo']); + $normalizer = new CheckCircularReferenceNormalizer($subNormalizer); + + $dummy = new Dummy(); + + $data = $normalizer->normalize($dummy, 'json'); + $this->assertSame(['foo' => 'foo'], $data); + + $data = $normalizer->normalize($dummy, 'json'); + $this->assertSame(['foo' => 'foo'], $data); + } + + public function testSupportNormalization() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $subNormalizer->method('supportsNormalization')->willReturn(true); + $normalizer = new CheckCircularReferenceNormalizer($subNormalizer); + + $this->assertTrue($normalizer->supportsNormalization([], 'json')); + + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $normalizer = new CheckCircularReferenceNormalizer($subNormalizer); + $subNormalizer->method('supportsNormalization')->willReturn(false); + $this->assertFalse($normalizer->supportsNormalization([], 'json')); + } + + public function testDenormalize() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $dummy = new DummyCircular(); + $subNormalizer->method('denormalize')->willReturn($dummy); + $normalizer = new CheckCircularReferenceNormalizer($subNormalizer); + + $data = $normalizer->denormalize([], 'type', 'json'); + + $this->assertSame($dummy, $data); + } + + public function testSupportDenormalization() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $subNormalizer->method('supportsDenormalization')->willReturn(true); + $normalizer = new CheckCircularReferenceNormalizer($subNormalizer); + + $this->assertTrue($normalizer->supportsDenormalization([], 'type', 'json')); + + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $normalizer = new CheckCircularReferenceNormalizer($subNormalizer); + $subNormalizer->method('supportsDenormalization')->willReturn(false); + $this->assertFalse($normalizer->supportsDenormalization([], 'type', 'json')); + } + + public function testHasCacheableSupportMethod() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, CacheableSupportsMethodInterface::class]) + ->getMock() + ; + + $subNormalizer->method('hasCacheableSupportsMethod')->willReturn(true); + $normalizer = new CheckCircularReferenceNormalizer($subNormalizer); + + $this->assertTrue($normalizer->hasCacheableSupportsMethod()); + + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, CacheableSupportsMethodInterface::class]) + ->getMock() + ; + + $normalizer = new CheckCircularReferenceNormalizer($subNormalizer); + $subNormalizer->method('hasCacheableSupportsMethod')->willReturn(false); + $this->assertFalse($normalizer->hasCacheableSupportsMethod()); + } + + + /** + * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException + */ + public function testThrowException() + { + $dummyCircular = new DummyCircular(); + $dummyNormalizer = new DummyNormalizer(); + $normalizer = new CheckCircularReferenceNormalizer($dummyNormalizer); + $serializer = new Serializer([$normalizer]); + + $normalizer->normalize($dummyCircular, 'json'); + } + + public function testLimitCounter() + { + $dummyCircular = new DummyCircular(); + $dummyNormalizer = new DummyNormalizer(); + $normalizer = new CheckCircularReferenceNormalizer($dummyNormalizer); + $serializer = new Serializer([$normalizer]); + + try { + $normalizer->normalize($dummyCircular, 'json', [ + CheckCircularReferenceNormalizer::CIRCULAR_REFERENCE_LIMIT => 3 + ]); + } catch (CircularReferenceException $exception) { + $this->assertSame(3, $dummyCircular->counter); + + return; + } + + $this->assertFalse(true); + } + + public function testHandler() + { + $dummyCircular = new DummyCircular(); + $dummyNormalizer = new DummyNormalizer(); + $normalizer = new CheckCircularReferenceNormalizer($dummyNormalizer); + $serializer = new Serializer([$normalizer]); + + $data = $normalizer->normalize($dummyCircular, 'format', [ + 'context_key' => 'context_value', + CheckCircularReferenceNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) use ($dummyCircular) { + $this->assertSame($dummyCircular, $object); + $this->assertSame(1, $object->counter); + $this->assertSame('format', $format); + $this->assertInternalType('array', $context); + $this->assertArrayHasKey('context_key', $context); + $this->assertSame($context['context_key'], 'context_value'); + + return 'dummy'; + } + ]); + + $this->assertSame('dummy', $data); + } +} + +class DummyCircular +{ + public $counter = 0; +} + +class DummyNormalizer implements NormalizerInterface, NormalizerAwareInterface +{ + use NormalizerAwareTrait; + + public $object; + + public function normalize($object, $format = null, array $context = []) + { + $object->counter++; + + return $this->normalizer->normalize($object, $format, $context); + } + + public function supportsNormalization($data, $format = null) + { + return true; + } +} From e525ad837c6eba54474d1ec8d8c21b1e492e925b Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 9 Apr 2019 19:26:04 +0200 Subject: [PATCH 6/6] Add discriminant normalizer --- .../ClassDiscriminatorNormalizer.php | 139 ++++++++++ .../ClassDiscriminatorNormalizerTest.php | 252 ++++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Normalizer/ClassDiscriminatorNormalizer.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/ClassDiscriminatorNormalizerTest.php diff --git a/src/Symfony/Component/Serializer/Normalizer/ClassDiscriminatorNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ClassDiscriminatorNormalizer.php new file mode 100644 index 0000000000000..109d2129b8629 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/ClassDiscriminatorNormalizer.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\RuntimeException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +class ClassDiscriminatorNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface, DenormalizerAwareInterface, SerializerAwareInterface +{ + private $denormalizer; + + private $denormalizerChain; + + private $classDiscriminatorResolver; + + public function __construct(DenormalizerInterface $denormalizer, ClassDiscriminatorResolverInterface $classDiscriminatorResolver) + { + $this->denormalizer = $denormalizer; + $this->classDiscriminatorResolver = $classDiscriminatorResolver; + } + + /** + * {@inheritdoc} + */ + public function normalize($object, $format = null, array $context = []) + { + if (!$this->denormalizer instanceof NormalizerInterface) { + throw new LogicException('Cannot normalize object because injected denormalizer is not a normalizer'); + } + + $data = $this->denormalizer->normalize($object, $format, $context); + $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object); + + if (null !== $mapping && (null !== ($typeValue = $mapping->getMappedObjectType($object)))) { + $data[$mapping->getTypeProperty()] = $typeValue; + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + if (!$this->denormalizer instanceof NormalizerInterface) { + return false; + } + + return $this->denormalizer->supportsNormalization($data, $format); + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $class, $format = null, array $context = []) + { + $mapping = $this->classDiscriminatorResolver->getMappingForClass($class); + + if (null === $mapping) { + return $this->denormalizer->denormalize($data, $class, $format, $context); + } + + if (!isset($data[$mapping->getTypeProperty()])) { + throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class)); + } + + $type = $data[$mapping->getTypeProperty()]; + + if (null === ($mappedClass = $mapping->getClassForType($type))) { + throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class)); + } + + return $this->denormalizerChain->denormalize($data, $mappedClass, $format, $context); + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null) + { + return + $this->denormalizer->supportsDenormalization($data, $type, $format) + || + (\interface_exists($type, false) && null !== $this->classDiscriminatorResolver->getMappingForClass($type)) + ; + } + + /** + * {@inheritdoc} + */ + public function hasCacheableSupportsMethod(): bool + { + return $this->denormalizer instanceof CacheableSupportsMethodInterface && $this->denormalizer->hasCacheableSupportsMethod(); + } + + /** + * {@inheritdoc} + */ + public function setDenormalizer(DenormalizerInterface $denormalizer) + { + if ($this->denormalizer instanceof DenormalizerAwareInterface) { + $this->denormalizer->setDenormalizer($denormalizer); + } + $this->denormalizerChain = $denormalizer; + } + + /** + * {@inheritdoc} + */ + public function setNormalizer(NormalizerInterface $normalizer) + { + if ($this->denormalizer instanceof NormalizerAwareInterface) { + $this->denormalizer->setNormalizer($normalizer); + } + } + + /** + * {@inheritdoc} + */ + public function setSerializer(SerializerInterface $serializer) + { + if ($this->denormalizer instanceof SerializerAwareInterface) { + $this->denormalizer->setSerializer($serializer); + } + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ClassDiscriminatorNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ClassDiscriminatorNormalizerTest.php new file mode 100644 index 0000000000000..fb9cb53a8d2b2 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ClassDiscriminatorNormalizerTest.php @@ -0,0 +1,252 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Normalizer; + +use Doctrine\Common\Annotations\AnnotationReader; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; +use Symfony\Component\Serializer\Normalizer\ClassDiscriminatorNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; + +class ClassDiscriminatorNormalizerTest extends TestCase +{ + public function testNormalize() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $subNormalizer->method('normalize')->willReturn(['foo' => 'foo']); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($subNormalizer, $resolver); + + $dummy = new Dummy(); + + $data = $normalizer->normalize($dummy, 'json'); + $this->assertSame(['foo' => 'foo'], $data); + + $data = $normalizer->normalize($dummy, 'json'); + $this->assertSame(['foo' => 'foo'], $data); + } + + public function testSupportNormalization() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $subNormalizer->method('supportsNormalization')->willReturn(true); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($subNormalizer, $resolver); + + $this->assertTrue($normalizer->supportsNormalization([], 'json')); + + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $normalizer = new ClassDiscriminatorNormalizer($subNormalizer, $resolver); + $subNormalizer->method('supportsNormalization')->willReturn(false); + $this->assertFalse($normalizer->supportsNormalization([], 'json')); + } + + public function testDenormalize() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $dummy = new DummyCircular(); + $subNormalizer->method('denormalize')->willReturn($dummy); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($subNormalizer, $resolver); + + $data = $normalizer->denormalize([], 'type', 'json'); + + $this->assertSame($dummy, $data); + } + + public function testSupportDenormalization() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $subNormalizer->method('supportsDenormalization')->willReturn(true); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($subNormalizer, $resolver); + + $this->assertTrue($normalizer->supportsDenormalization([], 'type', 'json')); + + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) + ->getMock() + ; + + $normalizer = new ClassDiscriminatorNormalizer($subNormalizer, $resolver); + $subNormalizer->method('supportsDenormalization')->willReturn(false); + $this->assertFalse($normalizer->supportsDenormalization([], 'type', 'json')); + } + + public function testHasCacheableSupportMethod() + { + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class, CacheableSupportsMethodInterface::class]) + ->getMock() + ; + + $subNormalizer->method('hasCacheableSupportsMethod')->willReturn(true); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($subNormalizer, $resolver); + + $this->assertTrue($normalizer->hasCacheableSupportsMethod()); + + $subNormalizer = $this + ->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class, CacheableSupportsMethodInterface::class]) + ->getMock() + ; + + $subNormalizer->method('hasCacheableSupportsMethod')->willReturn(false); + $normalizer = new ClassDiscriminatorNormalizer($subNormalizer, $resolver); + $this->assertFalse($normalizer->hasCacheableSupportsMethod()); + } + + public function testDiscriminantNormalize() + { + $childNormalizer = new DummyChildNormalizer(); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($childNormalizer, $resolver); + $serializer = new Serializer([$normalizer]); + + $data = $normalizer->normalize(new DummyFooChild(), 'json'); + + $this->assertSame([ + 'foo' => 'foo', + 'type' => 'foo', + ], $data); + + $data = $normalizer->normalize(new DummyBarChild(), 'json'); + + $this->assertSame([ + 'bar' => 'bar', + 'type' => 'bar', + ], $data); + + $data = $normalizer->normalize(new DummyParent(), 'json'); + + $this->assertSame([], $data); + } + + public function testDiscriminantDenormalize() + { + $childNormalizer = new DummyChildNormalizer(); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($childNormalizer, $resolver); + $serializer = new Serializer([$normalizer]); + + $data = $normalizer->denormalize(['type' => 'foo'], DummyParent::class, 'json'); + $this->assertInstanceOf(DummyFooChild::class, $data); + + $data = $normalizer->denormalize(['type' => 'bar'], DummyParent::class, 'json'); + $this->assertInstanceOf(DummyBarChild::class, $data); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\RuntimeException + */ + public function testUnknowValue() + { + $childNormalizer = new DummyChildNormalizer(); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($childNormalizer, $resolver); + $serializer = new Serializer([$normalizer]); + + $data = $normalizer->denormalize(['type' => 'unknow'], DummyParent::class, 'json'); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\RuntimeException + */ + public function testNoType() + { + $childNormalizer = new DummyChildNormalizer(); + $resolver = new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $normalizer = new ClassDiscriminatorNormalizer($childNormalizer, $resolver); + $serializer = new Serializer([$normalizer]); + + $data = $normalizer->denormalize([], DummyParent::class, 'json'); + } +} + +/** + * @DiscriminatorMap(mapping={ + * "foo"="Symfony\Component\Serializer\Tests\Normalizer\DummyFooChild", + * "bar"="Symfony\Component\Serializer\Tests\Normalizer\DummyBarChild" + * }, typeProperty="type") + */ +class DummyParent +{ +} + +class DummyFooChild extends DummyParent +{ +} + +class DummyBarChild extends DummyParent +{ +} + +class DummyChildNormalizer implements NormalizerInterface, DenormalizerInterface +{ + public function denormalize($data, $class, $format = null, array $context = []) + { + if ($class === DummyFooChild::class) { + return new DummyFooChild(); + } + + return new DummyBarChild(); + } + + public function supportsDenormalization($data, $type, $format = null) + { + return true; + } + + public function normalize($object, $format = null, array $context = []) + { + if ($object instanceof DummyFooChild) { + return ['foo' => 'foo']; + } + + if ($object instanceof DummyBarChild) { + return ['bar' => 'bar']; + } + + return []; + } + + public function supportsNormalization($data, $format = null) + { + return true; + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy