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:
commit
279dc46756
@ -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')) {
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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[
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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: ~
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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');
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user