Skip to content

Commit 3476ea1

Browse files
committed
Add class discriminator mapping to resolve abstract classes
1 parent 260d2f0 commit 3476ea1

File tree

4 files changed

+245
-1
lines changed

4 files changed

+245
-1
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 <samuel.roze@gmail.com>
16+
*/
17+
final class ClassDiscriminatorMapping
18+
{
19+
private $typeProperty;
20+
private $typesMapping;
21+
22+
public function __construct(string $typeProperty, array $typesMapping = [])
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 addTypeMapping(string $type, string $typeClass)
34+
{
35+
if (isset($this->typesMapping[$type])) {
36+
throw new \InvalidArgumentException(sprintf('Mapping for type "%s" already exists', $type));
37+
}
38+
39+
$this->typesMapping[$type] = $typeClass;
40+
}
41+
42+
/**
43+
* @param string $type
44+
*
45+
* @return string|null
46+
*/
47+
public function getClassForType(string $type)
48+
{
49+
if (isset($this->typesMapping[$type])) {
50+
return $this->typesMapping[$type];
51+
}
52+
53+
return null;
54+
}
55+
56+
/**
57+
* @param object $object
58+
*
59+
* @return string|null
60+
*/
61+
public function getMappedObjectType($object)
62+
{
63+
foreach ($this->typesMapping as $type => $typeClass) {
64+
if (is_a($object, $typeClass)) {
65+
return $type;
66+
}
67+
}
68+
69+
return null;
70+
}
71+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 <samuel.roze@gmail.com>
16+
*/
17+
final class ClassDiscriminatorResolver
18+
{
19+
/**
20+
* @var ClassDiscriminatorMapping[]
21+
*/
22+
private $mapping = [];
23+
24+
public function addClassMapping(string $class, ClassDiscriminatorMapping $mapping)
25+
{
26+
if (isset($this->mapping[$class])) {
27+
throw new \InvalidArgumentException(sprintf('Mapping for class "%s" already exists', $class));
28+
}
29+
30+
$this->mapping[$class] = $mapping;
31+
}
32+
33+
/**
34+
* @return ClassDiscriminatorMapping|null
35+
*/
36+
public function getMappingForClass(string $class)
37+
{
38+
if (isset($this->mapping[$class])) {
39+
return $this->mapping[$class];
40+
}
41+
42+
return null;
43+
}
44+
45+
/**
46+
* @param object $object
47+
*
48+
* @return ClassDiscriminatorMapping|null
49+
*/
50+
public function getMappingForMappedObject($object)
51+
{
52+
foreach ($this->mapping as $classMapping) {
53+
if (null !== $classMapping->getMappedObjectType($object)) {
54+
return $classMapping;
55+
}
56+
}
57+
58+
return null;
59+
}
60+
61+
/**
62+
* @param object $object
63+
*
64+
* @return string|null
65+
*/
66+
public function getTypeForMappedObject($object)
67+
{
68+
foreach ($this->mapping as $className => $classMapping) {
69+
if ($type = $classMapping->getMappedObjectType($object)) {
70+
return $type;
71+
}
72+
}
73+
74+
return null;
75+
}
76+
}

src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
use Symfony\Component\PropertyAccess\PropertyAccess;
1616
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1717
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
18+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolver;
1819
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
1920
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
21+
use Symfony\Component\Serializer\Exception\RuntimeException;
2022

2123
/**
2224
* Converts between objects and arrays using the PropertyAccess component.
@@ -30,11 +32,51 @@ class ObjectNormalizer extends AbstractObjectNormalizer
3032
*/
3133
protected $propertyAccessor;
3234

33-
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
35+
/**
36+
* @var ClassDiscriminatorResolver
37+
*/
38+
protected $classDiscriminatorResolver;
39+
40+
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolver $classDiscriminatorResolver = null)
3441
{
3542
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor);
3643

3744
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
45+
$this->classDiscriminatorResolver = $classDiscriminatorResolver ?: new ClassDiscriminatorResolver();
46+
}
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
51+
{
52+
if ($mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
53+
if (!isset($data[$mapping->getTypeProperty()])) {
54+
throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping['typeProperty'], $class));
55+
}
56+
57+
$type = $data[$mapping->getTypeProperty()];
58+
if (null === ($class = $mapping->getClassForType($type))) {
59+
throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class));
60+
}
61+
62+
$reflectionClass = new \ReflectionClass($class);
63+
}
64+
65+
return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format);
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
protected function getAttributes($object, $format = null, array $context)
72+
{
73+
$attributes = parent::getAttributes($object, $format, $context);
74+
75+
if ($mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
76+
array_unshift($attributes, $mapping->getTypeProperty());
77+
}
78+
79+
return $attributes;
3880
}
3981

4082
/**
@@ -98,6 +140,12 @@ protected function extractAttributes($object, $format = null, array $context = a
98140
*/
99141
protected function getAttributeValue($object, $attribute, $format = null, array $context = array())
100142
{
143+
if ($mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
144+
if ($attribute == $mapping->getTypeProperty()) {
145+
return $this->classDiscriminatorResolver->getTypeForMappedObject($object);
146+
}
147+
}
148+
101149
return $this->propertyAccessor->getValue($object, $attribute);
102150
}
103151

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
namespace Symfony\Component\Serializer\Tests;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Serializer\DiscriminatorMap\DiscriminatorMapObjectNormalizer;
16+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
17+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolver;
1518
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
1619
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
1720
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
@@ -346,6 +349,52 @@ public function testDeserializeObjectConstructorWithObjectTypeHint()
346349

347350
$this->assertEquals(new Foo(new Bar('baz')), $serializer->deserialize($jsonData, Foo::class, 'json'));
348351
}
352+
353+
public function testDeserializeFromAnAbstractClass()
354+
{
355+
$jsonData = '{"type":"first","common":"blah","first":"first"}';
356+
357+
$discriminatorResolver = new ClassDiscriminatorResolver();
358+
$discriminatorResolver->addClassMapping(AbstractExample::class, new ClassDiscriminatorMapping('type', [
359+
'first' => FirstExample::class,
360+
]));
361+
362+
$serializer = new Serializer(array(new ObjectNormalizer(null, null, null, null, $discriminatorResolver)), array('json' => new JsonEncoder()));
363+
364+
$example = new FirstExample();
365+
$example->common = 'blah';
366+
$example->first = 'first';
367+
368+
$this->assertEquals($example, $serializer->deserialize($jsonData, AbstractExample::class, 'json'));
369+
}
370+
371+
public function testSerializeAnObjectPartOfAnAbstractDefinition()
372+
{
373+
$discriminatorResolver = new ClassDiscriminatorResolver();
374+
$discriminatorResolver->addClassMapping(AbstractExample::class, new ClassDiscriminatorMapping('type', [
375+
'first' => FirstExample::class,
376+
]));
377+
378+
$serializer = new Serializer(array(new ObjectNormalizer(null, null, null, null, $discriminatorResolver)), array('json' => new JsonEncoder()));
379+
380+
$example = new FirstExample();
381+
$example->common = 'blah';
382+
$example->first = 'first';
383+
384+
$jsonData = '{"type":"first","first":"first","common":"blah"}';
385+
386+
$this->assertEquals($jsonData, $serializer->serialize($example, 'json'));
387+
}
388+
}
389+
390+
abstract class AbstractExample
391+
{
392+
public $common;
393+
}
394+
395+
class FirstExample extends AbstractExample
396+
{
397+
public $first;
349398
}
350399

351400
class Model

0 commit comments

Comments
 (0)