feature #24375 [Serializer] Serialize and deserialize from abstract classes (sroze)

This PR was squashed before being merged into the 4.1-dev branch (closes #24375).

Discussion
----------

[Serializer] Serialize and deserialize from abstract classes

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | ø
| License       | MIT
| Doc PR        | Not yet

This PR adds a feature in the Serializer: allow to serialize and de-serialize abstract classes. Such feature is especially useful when dealing with domain objects.

# Example

Let's take the example of the following objects:
- `CodeRepository` defines a set of properties like `name` and `url`
- `GitHubCodeRepository` and `BitBucketCodeRepository` extends from the abstract `CodeRepository` class and adds a few properties.
- `Project` has a relation with a `codeRepository`, which has a type `CodeRepository`.

At the moment, the serializer can't serialize/deserialize correctly this `Project` object has it doesn't know how to deal with this `CodeRepository` abstract object.

This feature allows the serializer to deal with such situation. The `ObjectNormalizer` has now access to a `ClassDiscriminatorResolver` that knows, for a given abstract class:
- Is the "type" property it needs to read/write to uniquely identify each sub-class
- What's the name of the "type" for each sub-class mapping

# Usage without Framework Bundle

```php
$discriminatorResolver = new ClassDiscriminatorResolver();
$discriminatorResolver->addClassMapping(CodeRepository::class, new ClassDiscriminatorMapping('type', [
    'github' => GitHubCodeRepository::class,
    'bitbucket' => BitBucketCodeRepository::class,
]));

$serializer = new Serializer(array(new ObjectNormalizer(null, null, null, null, $discriminatorResolver)), array('json' => new JsonEncoder()));

$serialized = $serializer->serialize(new GitHubCodeRepository());
// {"type": "github"}

$repository = $serializer->unserialize($serialized, CodeRepository::class, 'json');
// GitHubCodeRepository

```

# Usage with the Framework Bundle

```yaml
framework:
    serializer:
        discriminator_class_mapping:
            App\CodeRepository:
                 type_property: type
                 mapping:
                    github: App\GitHubCodeRepository
                    bitbucket: App\BitBucketCodeRepository
```

# Usage with Annotations/XML/YAML

```php
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;
    }
}
```

# TODO

- [x] Working as standalone
- [x] Working with the framework bundle
- [x] Tests on mapping classes

Commits
-------

4c6e05b7ee [Serializer] Serialize and deserialize from abstract classes
This commit is contained in:
Fabien Potencier 2017-12-07 10:34:02 -08:00
commit 279dc46756
27 changed files with 853 additions and 9 deletions

View File

@ -62,6 +62,7 @@ use Symfony\Component\Routing\Loader\AnnotationFileLoader;
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 @@ class FrameworkExtension extends Extension
$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')) {

View File

@ -24,6 +24,12 @@
<service id="serializer.property_accessor" alias="property_accessor" />
<!-- Discriminator Map -->
<service id="serializer.mapping.class_discriminator_resolver" class="Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata">
<argument type="service" id="serializer.mapping.class_metadata_factory" />
</service>
<service id="Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface" alias="serializer.mapping.class_discriminator_resolver" />
<!-- Normalizer -->
<service id="serializer.normalizer.dateinterval" class="Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer">
<!-- Run before serializer.normalizer.object -->
@ -50,6 +56,7 @@
<argument>null</argument> <!-- name converter -->
<argument type="service" id="serializer.property_accessor" />
<argument type="service" id="property_info" on-invalid="ignore" />
<argument type="service" id="serializer.mapping.class_discriminator_resolver" on-invalid="ignore" />
<!-- Run after all custom normalizers -->
<tag name="serializer.normalizer" priority="-1000" />

View File

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <samuel.roze@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <samuel.roze@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <samuel.roze@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <samuel.roze@gmail.com>
*/
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;
}

View File

@ -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 @@ class ClassMetadata implements ClassMetadataInterface
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 @@ class ClassMetadata implements ClassMetadataInterface
return array(
'name',
'attributesMetadata',
'classDiscriminatorMapping',
);
}
}

View File

@ -54,4 +54,14 @@ interface ClassMetadataInterface
* @return \ReflectionClass
*/
public function getReflectionClass();
/**
* @return ClassDiscriminatorMapping|null
*/
public function getClassDiscriminatorMapping();
/**
* @param ClassDiscriminatorMapping|null $mapping
*/
public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null);
}

View File

@ -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 @@ class AnnotationLoader implements LoaderInterface
$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);

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Serializer\Mapping\Loader;
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 @@ class XmlFileLoader extends FileLoader
}
}
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;
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Serializer\Mapping\Loader;
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;
use Symfony\Component\Yaml\Yaml;
@ -87,6 +88,21 @@ class YamlFileLoader extends FileLoader
}
}
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;
}

View File

@ -8,7 +8,7 @@
<xsd:annotation>
<xsd:documentation><![CDATA[
Symfony Serializer Mapping Schema, version 1.0
Authors: Kévin Dunglas
Authors: Kévin Dunglas, Samuel Roze
A serializer mapping connects attributes with serialization groups.
]]></xsd:documentation>
@ -37,10 +37,23 @@
</xsd:annotation>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="attribute" type="attribute" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="discriminator-map" type="discriminator-map" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="discriminator-map">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="mapping" type="discriminator-map-mapping" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="type-property" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="discriminator-map-mapping">
<xsd:attribute name="type" type="xsd:string" use="required" />
<xsd:attribute name="class" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="attribute">
<xsd:annotation>
<xsd:documentation><![CDATA[

View File

@ -18,7 +18,10 @@ use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@ -38,11 +41,21 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
private $attributesCache = array();
private $cache = array();
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
/**
* @var ClassDiscriminatorResolverInterface|null
*/
protected $classDiscriminatorResolver;
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null)
{
parent::__construct($classMetadataFactory, $nameConverter);
$this->propertyTypeExtractor = $propertyTypeExtractor;
if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) {
$classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
}
$this->classDiscriminatorResolver = $classDiscriminatorResolver;
}
/**
@ -101,6 +114,28 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
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 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
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 class AbstractObjectNormalizer extends AbstractNormalizer
*/
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 class AbstractObjectNormalizer extends AbstractNormalizer
/**
* Validates the submitted data and denormalizes it.
*
* @param mixed $data
* @param mixed $data
*
* @return mixed
*
@ -298,7 +343,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
/**
* 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
{

View File

@ -16,6 +16,7 @@ use Symfony\Component\PropertyAccess\PropertyAccess;
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 @@ class ObjectNormalizer extends AbstractObjectNormalizer
*/
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);
}

View File

@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <samuel.roze@gmail.com>
*/
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()));
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <samuel.roze@gmail.com>
*/
interface DummyMessageInterface
{
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <samuel.roze@gmail.com>
*/
class DummyMessageNumberOne implements DummyMessageInterface
{
public $one;
}

View File

@ -20,4 +20,13 @@
<attribute name="bar" max-depth="3" />
</class>
<class name="Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy">
<discriminator-map type-property="type">
<mapping type="first" class="Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild" />
<mapping type="second" class="Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild" />
</discriminator-map>
<attribute name="foo" />
</class>
</serializer>

View File

@ -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: ~

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <samuel.roze@gmail.com>
*/
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()));
}
}

View File

@ -13,8 +13,13 @@ namespace Symfony\Component\Serializer\Tests\Mapping\Loader;
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 @@ class AnnotationLoaderTest extends TestCase
$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');

View File

@ -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 @@ class XmlFileLoaderTest extends TestCase
$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);
}
}

View File

@ -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 @@ class YamlFileLoaderTest extends TestCase
$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);
}
}

View File

@ -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\Serializer;
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 @@ class SerializerTest extends TestCase
$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