Skip to content

Commit e6960e1

Browse files
committed
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
2 parents d731b56 + ee5818c commit e6960e1

25 files changed

+840
-9
lines changed

Annotation/DiscriminatorMap.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Annotation;
13+
14+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
15+
16+
/**
17+
* Annotation class for @DiscriminatorMap().
18+
*
19+
* @Annotation
20+
* @Target({"CLASS"})
21+
*
22+
* @author Samuel Roze <[email protected]>
23+
*/
24+
class DiscriminatorMap
25+
{
26+
/**
27+
* @var string
28+
*/
29+
private $typeProperty;
30+
31+
/**
32+
* @var array
33+
*/
34+
private $mapping;
35+
36+
/**
37+
* @param array $data
38+
*
39+
* @throws InvalidArgumentException
40+
*/
41+
public function __construct(array $data)
42+
{
43+
if (empty($data['typeProperty'])) {
44+
throw new InvalidArgumentException(sprintf('Parameter "typeProperty" of annotation "%s" cannot be empty.', get_class($this)));
45+
}
46+
47+
if (empty($data['mapping'])) {
48+
throw new InvalidArgumentException(sprintf('Parameter "mapping" of annotation "%s" cannot be empty.', get_class($this)));
49+
}
50+
51+
$this->typeProperty = $data['typeProperty'];
52+
$this->mapping = $data['mapping'];
53+
}
54+
55+
public function getTypeProperty(): string
56+
{
57+
return $this->typeProperty;
58+
}
59+
60+
public function getMapping(): array
61+
{
62+
return $this->mapping;
63+
}
64+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Mapping;
13+
14+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
15+
16+
/**
17+
* @author Samuel Roze <[email protected]>
18+
*/
19+
class ClassDiscriminatorFromClassMetadata implements ClassDiscriminatorResolverInterface
20+
{
21+
/**
22+
* @var ClassMetadataFactoryInterface
23+
*/
24+
private $classMetadataFactory;
25+
private $mappingForMappedObjectCache = array();
26+
27+
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory)
28+
{
29+
$this->classMetadataFactory = $classMetadataFactory;
30+
}
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public function getMappingForClass(string $class): ?ClassDiscriminatorMapping
36+
{
37+
if ($this->classMetadataFactory->hasMetadataFor($class)) {
38+
return $this->classMetadataFactory->getMetadataFor($class)->getClassDiscriminatorMapping();
39+
}
40+
41+
return null;
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
public function getMappingForMappedObject($object): ?ClassDiscriminatorMapping
48+
{
49+
if ($this->classMetadataFactory->hasMetadataFor($object)) {
50+
$metadata = $this->classMetadataFactory->getMetadataFor($object);
51+
52+
if (null !== $metadata->getClassDiscriminatorMapping()) {
53+
return $metadata->getClassDiscriminatorMapping();
54+
}
55+
}
56+
57+
$cacheKey = is_object($object) ? get_class($object) : $object;
58+
if (!array_key_exists($cacheKey, $this->mappingForMappedObjectCache)) {
59+
$this->mappingForMappedObjectCache[$cacheKey] = $this->resolveMappingForMappedObject($object);
60+
}
61+
62+
return $this->mappingForMappedObjectCache[$cacheKey];
63+
}
64+
65+
/**
66+
* {@inheritdoc}
67+
*/
68+
public function getTypeForMappedObject($object): ?string
69+
{
70+
if (null === $mapping = $this->getMappingForMappedObject($object)) {
71+
return null;
72+
}
73+
74+
return $mapping->getMappedObjectType($object);
75+
}
76+
77+
private function resolveMappingForMappedObject($object)
78+
{
79+
$reflectionClass = new \ReflectionClass($object);
80+
if ($parentClass = $reflectionClass->getParentClass()) {
81+
return $this->getMappingForMappedObject($parentClass->getName());
82+
}
83+
84+
foreach ($reflectionClass->getInterfaceNames() as $interfaceName) {
85+
if (null !== ($interfaceMapping = $this->getMappingForMappedObject($interfaceName))) {
86+
return $interfaceMapping;
87+
}
88+
}
89+
90+
return null;
91+
}
92+
}

Mapping/ClassDiscriminatorMapping.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Mapping;
13+
14+
/**
15+
* @author Samuel Roze <[email protected]>
16+
*/
17+
class ClassDiscriminatorMapping
18+
{
19+
private $typeProperty;
20+
private $typesMapping;
21+
22+
public function __construct(string $typeProperty, array $typesMapping = array())
23+
{
24+
$this->typeProperty = $typeProperty;
25+
$this->typesMapping = $typesMapping;
26+
}
27+
28+
public function getTypeProperty(): string
29+
{
30+
return $this->typeProperty;
31+
}
32+
33+
public function getClassForType(string $type): ?string
34+
{
35+
if (isset($this->typesMapping[$type])) {
36+
return $this->typesMapping[$type];
37+
}
38+
39+
return null;
40+
}
41+
42+
/**
43+
* @param object|string $object
44+
*
45+
* @return string|null
46+
*/
47+
public function getMappedObjectType($object): ?string
48+
{
49+
foreach ($this->typesMapping as $type => $typeClass) {
50+
if (is_a($object, $typeClass)) {
51+
return $type;
52+
}
53+
}
54+
55+
return null;
56+
}
57+
58+
public function getTypesMapping(): array
59+
{
60+
return $this->typesMapping;
61+
}
62+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Mapping;
13+
14+
/**
15+
* Knows how to get the class discriminator mapping for classes and objects.
16+
*
17+
* @author Samuel Roze <[email protected]>
18+
*/
19+
interface ClassDiscriminatorResolverInterface
20+
{
21+
/**
22+
* @param string $class
23+
*
24+
* @return ClassDiscriminatorMapping|null
25+
*/
26+
public function getMappingForClass(string $class): ?ClassDiscriminatorMapping;
27+
28+
/**
29+
* @param object|string $object
30+
*
31+
* @return ClassDiscriminatorMapping|null
32+
*/
33+
public function getMappingForMappedObject($object): ?ClassDiscriminatorMapping;
34+
35+
/**
36+
* @param object|string $object
37+
*
38+
* @return string|null
39+
*/
40+
public function getTypeForMappedObject($object): ?string;
41+
}

Mapping/ClassMetadata.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,25 @@ class ClassMetadata implements ClassMetadataInterface
3939
*/
4040
private $reflClass;
4141

42-
public function __construct(string $class)
42+
/**
43+
* @var ClassDiscriminatorMapping|null
44+
*
45+
* @internal This property is public in order to reduce the size of the
46+
* class' serialized representation. Do not access it. Use
47+
* {@link getClassDiscriminatorMapping()} instead.
48+
*/
49+
public $classDiscriminatorMapping;
50+
51+
/**
52+
* Constructs a metadata for the given class.
53+
*
54+
* @param string $class
55+
* @param ClassDiscriminatorMapping|null $classDiscriminatorMapping
56+
*/
57+
public function __construct(string $class, ClassDiscriminatorMapping $classDiscriminatorMapping = null)
4358
{
4459
$this->name = $class;
60+
$this->classDiscriminatorMapping = $classDiscriminatorMapping;
4561
}
4662

4763
/**
@@ -94,6 +110,22 @@ public function getReflectionClass()
94110
return $this->reflClass;
95111
}
96112

113+
/**
114+
* {@inheritdoc}
115+
*/
116+
public function getClassDiscriminatorMapping()
117+
{
118+
return $this->classDiscriminatorMapping;
119+
}
120+
121+
/**
122+
* {@inheritdoc}
123+
*/
124+
public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null)
125+
{
126+
$this->classDiscriminatorMapping = $mapping;
127+
}
128+
97129
/**
98130
* Returns the names of the properties that should be serialized.
99131
*
@@ -104,6 +136,7 @@ public function __sleep()
104136
return array(
105137
'name',
106138
'attributesMetadata',
139+
'classDiscriminatorMapping',
107140
);
108141
}
109142
}

Mapping/ClassMetadataInterface.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,14 @@ public function merge(ClassMetadataInterface $classMetadata);
5454
* @return \ReflectionClass
5555
*/
5656
public function getReflectionClass();
57+
58+
/**
59+
* @return ClassDiscriminatorMapping|null
60+
*/
61+
public function getClassDiscriminatorMapping();
62+
63+
/**
64+
* @param ClassDiscriminatorMapping|null $mapping
65+
*/
66+
public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null);
5767
}

Mapping/Loader/AnnotationLoader.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
namespace Symfony\Component\Serializer\Mapping\Loader;
1313

1414
use Doctrine\Common\Annotations\Reader;
15+
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
1516
use Symfony\Component\Serializer\Annotation\Groups;
1617
use Symfony\Component\Serializer\Annotation\MaxDepth;
1718
use Symfony\Component\Serializer\Exception\MappingException;
1819
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
20+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
1921
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
2022

2123
/**
@@ -43,6 +45,15 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
4345

4446
$attributesMetadata = $classMetadata->getAttributesMetadata();
4547

48+
foreach ($this->reader->getClassAnnotations($reflectionClass) as $annotation) {
49+
if ($annotation instanceof DiscriminatorMap) {
50+
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
51+
$annotation->getTypeProperty(),
52+
$annotation->getMapping()
53+
));
54+
}
55+
}
56+
4657
foreach ($reflectionClass->getProperties() as $property) {
4758
if (!isset($attributesMetadata[$property->name])) {
4859
$attributesMetadata[$property->name] = new AttributeMetadata($property->name);

Mapping/Loader/XmlFileLoader.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Config\Util\XmlUtils;
1515
use Symfony\Component\Serializer\Exception\MappingException;
1616
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
17+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
1718
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
1819

1920
/**
@@ -67,6 +68,18 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
6768
}
6869
}
6970

71+
if (isset($xml->{'discriminator-map'})) {
72+
$mapping = array();
73+
foreach ($xml->{'discriminator-map'}->mapping as $element) {
74+
$mapping[(string) $element->attributes()->type] = (string) $element->attributes()->class;
75+
}
76+
77+
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
78+
(string) $xml->{'discriminator-map'}->attributes()->{'type-property'},
79+
$mapping
80+
));
81+
}
82+
7083
return true;
7184
}
7285

0 commit comments

Comments
 (0)