10000 [Serializer] Add defaultType to DiscriminatorMap · symfony/symfony@9d089bd · GitHub
[go: up one dir, main page]

Skip to content

Commit 9d089bd

Browse files
committed
[Serializer] Add defaultType to DiscriminatorMap
1 parent 946883f commit 9d089bd

18 files changed

+98
-11
lines changed

src/Symfony/Component/Serializer/Attribute/DiscriminatorMap.php

+11
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ class DiscriminatorMap
2222
/**
2323
* @param string $typeProperty The property holding the type discriminator
2424
* @param array<string, class-string> $mapping The mapping between types and classes (i.e. ['admin_user' => AdminUser::class])
25+
* @param ?string $defaultType The fallback value if nothing specified by $typeProperty
2526
*
2627
* @throws InvalidArgumentException
2728
*/
2829
public function __construct(
2930
private readonly string $typeProperty,
3031
private readonly array $mapping,
32+
private readonly ?string $defaultType = null,
3133
) {
3234
if (!$typeProperty) {
3335
throw new InvalidArgumentException(\sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class));
@@ -36,6 +38,10 @@ public function __construct(
3638
if (!$mapping) {
3739
throw new InvalidArgumentException(\sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class));
3840
}
41+
42+
if (null !== $this->defaultType && !\array_key_exists($this->defaultType, $this->mapping)) {
43+
throw new InvalidArgumentException(\sprintf('Default type "%s" given to "%s" must be present in "mapping" types.', $this->defaultType, static::class));
44+
}
3945
}
4046

4147
public function getTypeProperty(): string
@@ -47,6 +53,11 @@ public function getMapping(): array
4753
{
4854
return $this->mapping;
4955
}
56+
57+
public function getDefaultType(): ?string
58+
{
59+
return $this->defaultType;
60+
}
5061
}
5162

5263
if (!class_exists(\Symfony\Component\Serializer\Annotation\DiscriminatorMap::class, false)) {

src/Symfony/Component/Serializer/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes
88
* Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers
99
* Add `NumberNormalizer` to normalize `BcMath\Number` and `GMP` as `string`
10+
* Add `defaultType` to `DiscriminatorMap`
1011

1112
7.2
1213
---

src/Symfony/Component/Serializer/Mapping/ClassDiscriminatorMapping.php

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class ClassDiscriminatorMapping
2222
public function __construct(
2323
private readonly string $typeProperty,
2424
private array $typesMapping = [],
25+
private readonly ?string $defaultType = null,
2526
) {
2627
uasort($this->typesMapping, static function (string $a, string $b): int {
2728
if (is_a($a, $b, true)) {
@@ -61,4 +62,9 @@ public function getTypesMapping(): array
6162
{
6263
return $this->typesMapping;
6364
}
65+
66+
public function getDefaultType(): ?string
67+
{
68+
return $this->defaultType;
69+
}
6470
}

src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ private function generateDeclaredClassMetadata(array $classMetadatas): string
5555
$classDiscriminatorMapping = $classMetadata->getClassDiscriminatorMapping() ? [
5656
$classMetadata->getClassDiscriminatorMapping()->getTypeProperty(),
5757
$classMetadata->getClassDiscriminatorMapping()->getTypesMapping(),
58+
$classMetadata->getClassDiscriminatorMapping()->getDefaultType(),
5859
] : null;
5960

6061
$compiled .= \sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([

src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
5959

6060
foreach ($this->loadAttributes($reflectionClass) as $attribute) {
6161
match (true) {
62-
$attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping())),
62+
$attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping(), $attribute->getDefaultType())),
6363
$attribute instanceof Groups => $classGroups = $attribute->getGroups(),
6464
$attribute instanceof Context => $classContextAttribute = $attribute,
6565
default => null,

src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
107107

108108
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
109109
(string) $xml->{'discriminator-map'}->attributes()->{'type-property'},
110-
$mapping
110+
$mapping,
111+
$xml->{'discriminator-map'}->attributes()->{'default-type'} ?? null
111112
));
112113
}
113114

src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
133133

134134
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
135135
$yaml['discriminator_map']['type_property'],
136-
$yaml['discriminator_map']['mapping']
136+
$yaml['discriminator_map']['mapping'],
137+
$yaml['discriminator_map']['default_type'] ?? null
137138
));
138139
}
139140

src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<xsd:element name="mapping" type="discriminator-map-mapping" maxOccurs="unbounded" />
4848
</xsd:choice>
4949
<xsd:attribute name="type-property" type="xsd:string" use="required" />
50+
<xsd:attribute name="default-type" type="xsd:string" />
5051
</xsd:complexType>
5152

5253
<xsd:complexType name="discriminator-map-mapping">

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1179,7 +1179,7 @@ private function getMappedClass(array $data, string $class, array $context): str
11791179
return $class;
11801180
}
11811181

1182-
if (null === $type = $data[$mapping->getTypeProperty()] ?? null) {
1182+
if (null === $type = $data[$mapping->getTypeProperty()] ?? $mapping->getDefaultType()) {
11831183
throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false);
11841184
}
11851185

src/Symfony/Component/Serializer/Tests/Attribute/DiscriminatorMapTest.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,16 @@ public function testExceptionWithEmptyTypeProperty()
4040
new DiscriminatorMap(typeProperty: '', mapping: ['foo' => 'FooClass']);
4141
}
4242

43-
public function testExceptionWitEmptyMappingProperty()
43+
public function testExceptionWithEmptyMappingProperty()
4444
{
4545
$this->expectException(InvalidArgumentException::class);
4646
new DiscriminatorMap(typeProperty: 'type', mapping: []);
4747
}
48+
49+
public function testExceptionWithMissingDefaultTypeInMapping()
50+
{
51+
$this->expectException(InvalidArgumentException::class);
52+
$this->expectExceptionMessage(sprintf('Default type "bar" given to "%s" must be present in "mapping" types.', DiscriminatorMap::class));
53+
new DiscriminatorMap(typeProperty: 'type', mapping: ['foo' => 'FooClass'], defaultType: 'bar');
54+
}
4855
}

src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/AbstractDummy.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
'first' => AbstractDummyFirstChild::class,
1818
'second' => AbstractDummySecondChild::class,
1919
'third' => AbstractDummyThirdChild::class,
20-
])]
20+
], defaultType: 'third')]
2121
abstract class AbstractDummy
2222
{
2323
public $foo;

src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
</class>
3636

3737
<class name="Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy">
38-
<discriminator-map type-property="type">
38+
<discriminator-map type-property="type" default-type="second">
3939
<mapping type="first" class="Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild" />
4040
<mapping type="second" class="Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild" />
4141
</discriminator-map>

src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
mapping:
3333
first: 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild'
3434
second: 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild'
35+
default_type: first
3536
attributes:
3637
foo: ~
3738
'Symfony\Component\Serializer\Tests\Fixtures\Attributes\IgnoreDummy':

src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php

+23-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
1616
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler;
1717
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
18+
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy;
19+
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild;
20+
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild;
21+
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyThirdChild;
1822
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\MaxDepthDummy;
1923
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedNameDummy;
2024
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedPathDummy;
@@ -40,13 +44,15 @@ public function testItDumpMetadata()
4044
$classMetatadataFactory = new ClassMetadataFactory(new AttributeLoader());
4145

4246
$dummyMetadata = $classMetatadataFactory->getMetadataFor(Dummy::class);
47+
$abstractDummyMetadata = $classMetatadataFactory->getMetadataFor(AbstractDummy::class);
4348
$maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class);
4449
$serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class);
4550
$serializedPathDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathDummy::class);
4651
$serializedPathInConstructorDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathInConstructorDummy::class);
4752

4853
$code = (new ClassMetadataFactoryCompiler())->compile([
4954
$dummyMetadata,
55+
$abstractDummyMetadata,
5056
$maxDepthDummyMetadata,
5157
$serializedNameDummyMetadata,
5258
$serializedPathDummyMetadata,
@@ -56,7 +62,7 @@ public function testItDumpMetadata()
5662
file_put_contents($this->dumpPath, $code);
5763
$compiledMetadata = require $this->dumpPath;
5864

59-
$this->assertCount(5, $compiledMetadata);
65+
$this->assertCount(6, $compiledMetadata);
6066

6167
$this->assertArrayHasKey(Dummy::class, $compiledMetadata);
6268
$this->assertEquals([
@@ -69,6 +75,22 @@ public function testItDumpMetadata()
6975
null,
7076
], $compiledMetadata[Dummy::class]);
7177

78+
$this->assertArrayHasKey(AbstractDummy::class, $compiledMetadata);
79+
$this->assertEquals([
80+
[
81+
'foo' => [[], null, null, null],
82+
],
83+
[
84+
'type',
85+
[
86+
'first' => AbstractDummyFirstChild::class,
87+
'second' => AbstractDummySecondChild::class,
88+
'third' => AbstractDummyThirdChild::class,
89+
],
90+
'third',
91+
],
92+
], $compiledMetadata[AbstractDummy::class]);
93+
7294
$this->assertArrayHasKey(MaxDepthDummy::class, $compiledMetadata);
7395
$this->assertEquals([
7496
[

src/Symfony/Component/Serializer/Tests/Mapping/Loader/AttributeLoaderTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function testLoadDiscriminatorMap()
8585
'first' => AbstractDummyFirstChild::class,
8686
'second' => AbstractDummySecondChild::class,
8787
'third' => AbstractDummyThirdChild::class,
88-
]));
88+
], 'third'));
8989

9090
$expected->addAttributeMetadata(new AttributeMetadata('foo'));
9191
$expected->getReflectionClass();

src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public function testLoadDiscriminatorMap()
109109
$expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', [
110110
'first' => AbstractDummyFirstChild::class,
111111
'second' => AbstractDummySecondChild::class,
112-
]));
112+
], 'second'));
113113

114114
$expected->addAttributeMetadata(new AttributeMetadata('foo'));
115115

src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public function testLoadDiscriminatorMap()
126126
$expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', [
127127
'first' => AbstractDummyFirstChild::class,
128128
'second' => AbstractDummySecondChild::class,
129-
]));
129+
], 'first'));
130130

131131
$expected->addAttributeMetadata(new AttributeMetadata('foo'));
132132

src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php

+35
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,41 @@ public function hasMetadataFor($value): bool
560560
$this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux);
561561
}
562562

563+
public function testDenormalizeWithDiscriminatorMapUsesCorrectClassnameWithDefaultType()
564+
{
565+
$factory = new ClassMetadataFactory(new AttributeLoader());
566+
567+
$loaderMock = new class implements ClassMetadataFactoryInterface {
568+
public function getMetadataFor($value): ClassMetadataInterface
569+
{
570+
if (AbstractDummy::class === $value) {
571+
return new ClassMetadata(
572+
AbstractDummy::class,
573+
new ClassDiscriminatorMapping('type', [
574+
'first' => AbstractDummyFirstChild::class,
575+
'second' => AbstractDummySecondChild::class,
576+
], 'second')
577+
);
578+
}
579+
580+
throw new InvalidArgumentException(sprintf('"%s" is not handled.', $value));
581+
}
582+
583+
public function hasMetadataFor($value): bool
584+
{
585+
return AbstractDummy::class === $value;
586+
}
587+
};
588+
589+
$discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock);
590+
$normalizer = new AbstractObjectNormalizerDummy($factory, null, new PhpDocExtractor(), $discriminatorResolver);
591+
$serializer = new Serializer([$normalizer]);
592+
$normalizer->setSerializer($serializer);
593+
$normalizedData = $normalizer->denormalize(['foo' => 'foo', 'baz' => 'baz', 'quux' => ['value' => 'quux']], AbstractDummy::class);
594+
595+
$this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux);
596+
}
597+
563598
public function testDenormalizeWithDiscriminatorMapAndObjectToPopulateUsesCorrectClassname()
564599
{
565600
$factory = new ClassMetadataFactory(new AttributeLoader());

0 commit comments

Comments
 (0)
0