Skip to content

Commit cb93a4f

Browse files
committed
feature symfony#59828 [Serializer] Add defaultType to DiscriminatorMap (alanpoulain)
This PR was merged into the 7.3 branch. Discussion ---------- [Serializer] Add `defaultType` to `DiscriminatorMap` | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | N/A | License | MIT I thought it would be nice to have a default type value for the discriminator map of the serializer. For instance with this configuration: ```php #[DiscriminatorMap( typeProperty: 'type', mapping: [ 'article' => Article::class, 'video' => Video::class, ], defaultType: 'article' )] abstract class Element { public string $product; } final class Article extends Element { public string $title; } final class Video extends Element { public int $duration; } ``` It would deserialize to an `Article` with this data: ```json { "product": "desk", "title": "PHP devs love Symfony" } ``` And to a `Video` with this: ```json { "product": "desk", "duration": 8765, "type": "video" } ``` Commits ------- 9d089bd [Serializer] Add defaultType to DiscriminatorMap
2 parents a1b06b7 + 9d089bd commit cb93a4f

File tree

18 files changed

+98
-11
lines changed

18 files changed

+98
-11
lines changed

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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 8 additions & 1 deletion
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
}

0 commit comments

Comments
 (0)