Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3476ea1

Browse filesBrowse files
committed
Add class discriminator mapping to resolve abstract classes
1 parent 260d2f0 commit 3476ea1
Copy full SHA for 3476ea1

File tree

4 files changed

+245
-1
lines changed
Filter options

4 files changed

+245
-1
lines changed
+71Lines 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+
}
+76Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
+49-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/SerializerTest.php
+49Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.