Skip to content

Navigation Menu

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

[Serializer] Add defaultType to DiscriminatorMap #59828

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions 11 src/Symfony/Component/Serializer/Attribute/DiscriminatorMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ class DiscriminatorMap
/**
* @param string $typeProperty The property holding the type discriminator
* @param array<string, class-string> $mapping The mapping between types and classes (i.e. ['admin_user' => AdminUser::class])
* @param ?string $defaultType The fallback value if nothing specified by $typeProperty
*
* @throws InvalidArgumentException
*/
public function __construct(
private readonly string $typeProperty,
private readonly array $mapping,
private readonly ?string $defaultType = null,
) {
if (!$typeProperty) {
throw new InvalidArgumentException(\sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class));
Expand All @@ -36,6 +38,10 @@ public function __construct(
if (!$mapping) {
throw new InvalidArgumentException(\sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class));
}

if (null !== $this->defaultType && !\array_key_exists($this->defaultType, $this->mapping)) {
throw new InvalidArgumentException(\sprintf('Default type "%s" given to "%s" must be present in "mapping" types.', $this->defaultType, static::class));
}
}

public function getTypeProperty(): string
Expand All @@ -47,6 +53,11 @@ public function getMapping(): array
{
return $this->mapping;
}

public function getDefaultType(): ?string
{
return $this->defaultType;
}
}

if (!class_exists(\Symfony\Component\Serializer\Annotation\DiscriminatorMap::class, false)) {
Expand Down
1 change: 1 addition & 0 deletions 1 src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG
* Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes
* Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers
* Add `NumberNormalizer` to normalize `BcMath\Number` and `GMP` as `string`
* Add `defaultType` to `DiscriminatorMap`

7.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class ClassDiscriminatorMapping
public function __construct(
private readonly string $typeProperty,
private array $typesMapping = [],
private readonly ?string $defaultType = null,
) {
uasort($this->typesMapping, static function (string $a, string $b): int {
if (is_a($a, $b, true)) {
Expand Down Expand Up @@ -61,4 +62,9 @@ public function getTypesMapping(): array
{
return $this->typesMapping;
}

public function getDefaultType(): ?string
{
return $this->defaultType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ private function generateDeclaredClassMetadata(array $classMetadatas): string
$classDiscriminatorMapping = $classMetadata->getClassDiscriminatorMapping() ? [
$classMetadata->getClassDiscriminatorMapping()->getTypeProperty(),
$classMetadata->getClassDiscriminatorMapping()->getTypesMapping(),
$classMetadata->getClassDiscriminatorMapping()->getDefaultType(),
] : null;

$compiled .= \sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool

foreach ($this->loadAttributes($reflectionClass) as $attribute) {
match (true) {
$attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping())),
$attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping(), $attribute->getDefaultType())),
$attribute instanceof Groups => $classGroups = $attribute->getGroups(),
$attribute instanceof Context => $classContextAttribute = $attribute,
default => null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool

$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
(string) $xml->{'discriminator-map'}->attributes()->{'type-property'},
$mapping
$mapping,
$xml->{'discriminator-map'}->attributes()->{'default-type'} ?? null
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool

$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
$yaml['discriminator_map']['type_property'],
$yaml['discriminator_map']['mapping']
$yaml['discriminator_map']['mapping'],
$yaml['discriminator_map']['default_type'] ?? null
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<xsd:element name="mapping" type="discriminator-map-mapping" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="type-property" type="xsd:string" use="required" />
<xsd:attribute name="default-type" type="xsd:string" />
</xsd:complexType>

<xsd:complexType name="discriminator-map-mapping">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1179,7 +1179,7 @@ private function getMappedClass(array $data, string $class, array $context): str
return $class;
}

if (null === $type = $data[$mapping->getTypeProperty()] ?? null) {
if (null === $type = $data[$mapping->getTypeProperty()] ?? $mapping->getDefaultType()) {
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,16 @@ public function testExceptionWithEmptyTypeProperty()
new DiscriminatorMap(typeProperty: '', mapping: ['foo' => 'FooClass']);
}

public function testExceptionWitEmptyMappingProperty()
public function testExceptionWithEmptyMappingProperty()
{
$this->expectException(InvalidArgumentException::class);
new DiscriminatorMap(typeProperty: 'type', mapping: []);
}

public function testExceptionWithMissingDefaultTypeInMapping()
{
$this->expectException(InvalidArgumentException::class);
alanpoulain marked this conversation as resolved.
Show resolved Hide resolved
$this->expectExceptionMessage(sprintf('Default type "bar" given to "%s" must be present in "mapping" types.', DiscriminatorMap::class));
new DiscriminatorMap(typeProperty: 'type', mapping: ['foo' => 'FooClass'], defaultType: 'bar');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
'first' => AbstractDummyFirstChild::class,
'second' => AbstractDummySecondChild::class,
'third' => AbstractDummyThirdChild::class,
])]
], defaultType: 'third')]
abstract class AbstractDummy
{
public $foo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
</class>

<class name="Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy">
<discriminator-map type-property="type">
<discriminator-map type-property="type" default-type="second">
<mapping type="first" class="Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild" />
<mapping type="second" class="Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild" />
</discriminator-map>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
mapping:
first: 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild'
second: 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild'
default_type: first
attributes:
foo: ~
'Symfony\Component\Serializer\Tests\Fixtures\Attributes\IgnoreDummy':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyThirdChild;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\MaxDepthDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedNameDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedPathDummy;
Expand All @@ -40,13 +44,15 @@ public function testItDumpMetadata()
$classMetatadataFactory = new ClassMetadataFactory(new AttributeLoader());

$dummyMetadata = $classMetatadataFactory->getMetadataFor(Dummy::class);
$abstractDummyMetadata = $classMetatadataFactory->getMetadataFor(AbstractDummy::class);
$maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class);
$serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class);
$serializedPathDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathDummy::class);
$serializedPathInConstructorDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathInConstructorDummy::class);

$code = (new ClassMetadataFactoryCompiler())->compile([
$dummyMetadata,
$abstractDummyMetadata,
$maxDepthDummyMetadata,
$serializedNameDummyMetadata,
$serializedPathDummyMetadata,
Expand All @@ -56,7 +62,7 @@ public function testItDumpMetadata()
file_put_contents($this->dumpPath, $code);
$compiledMetadata = require $this->dumpPath;

$this->assertCount(5, $compiledMetadata);
$this->assertCount(6, $compiledMetadata);

$this->assertArrayHasKey(Dummy::class, $compiledMetadata);
$this->assertEquals([
Expand All @@ -69,6 +75,22 @@ public function testItDumpMetadata()
null,
], $compiledMetadata[Dummy::class]);

$this->assertArrayHasKey(AbstractDummy::class, $compiledMetadata);
$this->assertEquals([
[
'foo' => [[], null, null, null],
],
[
'type',
[
'first' => AbstractDummyFirstChild::class,
'second' => AbstractDummySecondChild::class,
'third' => AbstractDummyThirdChild::class,
],
'third',
],
], $compiledMetadata[AbstractDummy::class]);

$this->assertArrayHasKey(MaxDepthDummy::class, $compiledMetadata);
$this->assertEquals([
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function testLoadDiscriminatorMap()
'first' => AbstractDummyFirstChild::class,
'second' => AbstractDummySecondChild::class,
'third' => AbstractDummyThirdChild::class,
]));
], 'third'));

$expected->addAttributeMetadata(new AttributeMetadata('foo'));
$expected->getReflectionClass();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public function testLoadDiscriminatorMap()
$expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', [
'first' => AbstractDummyFirstChild::class,
'second' => AbstractDummySecondChild::class,
]));
], 'second'));

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public function testLoadDiscriminatorMap()
$expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', [
'first' => AbstractDummyFirstChild::class,
'second' => AbstractDummySecondChild::class,
]));
], 'first'));

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,41 @@ public function hasMetadataFor($value): bool
$this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux);
}

public function testDenormalizeWithDiscriminatorMapUsesCorrectClassnameWithDefaultType()
{
$factory = new ClassMetadataFactory(new AttributeLoader());

$loaderMock = new class implements ClassMetadataFactoryInterface {
public function getMetadataFor($value): ClassMetadataInterface
{
if (AbstractDummy::class === $value) {
return new ClassMetadata(
AbstractDummy::class,
new ClassDiscriminatorMapping('type', [
'first' => AbstractDummyFirstChild::class,
'second' => AbstractDummySecondChild::class,
], 'second')
);
}

throw new InvalidArgumentException(sprintf('"%s" is not handled.', $value));
}

public function hasMetadataFor($value): bool
{
return AbstractDummy::class === $value;
}
};

$discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock);
$normalizer = new AbstractObjectNormalizerDummy($factory, null, new PhpDocExtractor(), $discriminatorResolver);
$serializer = new Serializer([$normalizer]);
$normalizer->setSerializer($serializer);
alanpoulain marked this conversation as resolved.
Show resolved Hide resolved
$normalizedData = $normalizer->denormalize(['foo' => 'foo', 'baz' => 'baz', 'quux' => ['value' => 'quux']], AbstractDummy::class);

$this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux);
}

public function testDenormalizeWithDiscriminatorMapAndObjectToPopulateUsesCorrectClassname()
{
$factory = new ClassMetadataFactory(new AttributeLoader());
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.