From 4c70e94b76d681083b86a79e156f0bca188e885b Mon Sep 17 00:00:00 2001 From: Samuel ROZE Date: Sat, 15 Apr 2017 17:59:12 +0100 Subject: [PATCH 1/2] Add class discriminator mapping to resolve abstract classes Add the `discriminator_class_mapping` in the framework extension Discriminator map is compatible with interfaces --- .../Resources/config/serializer.xml | 7 ++ .../Annotation/DiscriminatorMap.php | 64 ++++++++++ .../ClassDiscriminatorFromClassMetadata.php | 92 +++++++++++++++ .../Mapping/ClassDiscriminatorMapping.php | 62 ++++++++++ .../ClassDiscriminatorResolverInterface.php | 41 +++++++ .../Serializer/Mapping/ClassMetadata.php | 35 +++++- .../Mapping/ClassMetadataInterface.php | 10 ++ .../Mapping/Loader/AnnotationLoader.php | 11 ++ .../Mapping/Loader/XmlFileLoader.php | 13 +++ .../Mapping/Loader/YamlFileLoader.php | 16 +++ .../serializer-mapping-1.0.xsd | 15 ++- .../Normalizer/AbstractObjectNormalizer.php | 55 ++++++++- .../Normalizer/ObjectNormalizer.php | 13 ++- .../Tests/Annotation/DiscriminatorMapTest.php | 67 +++++++++++ .../Tests/Fixtures/AbstractDummy.php | 30 +++++ .../Fixtures/AbstractDummyFirstChild.php | 24 ++++ .../Fixtures/AbstractDummySecondChild.php | 24 ++++ .../Tests/Fixtures/DummyMessageInterface.php | 26 +++++ .../Tests/Fixtures/DummyMessageNumberOne.php | 20 ++++ .../Tests/Fixtures/serialization.xml | 9 ++ .../Tests/Fixtures/serialization.yml | 8 ++ .../Mapping/ClassDiscriminatorMappingTest.php | 43 +++++++ .../Mapping/Loader/AnnotationLoaderTest.php | 21 ++++ .../Mapping/Loader/XmlFileLoaderTest.php | 20 ++++ .../Mapping/Loader/YamlFileLoaderTest.php | 20 ++++ .../Serializer/Tests/SerializerTest.php | 110 ++++++++++++++++++ 26 files changed, 847 insertions(+), 9 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Annotation/DiscriminatorMap.php create mode 100644 src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorFromClassMetadata.php create mode 100644 src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorMapping.php create mode 100644 src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorResolverInterface.php create mode 100644 src/Symfony/Component/Serializer/Tests/Annotation/DiscriminatorMapTest.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummy.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummyFirstChild.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummySecondChild.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberOne.php create mode 100644 src/Symfony/Component/Serializer/Tests/Mapping/ClassDiscriminatorMappingTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 4a5c276cdf8e4..0fd7052850718 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -24,6 +24,12 @@ + + + + @@ -50,6 +56,7 @@ null + diff --git a/src/Symfony/Component/Serializer/Annotation/DiscriminatorMap.php b/src/Symfony/Component/Serializer/Annotation/DiscriminatorMap.php new file mode 100644 index 0000000000000..61b952610c4bc --- /dev/null +++ b/src/Symfony/Component/Serializer/Annotation/DiscriminatorMap.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Annotation; + +use Symfony\Component\Serializer\Exception\InvalidArgumentException; + +/** + * Annotation class for @DiscriminatorMap(). + * + * @Annotation + * @Target({"CLASS"}) + * + * @author Samuel Roze + */ +class DiscriminatorMap +{ + /** + * @var string + */ + private $typeProperty; + + /** + * @var array + */ + private $mapping; + + /** + * @param array $data + * + * @throws InvalidArgumentException + */ + public function __construct(array $data) + { + if (empty($data['typeProperty'])) { + throw new InvalidArgumentException(sprintf('Parameter "typeProperty" of annotation "%s" cannot be empty.', get_class($this))); + } + + if (empty($data['mapping'])) { + throw new InvalidArgumentException(sprintf('Parameter "mapping" of annotation "%s" cannot be empty.', get_class($this))); + } + + $this->typeProperty = $data['typeProperty']; + $this->mapping = $data['mapping']; + } + + public function getTypeProperty(): string + { + return $this->typeProperty; + } + + public function getMapping(): array + { + return $this->mapping; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorFromClassMetadata.php b/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorFromClassMetadata.php new file mode 100644 index 0000000000000..7196651624369 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorFromClassMetadata.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Mapping; + +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +/** + * @author Samuel Roze + */ +class ClassDiscriminatorFromClassMetadata implements ClassDiscriminatorResolverInterface +{ + /** + * @var ClassMetadataFactoryInterface + */ + private $classMetadataFactory; + private $mappingForMappedObjectCache = array(); + + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory) + { + $this->classMetadataFactory = $classMetadataFactory; + } + + /** + * {@inheritdoc} + */ + public function getMappingForClass(string $class): ?ClassDiscriminatorMapping + { + if ($this->classMetadataFactory->hasMetadataFor($class)) { + return $this->classMetadataFactory->getMetadataFor($class)->getClassDiscriminatorMapping(); + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function getMappingForMappedObject($object): ?ClassDiscriminatorMapping + { + if ($this->classMetadataFactory->hasMetadataFor($object)) { + $metadata = $this->classMetadataFactory->getMetadataFor($object); + + if (null !== $metadata->getClassDiscriminatorMapping()) { + return $metadata->getClassDiscriminatorMapping(); + } + } + + $cacheKey = is_object($object) ? get_class($object) : $object; + if (!array_key_exists($cacheKey, $this->mappingForMappedObjectCache)) { + $this->mappingForMappedObjectCache[$cacheKey] = $this->resolveMappingForMappedObject($object); + } + + return $this->mappingForMappedObjectCache[$cacheKey]; + } + + /** + * {@inheritdoc} + */ + public function getTypeForMappedObject($object): ?string + { + if (null === $mapping = $this->getMappingForMappedObject($object)) { + return null; + } + + return $mapping->getMappedObjectType($object); + } + + private function resolveMappingForMappedObject($object) + { + $reflectionClass = new \ReflectionClass($object); + if ($parentClass = $reflectionClass->getParentClass()) { + return $this->getMappingForMappedObject($parentClass->getName()); + } + + foreach ($reflectionClass->getInterfaceNames() as $interfaceName) { + if (null !== ($interfaceMapping = $this->getMappingForMappedObject($interfaceName))) { + return $interfaceMapping; + } + } + + return null; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorMapping.php b/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorMapping.php new file mode 100644 index 0000000000000..5d33a001fd3de --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorMapping.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Mapping; + +/** + * @author Samuel Roze + */ +class ClassDiscriminatorMapping +{ + private $typeProperty; + private $typesMapping; + + public function __construct(string $typeProperty, array $typesMapping = array()) + { + $this->typeProperty = $typeProperty; + $this->typesMapping = $typesMapping; + } + + public function getTypeProperty(): string + { + return $this->typeProperty; + } + + public function getClassForType(string $type): ?string + { + if (isset($this->typesMapping[$type])) { + return $this->typesMapping[$type]; + } + + return null; + } + + /** + * @param object|string $object + * + * @return string|null + */ + public function getMappedObjectType($object): ?string + { + foreach ($this->typesMapping as $type => $typeClass) { + if (is_a($object, $typeClass)) { + return $type; + } + } + + return null; + } + + public function getTypesMapping(): array + { + return $this->typesMapping; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorResolverInterface.php b/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorResolverInterface.php new file mode 100644 index 0000000000000..073947bde5f23 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorResolverInterface.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Mapping; + +/** + * Knows how to get the class discriminator mapping for classes and objects. + * + * @author Samuel Roze + */ +interface ClassDiscriminatorResolverInterface +{ + /** + * @param string $class + * + * @return ClassDiscriminatorMapping|null + */ + public function getMappingForClass(string $class): ?ClassDiscriminatorMapping; + + /** + * @param object|string $object + * + * @return ClassDiscriminatorMapping|null + */ + public function getMappingForMappedObject($object): ?ClassDiscriminatorMapping; + + /** + * @param object|string $object + * + * @return string|null + */ + public function getTypeForMappedObject($object): ?string; +} diff --git a/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php b/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php index 75401fc14d05d..e1d474504c25b 100644 --- a/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php @@ -39,9 +39,25 @@ class ClassMetadata implements ClassMetadataInterface */ private $reflClass; - public function __construct(string $class) + /** + * @var ClassDiscriminatorMapping|null + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getClassDiscriminatorMapping()} instead. + */ + public $classDiscriminatorMapping; + + /** + * Constructs a metadata for the given class. + * + * @param string $class + * @param ClassDiscriminatorMapping|null $classDiscriminatorMapping + */ + public function __construct(string $class, ClassDiscriminatorMapping $classDiscriminatorMapping = null) { $this->name = $class; + $this->classDiscriminatorMapping = $classDiscriminatorMapping; } /** @@ -94,6 +110,22 @@ public function getReflectionClass() return $this->reflClass; } + /** + * {@inheritdoc} + */ + public function getClassDiscriminatorMapping() + { + return $this->classDiscriminatorMapping; + } + + /** + * {@inheritdoc} + */ + public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null) + { + $this->classDiscriminatorMapping = $mapping; + } + /** * Returns the names of the properties that should be serialized. * @@ -104,6 +136,7 @@ public function __sleep() return array( 'name', 'attributesMetadata', + 'classDiscriminatorMapping', ); } } diff --git a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php index 3811e56548a0c..ddcffe97c9b3f 100644 --- a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php @@ -54,4 +54,14 @@ public function merge(ClassMetadataInterface $classMetadata); * @return \ReflectionClass */ public function getReflectionClass(); + + /** + * @return ClassDiscriminatorMapping|null + */ + public function getClassDiscriminatorMapping(); + + /** + * @param ClassDiscriminatorMapping|null $mapping + */ + public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null); } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php index 5527b5f71731e..0c195f671dad9 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -12,10 +12,12 @@ namespace Symfony\Component\Serializer\Mapping\Loader; use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; /** @@ -43,6 +45,15 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) $attributesMetadata = $classMetadata->getAttributesMetadata(); + foreach ($this->reader->getClassAnnotations($reflectionClass) as $annotation) { + if ($annotation instanceof DiscriminatorMap) { + $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( + $annotation->getTypeProperty(), + $annotation->getMapping() + )); + } + } + foreach ($reflectionClass->getProperties() as $property) { if (!isset($attributesMetadata[$property->name])) { $attributesMetadata[$property->name] = new AttributeMetadata($property->name); diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php index 76d064326f168..eec766f91d533 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; /** @@ -67,6 +68,18 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) } } + if (isset($xml->{'discriminator-map'})) { + $mapping = array(); + foreach ($xml->{'discriminator-map'}->mapping as $element) { + $mapping[(string) $element->attributes()->type] = (string) $element->attributes()->class; + } + + $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( + (string) $xml->{'discriminator-map'}->attributes()->{'type-property'}, + $mapping + )); + } + return true; } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index 970241d34767f..706d3e414b2f9 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -13,6 +13,7 @@ use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Yaml\Parser; @@ -86,6 +87,21 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) } } + if (isset($yaml['discriminator_map'])) { + if (!isset($yaml['discriminator_map']['type_property'])) { + throw new MappingException(sprintf('The "type_property" key must be set for the discriminator map of the class "%s" in "%s".', $classMetadata->getName(), $this->file)); + } + + if (!isset($yaml['discriminator_map']['mapping'])) { + throw new MappingException(sprintf('The "mapping" key must be set for the discriminator map of the class "%s" in "%s".', $classMetadata->getName(), $this->file)); + } + + $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( + $yaml['discriminator_map']['type_property'], + $yaml['discriminator_map']['mapping'] + )); + } + return true; } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd index afa8b92191362..14eff8c4dea7f 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd +++ b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd @@ -8,7 +8,7 @@ @@ -37,10 +37,23 @@ + + + + + + + + + + + + + propertyTypeExtractor = $propertyTypeExtractor; + + if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) { + $classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + } + $this->classDiscriminatorResolver = $classDiscriminatorResolver; } /** @@ -101,6 +114,28 @@ public function normalize($object, $format = null, array $context = array()) return $data; } + /** + * {@inheritdoc} + */ + protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null) + { + if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { + 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)); + } + + $class = $mappedClass; + $reflectionClass = new \ReflectionClass($class); + } + + return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format); + } + /** * Gets and caches attributes for the given object, format and context. * @@ -137,7 +172,13 @@ protected function getAttributes($object, $format = null, array $context) return $this->attributesCache[$class]; } - return $this->attributesCache[$class] = $this->extractAttributes($object, $format, $context); + $attributes = $this->extractAttributes($object, $format, $context); + + if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) { + array_unshift($attributes, $mapping->getTypeProperty()); + } + + return $this->attributesCache[$class] = $attributes; } /** @@ -168,7 +209,11 @@ abstract protected function getAttributeValue($object, $attribute, $format = nul */ public function supportsDenormalization($data, $type, $format = null) { - return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type); + if (!isset($this->cache[$type])) { + $this->cache[$type] = class_exists($type) || (interface_exists($type) && null !== $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type)); + } + + return $this->cache[$type]; } /** @@ -229,7 +274,7 @@ abstract protected function setAttributeValue($object, $attribute, $value, $form /** * Validates the submitted data and denormalizes it. * - * @param mixed $data + * @param mixed $data * * @return mixed * @@ -298,7 +343,7 @@ private function validateAndDenormalize(string $currentClass, string $attribute, /** * Sets an attribute and apply the name converter if necessary. * - * @param mixed $attributeValue + * @param mixed $attributeValue */ private function updateData(array $data, string $attribute, $attributeValue): array { diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index a92eb176d9c6f..294fd3b322491 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -16,6 +16,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\Serializer\Exception\RuntimeException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -28,13 +29,13 @@ class ObjectNormalizer extends AbstractObjectNormalizer { protected $propertyAccessor; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null) { if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccess')) { throw new RuntimeException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.'); } - parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor); + parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver); $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } @@ -100,6 +101,14 @@ protected function extractAttributes($object, $format = null, array $context = a */ protected function getAttributeValue($object, $attribute, $format = null, array $context = array()) { + if (null !== $this->classDiscriminatorResolver) { + $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object); + + if (null !== $mapping && $attribute == $mapping->getTypeProperty()) { + return $this->classDiscriminatorResolver->getTypeForMappedObject($object); + } + } + return $this->propertyAccessor->getValue($object, $attribute); } diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/DiscriminatorMapTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/DiscriminatorMapTest.php new file mode 100644 index 0000000000000..df2f111fa34c3 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Annotation/DiscriminatorMapTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Annotation; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; + +/** + * @author Samuel Roze + */ +class DiscriminatorMapTest extends TestCase +{ + public function testGetTypePropertyAndMapping() + { + $annotation = new DiscriminatorMap(array('typeProperty' => 'type', 'mapping' => array( + 'foo' => 'FooClass', + 'bar' => 'BarClass', + ))); + + $this->assertEquals('type', $annotation->getTypeProperty()); + $this->assertEquals(array( + 'foo' => 'FooClass', + 'bar' => 'BarClass', + ), $annotation->getMapping()); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException + */ + public function testExceptionWithoutTypeProperty() + { + new DiscriminatorMap(array('mapping' => array('foo' => 'FooClass'))); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException + */ + public function testExceptionWithEmptyTypeProperty() + { + new DiscriminatorMap(array('typeProperty' => '', 'mapping' => array('foo' => 'FooClass'))); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException + */ + public function testExceptionWithoutMappingProperty() + { + new DiscriminatorMap(array('typeProperty' => 'type')); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException + */ + public function testExceptionWitEmptyMappingProperty() + { + new DiscriminatorMap(array('typeProperty' => 'type', 'mapping' => array())); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummy.php new file mode 100644 index 0000000000000..b25f7ff0c1a45 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummy.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "first"="Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild", + * "second"="Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild" + * }) + */ +abstract class AbstractDummy +{ + public $foo; + + public function __construct($foo = null) + { + $this->foo = $foo; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummyFirstChild.php b/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummyFirstChild.php new file mode 100644 index 0000000000000..645c307c35735 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummyFirstChild.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +class AbstractDummyFirstChild extends AbstractDummy +{ + public $bar; + + public function __construct($foo = null, $bar = null) + { + parent::__construct($foo); + + $this->bar = $bar; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummySecondChild.php b/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummySecondChild.php new file mode 100644 index 0000000000000..5a41b9441ad8b --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/AbstractDummySecondChild.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +class AbstractDummySecondChild extends AbstractDummy +{ + public $baz; + + public function __construct($foo = null, $baz = null) + { + parent::__construct($foo); + + $this->baz = $baz; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php new file mode 100644 index 0000000000000..f0b4c4d128c38 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "first"="Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild", + * "second"="Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild" + * }) + * + * @author Samuel Roze + */ +interface DummyMessageInterface +{ +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberOne.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberOne.php new file mode 100644 index 0000000000000..381f7f8a6c70b --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberOne.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +/** + * @author Samuel Roze + */ +class DummyMessageNumberOne implements DummyMessageInterface +{ + public $one; +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml index 9ba51cbfdf6d4..d6f5ce3795ae1 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml @@ -20,4 +20,13 @@ + + + + + + + + + diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml index c4038704a50de..a967faf2a6d49 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml @@ -10,3 +10,11 @@ max_depth: 2 bar: max_depth: 3 +'Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy': + discriminator_map: + type_property: type + mapping: + first: 'Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild' + second: 'Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild' + attributes: + foo: ~ diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/ClassDiscriminatorMappingTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/ClassDiscriminatorMappingTest.php new file mode 100644 index 0000000000000..390a5b281e99d --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/ClassDiscriminatorMappingTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Mapping; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild; + +/** + * @author Samuel Roze + */ +class ClassDiscriminatorMappingTest extends TestCase +{ + public function testGetClass() + { + $mapping = new ClassDiscriminatorMapping('type', array( + 'first' => AbstractDummyFirstChild::class, + )); + + $this->assertEquals(AbstractDummyFirstChild::class, $mapping->getClassForType('first')); + $this->assertEquals(null, $mapping->getClassForType('second')); + } + + public function testMappedObjectType() + { + $mapping = new ClassDiscriminatorMapping('type', array( + 'first' => AbstractDummyFirstChild::class, + )); + + $this->assertEquals('first', $mapping->getMappedObjectType(new AbstractDummyFirstChild())); + $this->assertEquals(null, $mapping->getMappedObjectType(new AbstractDummySecondChild())); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php index b2e5c69211227..b6566d333166c 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -13,8 +13,13 @@ use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; use Symfony\Component\Serializer\Mapping\ClassMetadata; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild; use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory; /** @@ -52,6 +57,22 @@ public function testLoadGroups() $this->assertEquals(TestClassMetadataFactory::createClassMetadata(), $classMetadata); } + public function testLoadDiscriminatorMap() + { + $classMetadata = new ClassMetadata(AbstractDummy::class); + $this->loader->loadClassMetadata($classMetadata); + + $expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', array( + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + ))); + + $expected->addAttributeMetadata(new AttributeMetadata('foo')); + $expected->getReflectionClass(); + + $this->assertEquals($expected, $classMetadata); + } + public function testLoadMaxDepth() { $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy'); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php index 974d42ee55926..db2d7fda81aa7 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -12,8 +12,13 @@ namespace Symfony\Component\Serializer\Tests\Mapping\Loader; use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\ClassMetadata; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild; use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory; /** @@ -62,4 +67,19 @@ public function testMaxDepth() $this->assertEquals(2, $attributesMetadata['foo']->getMaxDepth()); $this->assertEquals(3, $attributesMetadata['bar']->getMaxDepth()); } + + public function testLoadDiscriminatorMap() + { + $classMetadata = new ClassMetadata(AbstractDummy::class); + $this->loader->loadClassMetadata($classMetadata); + + $expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', array( + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + ))); + + $expected->addAttributeMetadata(new AttributeMetadata('foo')); + + $this->assertEquals($expected, $classMetadata); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php index 918af73b14d8f..7a0ecdd446d05 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -12,8 +12,13 @@ namespace Symfony\Component\Serializer\Tests\Mapping\Loader; use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; use Symfony\Component\Serializer\Mapping\ClassMetadata; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild; use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory; /** @@ -77,4 +82,19 @@ public function testMaxDepth() $this->assertEquals(2, $attributesMetadata['foo']->getMaxDepth()); $this->assertEquals(3, $attributesMetadata['bar']->getMaxDepth()); } + + public function testLoadDiscriminatorMap() + { + $classMetadata = new ClassMetadata(AbstractDummy::class); + $this->loader->loadClassMetadata($classMetadata); + + $expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', array( + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + ))); + + $expected->addAttributeMetadata(new AttributeMetadata('foo')); + + $this->assertEquals($expected, $classMetadata); + } } diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index f7f8594cb12bc..7550b3c9b7ba1 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -12,6 +12,10 @@ namespace Symfony\Component\Serializer\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Mapping\ClassMetadata; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -23,6 +27,11 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild; +use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild; +use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface; +use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne; use Symfony\Component\Serializer\Tests\Fixtures\TraversableDummy; use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy; use Symfony\Component\Serializer\Tests\Normalizer\TestNormalizer; @@ -346,6 +355,107 @@ public function testDeserializeObjectConstructorWithObjectTypeHint() $this->assertEquals(new Foo(new Bar('baz')), $serializer->deserialize($jsonData, Foo::class, 'json')); } + + public function testDeserializeAndSerializeAbstractObjectsWithTheClassMetadataDiscriminatorResolver() + { + $example = new AbstractDummyFirstChild('foo-value', 'bar-value'); + + $loaderMock = $this->getMockBuilder(ClassMetadataFactoryInterface::class)->getMock(); + $loaderMock->method('hasMetadataFor')->will($this->returnValueMap(array( + array( + AbstractDummy::class, + true, + ), + ))); + + $loaderMock->method('getMetadataFor')->will($this->returnValueMap(array( + array( + AbstractDummy::class, + new ClassMetadata( + AbstractDummy::class, + new ClassDiscriminatorMapping('type', array( + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + )) + ), + ), + ))); + + $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); + $serializer = new Serializer(array(new ObjectNormalizer(null, null, null, null, $discriminatorResolver)), array('json' => new JsonEncoder())); + + $jsonData = '{"type":"first","bar":"bar-value","foo":"foo-value"}'; + + $deserialized = $serializer->deserialize($jsonData, AbstractDummy::class, 'json'); + $this->assertEquals($example, $deserialized); + + $serialized = $serializer->serialize($deserialized, 'json'); + $this->assertEquals($jsonData, $serialized); + } + + public function testDeserializeAndSerializeInterfacedObjectsWithTheClassMetadataDiscriminatorResolver() + { + $example = new DummyMessageNumberOne(); + $example->one = 1; + + $jsonData = '{"message-type":"one","one":1}'; + + $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($this->metadataFactoryMockForDummyInterface()); + $serializer = new Serializer(array(new ObjectNormalizer(null, null, null, null, $discriminatorResolver)), array('json' => new JsonEncoder())); + + $deserialized = $serializer->deserialize($jsonData, DummyMessageInterface::class, 'json'); + $this->assertEquals($example, $deserialized); + + $serialized = $serializer->serialize($deserialized, 'json'); + $this->assertEquals($jsonData, $serialized); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\RuntimeException + * @expectedExceptionMessage The type "second" has no mapped class for the abstract object "Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface" + */ + public function testExceptionWhenTypeIsNotKnownInDiscriminator() + { + $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($this->metadataFactoryMockForDummyInterface()); + $serializer = new Serializer(array(new ObjectNormalizer(null, null, null, null, $discriminatorResolver)), array('json' => new JsonEncoder())); + $serializer->deserialize('{"message-type":"second","one":1}', DummyMessageInterface::class, 'json'); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\RuntimeException + * @expectedExceptionMessage Type property "message-type" not found for the abstract object "Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface" + */ + public function testExceptionWhenTypeIsNotInTheBodyToDeserialiaze() + { + $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($this->metadataFactoryMockForDummyInterface()); + $serializer = new Serializer(array(new ObjectNormalizer(null, null, null, null, $discriminatorResolver)), array('json' => new JsonEncoder())); + $serializer->deserialize('{"one":1}', DummyMessageInterface::class, 'json'); + } + + private function metadataFactoryMockForDummyInterface() + { + $factoryMock = $this->getMockBuilder(ClassMetadataFactoryInterface::class)->getMock(); + $factoryMock->method('hasMetadataFor')->will($this->returnValueMap(array( + array( + DummyMessageInterface::class, + true, + ), + ))); + + $factoryMock->method('getMetadataFor')->will($this->returnValueMap(array( + array( + DummyMessageInterface::class, + new ClassMetadata( + DummyMessageInterface::class, + new ClassDiscriminatorMapping('message-type', array( + 'one' => DummyMessageNumberOne::class, + )) + ), + ), + ))); + + return $factoryMock; + } } class Model From 72ee086f22fcbafac4fb52936b27a3cdc3dfddb6 Mon Sep 17 00:00:00 2001 From: Samuel ROZE Date: Sun, 3 Dec 2017 12:35:04 +0000 Subject: [PATCH 2/2] Remove the class discriminator definition and alias if class do not exists --- .../DependencyInjection/FrameworkExtension.php | 6 ++++++ .../Bundle/FrameworkBundle/Resources/config/serializer.xml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index ec9010a16e576..40016e87806ff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -62,6 +62,7 @@ use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -1153,6 +1154,11 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->removeDefinition('serializer.normalizer.dateinterval'); } + if (!class_exists(ClassDiscriminatorFromClassMetadata::class)) { + $container->removeAlias('Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface'); + $container->removeDefinition('serializer.mapping.class_discriminator_resolver'); + } + $chainLoader = $container->getDefinition('serializer.mapping.chain_loader'); if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccessor')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 0fd7052850718..90c4d1c5b5050 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -56,7 +56,7 @@ null - + 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