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 new file mode 100644 index 0000000000000..20ebe4798914c --- /dev/null +++ b/src/Symfony/Component/Serializer/Extractor/AllowedPropertyListExtractor.php @@ -0,0 +1,72 @@ + + * + * 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\Context\ChildContextBuilderInterface; + +/** + * Allow properties given an allowed list of properties in the context. + * + * @author Joel Wurtz + * + * @experimental in 4.3 + */ +final class AllowedPropertyListExtractor implements PropertyListExtractorInterface, ChildContextBuilderInterface +{ + 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); + } + + /** + * {@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 new file mode 100644 index 0000000000000..e4950e1da2f0a --- /dev/null +++ b/src/Symfony/Component/Serializer/Extractor/GroupPropertyListExtractor.php @@ -0,0 +1,69 @@ + + * + * 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\Context\ChildContextBuilderInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +/** + * Filter properties given a specific set of groups. + * + * @author Joel Wurtz + * + * @experimental in 4.3 + */ +final class GroupPropertyListExtractor implements PropertyListExtractorInterface, ChildContextBuilderInterface +{ + use DecorateChildContextBuilderTrait; + + public const GROUPS = 'groups'; + + private $classMetadataFactory; + + 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/Extractor/IgnoredPropertyListExtractor.php b/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php new file mode 100644 index 0000000000000..00f22b4504da4 --- /dev/null +++ b/src/Symfony/Component/Serializer/Extractor/IgnoredPropertyListExtractor.php @@ -0,0 +1,54 @@ + + * + * 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\Context\ChildContextBuilderInterface; + +/** + * Remove properties given a ignore list of attributes in the context. + * + * @author Joel Wurtz + * + * @experimental in 4.3 + */ +final class IgnoredPropertyListExtractor implements PropertyListExtractorInterface, ChildContextBuilderInterface +{ + use DecorateChildContextBuilderTrait; + + public const ATTRIBUTES = 'ignored_attributes'; + + 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/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/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/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/Extractor/AllowedPropertyListExtractorTest.php b/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php new file mode 100644 index 0000000000000..a8decc6be01eb --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Extractor/AllowedPropertyListExtractorTest.php @@ -0,0 +1,122 @@ + + * + * 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\Context\ChildContextBuilderInterface; +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); + } + + 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 new file mode 100644 index 0000000000000..cbe4732be0820 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Extractor/GroupPropertyListExtractorTest.php @@ -0,0 +1,174 @@ + + * + * 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\Context\ChildContextBuilderInterface; +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); + + $groupExtractor = new GroupPropertyListExtractor($factory, $extractor); + $properties = $groupExtractor->getProperties(DummyNoGroups::class); + + $this->assertNull($properties); + } + + public function testNoExtractor() + { + $factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $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 +{ + public $foo; + + public $bar; + + public $baz; +} + +class DummyGroups +{ + /** + * @Groups("foo") + */ + public $foo; + + /** + * @Groups({"bar"}) + */ + public $bar; + + /** + * @Groups({"bar", "baz"}) + */ + public $baz; +} 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..2b163165ae915 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Extractor/IgnoredPropertyListExtractorTest.php @@ -0,0 +1,82 @@ + + * + * 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\Context\ChildContextBuilderInterface; +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); + } + + 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); + } } 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; + } +} 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