From 25ceca84dd72508f8058455be20b63ccdf18e213 Mon Sep 17 00:00:00 2001 From: Martin Parsiegla Date: Fri, 4 Jul 2025 18:36:27 +0200 Subject: [PATCH 1/7] Add discriminator support With this addition, the class metadata now also holds the discriminator information (if any exists). --- src/Builder.php | 17 ++++++ src/Metadata/ClassDiscriminatorMetadata.php | 53 +++++++++++++++++++ src/Metadata/ClassMetadata.php | 13 ++++- src/ModelParser/JMSParser.php | 13 ++++- .../RawMetadata/RawClassMetadata.php | 53 +++++++++++++++++-- src/Parser.php | 13 +++++ tests/BuilderTest.php | 21 ++++++++ tests/ModelParser/JMSParserTest.php | 42 +++++++++++++++ tests/ModelParser/Model/BaseDiscriminator.php | 14 +++++ tests/ModelParser/Model/Boat.php | 17 ++++++ tests/ModelParser/Model/CabinCruiser.php | 9 ++++ tests/ModelParser/Model/Car.php | 10 ++++ .../Model/ClassWithVehicleProperty.php | 8 +++ .../Model/DiscriminatorWithFieldProperty.php | 10 ++++ tests/ModelParser/Model/Ferry.php | 9 ++++ tests/ModelParser/Model/Moped.php | 10 ++++ tests/ModelParser/Model/Vehicle.php | 17 ++++++ tests/ParserTest.php | 22 ++++++++ 18 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 src/Metadata/ClassDiscriminatorMetadata.php create mode 100644 tests/ModelParser/Model/BaseDiscriminator.php create mode 100644 tests/ModelParser/Model/Boat.php create mode 100644 tests/ModelParser/Model/CabinCruiser.php create mode 100644 tests/ModelParser/Model/Car.php create mode 100644 tests/ModelParser/Model/ClassWithVehicleProperty.php create mode 100644 tests/ModelParser/Model/DiscriminatorWithFieldProperty.php create mode 100644 tests/ModelParser/Model/Ferry.php create mode 100644 tests/ModelParser/Model/Moped.php create mode 100644 tests/ModelParser/Model/Vehicle.php diff --git a/src/Builder.php b/src/Builder.php index b6ed6f8..2bcdd39 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -51,6 +51,8 @@ public function build(string $className, array $reducers = []): ClassMetadata } foreach ($classMetadataList as $classMetadata) { + $this->setDiscriminatorClassMetadata($classMetadata, $classMetadataList); + foreach ($classMetadata->getProperties() as $property) { try { $this->setTypeClassMetadata($property->getType(), $classMetadataList); @@ -83,4 +85,19 @@ private function setTypeClassMetadata(PropertyType $type, array $classMetadataLi $this->setTypeClassMetadata($type->getLeafType(), $classMetadataList); } } + + private function setDiscriminatorClassMetadata(ClassMetadata $classMetadata, array $classMetadataList): void + { + if (null === $classMetadata->getDiscriminatorMetadata()) { + return; + } + + $classes = array_values($classMetadata->getDiscriminatorMetadata()->classMap); + $discriminatorMetadataList = []; + foreach ($classes as $class) { + $discriminatorMetadataList[] = $classMetadataList[$class]; + } + + $classMetadata->getDiscriminatorMetadata()->setClassMetadataList($discriminatorMetadataList); + } } diff --git a/src/Metadata/ClassDiscriminatorMetadata.php b/src/Metadata/ClassDiscriminatorMetadata.php new file mode 100644 index 0000000..b922583 --- /dev/null +++ b/src/Metadata/ClassDiscriminatorMetadata.php @@ -0,0 +1,53 @@ +classMetadataList = []; + foreach ($classMetadataList as $classMetadata) { + if (!$classMetadata instanceof ClassMetadata) { + throw new \InvalidArgumentException(sprintf('Expected instance of %s', ClassMetadata::class)); + } + + $this->classMetadataList[$classMetadata->getClassName()] = $classMetadata; + } + } + + public function getClassMetadataList(): array + { + return $this->classMetadataList; + } + + public function getMetadataForClass(string $className): ?ClassMetadata + { + return $this->classMetadataList[$className] ?? null; + } +} diff --git a/src/Metadata/ClassMetadata.php b/src/Metadata/ClassMetadata.php index f4b35e4..1257004 100644 --- a/src/Metadata/ClassMetadata.php +++ b/src/Metadata/ClassMetadata.php @@ -38,12 +38,14 @@ final class ClassMetadata implements \JsonSerializable */ private $constructorParameters = []; + private ?ClassDiscriminatorMetadata $discriminatorMetadata = null; + /** * @param PropertyMetadata[] $properties * @param ParameterMetadata[] $constructorParameters * @param string[] $postDeserializeMethods */ - public function __construct(string $className, array $properties, array $constructorParameters = [], array $postDeserializeMethods = []) + public function __construct(string $className, array $properties, array $constructorParameters = [], array $postDeserializeMethods = [], ?ClassDiscriminatorMetadata $discriminatorMetadata = null) { \assert(array_reduce($constructorParameters, static function (bool $carry, $parameter): bool { return $carry && $parameter instanceof ParameterMetadata; @@ -52,6 +54,7 @@ public function __construct(string $className, array $properties, array $constru $this->className = $className; $this->constructorParameters = $constructorParameters; $this->postDeserializeMethods = $postDeserializeMethods; + $this->discriminatorMetadata = $discriminatorMetadata; foreach ($properties as $property) { $this->addProperty($property); @@ -72,7 +75,8 @@ public static function fromRawClassMetadata(RawClassMetadata $rawClassMetadata, $rawClassMetadata->getClassName(), $properties, $rawClassMetadata->getConstructorParameters(), - $rawClassMetadata->getPostDeserializeMethods() + $rawClassMetadata->getPostDeserializeMethods(), + $rawClassMetadata->getDiscriminatorMetadata() ); } @@ -127,6 +131,11 @@ public function getConstructorParameter(string $name): ParameterMetadata throw new \InvalidArgumentException(\sprintf('Class %s has no constructor parameter called "%s"', $this->className, $name)); } + public function getDiscriminatorMetadata(): ?ClassDiscriminatorMetadata + { + return $this->discriminatorMetadata; + } + /** * Returns a copy of the class metadata with the specified properties removed. * diff --git a/src/ModelParser/JMSParser.php b/src/ModelParser/JMSParser.php index 39c7fb7..05a762f 100644 --- a/src/ModelParser/JMSParser.php +++ b/src/ModelParser/JMSParser.php @@ -8,6 +8,7 @@ use Doctrine\Common\Annotations\Reader; use JMS\Serializer\Annotation\Accessor; use JMS\Serializer\Annotation\AccessorOrder; +use JMS\Serializer\Annotation\Discriminator; use JMS\Serializer\Annotation\Exclude; use JMS\Serializer\Annotation\ExclusionPolicy; use JMS\Serializer\Annotation\Groups; @@ -145,7 +146,8 @@ private function parseClass(\ReflectionClass $reflClass, RawClassMetadata $class } catch (AnnotationException $e) { throw ParseException::classError($reflClass->getName(), $e); } - foreach ($attributes as $attribute) { + foreach ($attributes as $values) { + ['attribute' => $attribute, 'className' => $className] = $values; switch (true) { case $attribute instanceof AccessorOrder: if (self::ACCESS_ORDER_CUSTOM !== $attribute->order) { @@ -178,6 +180,10 @@ private function parseClass(\ReflectionClass $reflClass, RawClassMetadata $class // skip these attributes, we don't do xml break; + case $attribute instanceof Discriminator: + $classMetadata->setDiscriminator($reflClass, $className, $attribute); + break; + default: if (0 === strncmp('JMS\Serializer\\', \get_class($attribute), mb_strlen('JMS\Serializer\\'))) { // if there are attributes we can safely ignore, we need to explicitly ignore them @@ -201,7 +207,10 @@ private function gatherClassAttributes(\ReflectionClass $reflectionClass): array $attributes = $this->annotationOrAttributeReader->getClassAnnotations($reflectionClass); foreach ($attributes as $attribute) { - $map[\get_class($attribute)] = $attribute; + $map[\get_class($attribute)] = [ + 'attribute' => $attribute, + 'className' => $reflectionClass->getName(), + ]; } return $map; diff --git a/src/ModelParser/RawMetadata/RawClassMetadata.php b/src/ModelParser/RawMetadata/RawClassMetadata.php index 8856848..fe89b4f 100644 --- a/src/ModelParser/RawMetadata/RawClassMetadata.php +++ b/src/ModelParser/RawMetadata/RawClassMetadata.php @@ -4,6 +4,8 @@ namespace Liip\MetadataParser\ModelParser\RawMetadata; +use JMS\Serializer\Annotation\Discriminator; +use Liip\MetadataParser\Metadata\ClassDiscriminatorMetadata; use Liip\MetadataParser\Metadata\ParameterMetadata; /** @@ -13,10 +15,7 @@ */ final class RawClassMetadata implements \JsonSerializable { - /** - * @var string - */ - private $className; + private string $className; /** * This list contains the property collections for each property. @@ -39,6 +38,8 @@ final class RawClassMetadata implements \JsonSerializable */ private $constructorParameters = []; + private ?ClassDiscriminatorMetadata $discriminatorMetadata = null; + public function __construct(string $className) { $this->className = $className; @@ -246,6 +247,50 @@ public function getConstructorParameters(): array return $this->constructorParameters; } + public function setDiscriminator(\ReflectionClass $reflClass, string $baseClass, Discriminator $discriminatorAttribute): void + { + $classMap = $discriminatorAttribute->map; + $propertyName = $discriminatorAttribute->field; + if ('' === trim($propertyName)) { + throw new \UnexpectedValueException('The $fieldName cannot be empty.'); + } + + if (0 === \count($discriminatorAttribute->map)) { + throw new \UnexpectedValueException('The discriminator class map cannot be empty.'); + } + + foreach ($classMap as $childClass) { + if (!is_subclass_of($childClass, $baseClass)) { + throw new \UnexpectedValueException(\sprintf('Discriminator class "%s" is not a subclass of "%s".', $childClass, $baseClass)); + } + } + + $this->discriminatorMetadata = new ClassDiscriminatorMetadata(); + + $this->discriminatorMetadata->baseClass = $baseClass; + $this->discriminatorMetadata->propertyName = $propertyName; + $this->discriminatorMetadata->classMap = $classMap; + $this->discriminatorMetadata->groups = $discriminatorAttribute->groups; + $this->discriminatorMetadata->disabled = $discriminatorAttribute->disabled; + + if ($reflClass->isAbstract() || $reflClass->isInterface()) { + return; + } + + if (!\in_array($this->className, $classMap, true)) { + throw new \UnexpectedValueException(\sprintf('The sub-class "%s" is not listed in the discriminator map of the base class %s', $this->className, $baseClass)); + } + + if ($this->hasPropertyCollection($propertyName)) { + throw new \UnexpectedValueException(\sprintf('The discriminator field name "%s" of the base-class "%s" conflicts with a regular property of the sub-class "%s".', $propertyName, $baseClass, $this->className)); + } + } + + public function getDiscriminatorMetadata(): ?ClassDiscriminatorMetadata + { + return $this->discriminatorMetadata; + } + public function jsonSerialize(): array { return array_filter([ diff --git a/src/Parser.php b/src/Parser.php index 9ef3bc6..afd5aee 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -57,6 +57,8 @@ private function parseModel(string $className, ParserContext $context, RawClassM } $registry->add($rawClassMetadata); + $this->parseDiscriminatorClasses($rawClassMetadata, $registry); + foreach ($rawClassMetadata->getPropertyVariations() as $property) { $type = $property->getType(); if ($type instanceof PropertyTypeIterable) { @@ -67,4 +69,15 @@ private function parseModel(string $className, ParserContext $context, RawClassM } } } + + private function parseDiscriminatorClasses(RawClassMetadata $rawClassMetadata, RawClassMetadataRegistry $registry): void + { + if ($rawClassMetadata->getDiscriminatorMetadata() === null) { + return; + } + + foreach ($rawClassMetadata->getDiscriminatorMetadata()->classMap as $childClass) { + $this->parseModel($childClass, new ParserContext($childClass), $registry); + } + } } diff --git a/tests/BuilderTest.php b/tests/BuilderTest.php index 8a0f1f5..a4b7535 100644 --- a/tests/BuilderTest.php +++ b/tests/BuilderTest.php @@ -10,6 +10,7 @@ use Liip\MetadataParser\Metadata\PropertyMetadata; use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyTypeClass; +use Liip\MetadataParser\Metadata\PropertyTypePrimitive; use Liip\MetadataParser\ModelParser\JMSParser; use Liip\MetadataParser\ModelParser\PhpDocParser; use Liip\MetadataParser\ModelParser\ReflectionParser; @@ -17,6 +18,8 @@ use Liip\MetadataParser\RecursionChecker; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Tests\Liip\MetadataParser\ModelParser\Model\Car; +use Tests\Liip\MetadataParser\ModelParser\Model\Moped; use Tests\Liip\MetadataParser\ModelParser\Model\Nested; /** @@ -85,6 +88,24 @@ public function testPropertyWithDifferentSerializedName(): void $this->assertProperty('myProperty', 'myProperty', true, false, $props[0]); } + public function testDiscriminatorClassMetadataList(): void + { + $classMetadata = $this->builder->build(Car::class); + + $props = $classMetadata->getProperties(); + $this->assertCount(1, $props, 'Number of properties should match'); + + $this->assertProperty('carProperty', 'car_property', true, false, $props[0]); + $this->assertPropertyType($props[0]->getType(), PropertyTypePrimitive::class, 'string', false); + + $discriminatorMetadata = $classMetadata->getDiscriminatorMetadata(); + + $this->assertNotNull($discriminatorMetadata); + $this->assertCount(2, $discriminatorMetadata->getClassMetadataList()); + $this->assertNotNull($discriminatorMetadata->getMetadataForClass(Car::class)); + $this->assertNotNull($discriminatorMetadata->getMetadataForClass(Moped::class)); + } + private function assertProperty(string $name, string $serializedName, bool $public, bool $readOnly, PropertyMetadata $property): void { $this->assertSame($name, $property->getName(), 'Name of property should match'); diff --git a/tests/ModelParser/JMSParserTest.php b/tests/ModelParser/JMSParserTest.php index 4028117..73eb5ae 100644 --- a/tests/ModelParser/JMSParserTest.php +++ b/tests/ModelParser/JMSParserTest.php @@ -1483,6 +1483,48 @@ public function foo(): string $this->assertPropertyAccessor('foo', null, $property->getAccessor()); } + public function testDiscriminator(): void + { + $classMetadata = new RawClassMetadata(Car::class); + + $this->parser->parse($classMetadata); + + $this->assertSame(Vehicle::class, $classMetadata->getDiscriminatorMetadata()->baseClass); + $this->assertFalse($classMetadata->getDiscriminatorMetadata()->disabled); + + $this->assertArrayHasKey('moped', $classMetadata->getDiscriminatorMetadata()->classMap); + $this->assertSame(Moped::class, $classMetadata->getDiscriminatorMetadata()->classMap['moped']); + + $this->assertArrayHasKey('car', $classMetadata->getDiscriminatorMetadata()->classMap); + $this->assertSame(Car::class, $classMetadata->getDiscriminatorMetadata()->classMap['car']); + } + + public function testOverriddenDiscriminator(): void + { + $classMetadata = new RawClassMetadata(CabinCruiser::class); + + $this->parser->parse($classMetadata); + + $this->assertSame(Boat::class, $classMetadata->getDiscriminatorMetadata()->baseClass); + $this->assertFalse($classMetadata->getDiscriminatorMetadata()->disabled); + + $this->assertArrayHasKey('cabinCruiser', $classMetadata->getDiscriminatorMetadata()->classMap); + $this->assertSame(CabinCruiser::class, $classMetadata->getDiscriminatorMetadata()->classMap['cabinCruiser']); + + $this->assertArrayHasKey('ferry', $classMetadata->getDiscriminatorMetadata()->classMap); + $this->assertSame(Ferry::class, $classMetadata->getDiscriminatorMetadata()->classMap['ferry']); + } + + public function testExistingDiscriminatorPropertyThrowsException(): void + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('The discriminator field name "type" of the base-class "Tests\Liip\MetadataParser\ModelParser\Model\BaseDiscriminator" conflicts with a regular property of the sub-class "Tests\Liip\MetadataParser\ModelParser\Model\DiscriminatorWithFieldProperty'); + + $classMetadata = new RawClassMetadata(DiscriminatorWithFieldProperty::class); + + $this->parser->parse($classMetadata); + } + protected function assertPropertyCollection(string $serializedName, int $variations, PropertyCollection $prop): void { $this->assertSame($serializedName, $prop->getSerializedName(), 'Serialized name of property should match'); diff --git a/tests/ModelParser/Model/BaseDiscriminator.php b/tests/ModelParser/Model/BaseDiscriminator.php new file mode 100644 index 0000000..959d0fd --- /dev/null +++ b/tests/ModelParser/Model/BaseDiscriminator.php @@ -0,0 +1,14 @@ +parser = new Parser([ new ReflectionParser(), new PhpDocParser(), + new JMSParser(new AnnotationReader()), ]); } @@ -146,6 +153,21 @@ public function testNestedArray(): void $this->assertPropertyType($property->getType(), PropertyTypeUnknown::class, 'mixed', true); } + public function testDiscriminator(): void + { + $classMetadataList = $this->parser->parse(ClassWithVehicleProperty::class); + $classMetadata = $classMetadataList[0]; + + $this->assertSame(Vehicle::class, $classMetadata->getDiscriminatorMetadata()->baseClass); + $this->assertFalse($classMetadata->getDiscriminatorMetadata()->disabled); + + $this->assertArrayHasKey('moped', $classMetadata->getDiscriminatorMetadata()->classMap); + $this->assertSame(Moped::class, $classMetadata->getDiscriminatorMetadata()->classMap['moped']); + + $this->assertArrayHasKey('car', $classMetadata->getDiscriminatorMetadata()->classMap); + $this->assertSame(Car::class, $classMetadata->getDiscriminatorMetadata()->classMap['car']); + } + private function assertPropertyCollection(string $serializedName, int $variations, PropertyCollection $prop): void { $this->assertSame($serializedName, $prop->getSerializedName(), 'Serialized name of property should match'); From 7da13b59c8cc93cc3a81d192fe14b5330bf2ff27 Mon Sep 17 00:00:00 2001 From: Martin Parsiegla Date: Fri, 4 Jul 2025 18:56:28 +0200 Subject: [PATCH 2/7] Remove unused `ParserContext` class This class was only used in the `Parser` class, but even in there nothing was really done with it, so we can remove it. --- src/ModelParser/ParserContext.php | 46 ------------------------- src/Parser.php | 9 +++-- tests/ModelParser/ParserContextTest.php | 37 -------------------- 3 files changed, 4 insertions(+), 88 deletions(-) delete mode 100644 src/ModelParser/ParserContext.php delete mode 100644 tests/ModelParser/ParserContextTest.php diff --git a/src/ModelParser/ParserContext.php b/src/ModelParser/ParserContext.php deleted file mode 100644 index 5fead17..0000000 --- a/src/ModelParser/ParserContext.php +++ /dev/null @@ -1,46 +0,0 @@ -root = $root; - } - - public function __toString(): string - { - if (0 === \count($this->stack)) { - return $this->root; - } - - $stack = array_map(static function (PropertyVariationMetadata $propertyMetadata) { - return $propertyMetadata->getName(); - }, $this->stack); - - return \sprintf('%s->%s', $this->root, implode('->', $stack)); - } - - public function push(PropertyVariationMetadata $property): self - { - $context = clone $this; - $context->stack[] = $property; - - return $context; - } -} diff --git a/src/Parser.php b/src/Parser.php index afd5aee..696290a 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -10,7 +10,6 @@ use Liip\MetadataParser\ModelParser\ModelParserInterface; use Liip\MetadataParser\ModelParser\NamingStrategy\PropertyNamingStrategyInterface; use Liip\MetadataParser\ModelParser\NamingStrategy\SnakeCasePropertyNamingStrategy; -use Liip\MetadataParser\ModelParser\ParserContext; use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata; final class Parser @@ -40,12 +39,12 @@ public function parse(string $className): array { $registry = new RawClassMetadataRegistry(); - $this->parseModel($className, new ParserContext($className), $registry); + $this->parseModel($className, $registry); return $registry->getAll(); } - private function parseModel(string $className, ParserContext $context, RawClassMetadataRegistry $registry): void + private function parseModel(string $className, RawClassMetadataRegistry $registry): void { if ($registry->contains($className)) { return; @@ -65,7 +64,7 @@ private function parseModel(string $className, ParserContext $context, RawClassM $type = $type->getLeafType(); } if ($type instanceof PropertyTypeClass) { - $this->parseModel($type->getClassName(), $context->push($property), $registry); + $this->parseModel($type->getClassName(), $registry); } } } @@ -77,7 +76,7 @@ private function parseDiscriminatorClasses(RawClassMetadata $rawClassMetadata, R } foreach ($rawClassMetadata->getDiscriminatorMetadata()->classMap as $childClass) { - $this->parseModel($childClass, new ParserContext($childClass), $registry); + $this->parseModel($childClass, $registry); } } } diff --git a/tests/ModelParser/ParserContextTest.php b/tests/ModelParser/ParserContextTest.php deleted file mode 100644 index 78574d7..0000000 --- a/tests/ModelParser/ParserContextTest.php +++ /dev/null @@ -1,37 +0,0 @@ -assertStringContainsString('Root', $s); - $this->assertStringNotContainsString('property1', $s); - $this->assertStringNotContainsString('property2', $s); - } - - public function testPush(): void - { - $context = new ParserContext('Root'); - $context = $context->push(new PropertyVariationMetadata('property1', true, true)); - $context = $context->push(new PropertyVariationMetadata('property2', false, true)); - - $s = (string) $context; - $this->assertStringContainsString('Root', $s); - $this->assertStringContainsString('property1', $s); - $this->assertStringContainsString('property2', $s); - } -} From 2079c99cbf02503ad8cf823d0d32eee761e057fc Mon Sep 17 00:00:00 2001 From: Martin Parsiegla Date: Fri, 18 Jul 2025 14:39:02 +0200 Subject: [PATCH 3/7] Add type value to discriminator metadata --- src/Metadata/ClassDiscriminatorMetadata.php | 2 +- src/ModelParser/RawMetadata/RawClassMetadata.php | 5 ++++- tests/ModelParser/Model/ClassWithVehicleProperty.php | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Metadata/ClassDiscriminatorMetadata.php b/src/Metadata/ClassDiscriminatorMetadata.php index b922583..aaef47b 100644 --- a/src/Metadata/ClassDiscriminatorMetadata.php +++ b/src/Metadata/ClassDiscriminatorMetadata.php @@ -34,7 +34,7 @@ public function setClassMetadataList(array $classMetadataList): void $this->classMetadataList = []; foreach ($classMetadataList as $classMetadata) { if (!$classMetadata instanceof ClassMetadata) { - throw new \InvalidArgumentException(sprintf('Expected instance of %s', ClassMetadata::class)); + throw new \InvalidArgumentException(\sprintf('Expected instance of %s', ClassMetadata::class)); } $this->classMetadataList[$classMetadata->getClassName()] = $classMetadata; diff --git a/src/ModelParser/RawMetadata/RawClassMetadata.php b/src/ModelParser/RawMetadata/RawClassMetadata.php index fe89b4f..aa5c95c 100644 --- a/src/ModelParser/RawMetadata/RawClassMetadata.php +++ b/src/ModelParser/RawMetadata/RawClassMetadata.php @@ -277,13 +277,16 @@ public function setDiscriminator(\ReflectionClass $reflClass, string $baseClass, return; } - if (!\in_array($this->className, $classMap, true)) { + $typeValue = array_search($this->className, $classMap, true); + if (false === $typeValue) { throw new \UnexpectedValueException(\sprintf('The sub-class "%s" is not listed in the discriminator map of the base class %s', $this->className, $baseClass)); } if ($this->hasPropertyCollection($propertyName)) { throw new \UnexpectedValueException(\sprintf('The discriminator field name "%s" of the base-class "%s" conflicts with a regular property of the sub-class "%s".', $propertyName, $baseClass, $this->className)); } + + $this->discriminatorMetadata->value = $typeValue; } public function getDiscriminatorMetadata(): ?ClassDiscriminatorMetadata diff --git a/tests/ModelParser/Model/ClassWithVehicleProperty.php b/tests/ModelParser/Model/ClassWithVehicleProperty.php index 53e2681..e0c7523 100644 --- a/tests/ModelParser/Model/ClassWithVehicleProperty.php +++ b/tests/ModelParser/Model/ClassWithVehicleProperty.php @@ -1,8 +1,10 @@ Date: Fri, 7 Nov 2025 16:03:28 +0100 Subject: [PATCH 4/7] Add additional types to primitive types With this change we also allow the following additional primitive types: `false`, `true`, `null`. --- src/Metadata/PropertyTypePrimitive.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Metadata/PropertyTypePrimitive.php b/src/Metadata/PropertyTypePrimitive.php index 9045eea..a3b7702 100644 --- a/src/Metadata/PropertyTypePrimitive.php +++ b/src/Metadata/PropertyTypePrimitive.php @@ -18,6 +18,9 @@ final class PropertyTypePrimitive extends AbstractPropertyType 'int', 'float', 'bool', + 'null', + 'true', + 'false', ]; /** @@ -39,6 +42,10 @@ public function __construct(string $typeName, bool $nullable) public function __toString(): string { + if ('null' === $this->typeName) { + return $this->typeName; + } + return $this->typeName.parent::__toString(); } From eb7c7d9c608eacb3ead9cf0f240c55b28b4b051c Mon Sep 17 00:00:00 2001 From: Martin Parsiegla Date: Fri, 7 Nov 2025 16:05:42 +0100 Subject: [PATCH 5/7] Add support for union types With this change one can now use primitive union types which will be properly (de-)serialized when using the `ReflectionParser`. --- src/Builder.php | 7 + src/Metadata/PropertyTypeUnion.php | 150 ++++++++++++++++++ src/ModelParser/ReflectionParser.php | 46 +++++- src/Parser.php | 28 +++- .../Fixtures/UnionTypeDeclarationModel.php | 3 +- tests/ModelParser/ReflectionParserTest.php | 3 +- 6 files changed, 223 insertions(+), 14 deletions(-) create mode 100644 src/Metadata/PropertyTypeUnion.php diff --git a/src/Builder.php b/src/Builder.php index 2bcdd39..0c423aa 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -9,6 +9,7 @@ use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyTypeClass; use Liip\MetadataParser\Metadata\PropertyTypeIterable; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\Reducer\PropertyReducerInterface; /** @@ -84,6 +85,12 @@ private function setTypeClassMetadata(PropertyType $type, array $classMetadataLi if ($type instanceof PropertyTypeIterable) { $this->setTypeClassMetadata($type->getLeafType(), $classMetadataList); } + + if ($type instanceof PropertyTypeUnion) { + foreach ($type->getTypes() as $type) { + $this->setTypeClassMetadata($type, $classMetadataList); + } + } } private function setDiscriminatorClassMetadata(ClassMetadata $classMetadata, array $classMetadataList): void diff --git a/src/Metadata/PropertyTypeUnion.php b/src/Metadata/PropertyTypeUnion.php new file mode 100644 index 0000000..db400a5 --- /dev/null +++ b/src/Metadata/PropertyTypeUnion.php @@ -0,0 +1,150 @@ + + */ + private array $typeMap = []; + + private ?string $fieldName = null; + + /** + * @var PropertyType[] + */ + private array $types; + + /** + * @param PropertyType[] $types + */ + public function __construct(array $types, bool $nullable) + { + parent::__construct($nullable); + + $this->setTypes($types); + } + + /** + * @return PropertyType[] + */ + public function getTypes(): array + { + return $this->types; + } + + /** + * @param PropertyType[] $types + */ + public function setTypes(array $types): void + { + $this->types = $this->reorderTypes($types); + } + + public function getTypeByClassName(string $className): ?PropertyTypeClass + { + foreach ($this->types as $type) { + if (!$type instanceof PropertyTypeClass) { + continue; + } + + if ($type->getClassName() === $className) { + return $type; + } + } + + return null; + } + + public function setTypeMap(array $typeMap): void + { + $this->typeMap = $typeMap; + } + + public function getTypeMap(): array + { + return $this->typeMap; + } + + public function setFieldName(string $fieldName): void + { + $this->fieldName = $fieldName; + } + + public function getFieldName(): ?string + { + return $this->fieldName; + } + + public function __toString(): string + { + $classTypes = array_map( + static fn (PropertyType $type): string => $type->__toString(), + $this->types + ); + + return implode('|', $classTypes); + } + + public function merge(PropertyType $other): PropertyType + { + if (!$other instanceof self) { + throw new \UnexpectedValueException(\sprintf('Can\'t merge type %s with %s, they must be the same', self::class, \get_class($other))); + } + + $mergedTypes = [...$this->getTypes(), ...$other->getTypes()]; + + $mergedPropertyType = new self($mergedTypes, $this->isNullable() && $other->isNullable()); + + $mergedTypeMap = [...$this->getTypeMap(), ...$other->getTypeMap()]; + $mergedPropertyType->setTypeMap($mergedTypeMap); + + $fieldName = null; + if (null === $other->getFieldName()) { + $fieldName = $this->fieldName; + } elseif (null === $this->getfieldName()) { + $fieldName = $other->getFieldName(); + } elseif ($other->getFieldName() !== $this->getfieldName()) { + throw new \UnexpectedValueException(\sprintf('Can\'t merge type union type with field name %s with other union type with field name %s', $this->getFieldName(), $other->getFieldName())); + } + + $mergedPropertyType->setFieldName($fieldName); + + return $mergedPropertyType; + } + + /** + * Sorting the types by primitives first and then by class will make it easier when (de-)serializing the values. + * + * @param PropertyType[] $types + * + * @return PropertyType[] + */ + private function reorderTypes(array $types): array + { + uasort($types, static function (PropertyType $first, PropertyType $second) { + $order = ['null' => 0, 'array' => 1, 'true' => 2, 'false' => 3, 'bool' => 4, 'int' => 5, 'float' => 6, 'string' => 7]; + $firstTypeName = $first instanceof PropertyTypeIterable ? 'array' : null; + $secondTypeName = $second instanceof PropertyTypeIterable ? 'array' : null; + if ($first instanceof PropertyTypePrimitive) { + $firstTypeName = $first->getTypeName(); + } + + if ($second instanceof PropertyTypePrimitive) { + $secondTypeName = $second->getTypeName(); + } + + $firstOrder = $order[$firstTypeName] ?? self::DEFAULT_ORDER; + $secondOrder = $order[$secondTypeName] ?? self::DEFAULT_ORDER; + + return $firstOrder <=> $secondOrder; + }); + + return array_values($types); + } +} diff --git a/src/ModelParser/ReflectionParser.php b/src/ModelParser/ReflectionParser.php index efa12b6..00f2fb0 100644 --- a/src/ModelParser/ReflectionParser.php +++ b/src/ModelParser/ReflectionParser.php @@ -6,6 +6,8 @@ use Liip\MetadataParser\Exception\ParseException; use Liip\MetadataParser\Metadata\ParameterMetadata; +use Liip\MetadataParser\Metadata\PropertyType; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\ModelParser\NamingStrategy\PropertyNamingStrategyInterface; use Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata; use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata; @@ -13,6 +15,18 @@ final class ReflectionParser implements ModelParserInterface { + private const SUPPORTED_UNION_TYPES = [ + 'int', + 'float', + 'double', + 'bool', + 'true', + 'false', + 'string', + 'null', + 'array', + ]; + /** * @var PhpTypeParser */ @@ -52,11 +66,18 @@ private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $ foreach ($reflClass->getProperties() as $reflProperty) { $type = null; $reflectionType = $this->reflectionSupportsPropertyType ? $reflProperty->getType() : null; - if ($reflectionType instanceof \ReflectionNamedType) { - // If the field has a union type (since PHP 8.0) or intersection type (since PHP 8.1), - // the type would be a different kind of ReflectionType than ReflectionNamedType. - // We don't have support in the metadata model to handle multiple types. - $type = $this->typeParser->parseReflectionType($reflectionType); + switch (true) { + case $reflectionType instanceof \ReflectionNamedType: + $type = $this->typeParser->parseReflectionType($reflectionType); + break; + case $reflectionType instanceof \ReflectionUnionType: + $types = $this->getSupportedUnionTypes($reflectionType); + if (\count($types) > 1) { + $types = array_map(fn (\ReflectionType $namedType): PropertyType => $this->typeParser->parseReflectionType($namedType), $types); + $type = new PropertyTypeUnion($types, $reflectionType->allowsNull()); + } + + break; } if ($classMetadata->hasPropertyVariation($reflProperty->getName())) { $property = $classMetadata->getPropertyVariation($reflProperty->getName()); @@ -86,4 +107,19 @@ private function parseConstructor(\ReflectionClass $reflClass, RawClassMetadata $classMetadata->addConstructorParameter(ParameterMetadata::fromReflection($reflParameter)); } } + + /** + * @return \ReflectionType[] + */ + private function getSupportedUnionTypes(\ReflectionUnionType $reflectionUnionType): array + { + $supportedTypes = []; + foreach ($reflectionUnionType->getTypes() as $type) { + if (\in_array($type->getName(), self::SUPPORTED_UNION_TYPES, true)) { + $supportedTypes[] = $type; + } + } + + return $supportedTypes; + } } diff --git a/src/Parser.php b/src/Parser.php index 696290a..fa85900 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -5,8 +5,10 @@ namespace Liip\MetadataParser; use Liip\MetadataParser\Exception\ParseException; +use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyTypeClass; use Liip\MetadataParser\Metadata\PropertyTypeIterable; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\ModelParser\ModelParserInterface; use Liip\MetadataParser\ModelParser\NamingStrategy\PropertyNamingStrategyInterface; use Liip\MetadataParser\ModelParser\NamingStrategy\SnakeCasePropertyNamingStrategy; @@ -60,18 +62,13 @@ private function parseModel(string $className, RawClassMetadataRegistry $registr foreach ($rawClassMetadata->getPropertyVariations() as $property) { $type = $property->getType(); - if ($type instanceof PropertyTypeIterable) { - $type = $type->getLeafType(); - } - if ($type instanceof PropertyTypeClass) { - $this->parseModel($type->getClassName(), $registry); - } + $this->parsePropertyType($type, $registry); } } private function parseDiscriminatorClasses(RawClassMetadata $rawClassMetadata, RawClassMetadataRegistry $registry): void { - if ($rawClassMetadata->getDiscriminatorMetadata() === null) { + if (null === $rawClassMetadata->getDiscriminatorMetadata()) { return; } @@ -79,4 +76,21 @@ private function parseDiscriminatorClasses(RawClassMetadata $rawClassMetadata, R $this->parseModel($childClass, $registry); } } + + private function parsePropertyType(PropertyType $type, RawClassMetadataRegistry $registry): void + { + if ($type instanceof PropertyTypeIterable) { + $type = $type->getLeafType(); + } + + if ($type instanceof PropertyTypeClass) { + $this->parseModel($type->getClassName(), $registry); + } + + if ($type instanceof PropertyTypeUnion) { + foreach ($type->getTypes() as $subType) { + $this->parsePropertyType($subType, $registry); + } + } + } } diff --git a/tests/ModelParser/Fixtures/UnionTypeDeclarationModel.php b/tests/ModelParser/Fixtures/UnionTypeDeclarationModel.php index 3e30d51..a70bdcb 100644 --- a/tests/ModelParser/Fixtures/UnionTypeDeclarationModel.php +++ b/tests/ModelParser/Fixtures/UnionTypeDeclarationModel.php @@ -10,5 +10,6 @@ class UnionTypeDeclarationModel { protected ReflectionParserTest|ReflectionParser $property1; - public int|string|null $property2; + + public int|string|array|false|null $property2; } diff --git a/tests/ModelParser/ReflectionParserTest.php b/tests/ModelParser/ReflectionParserTest.php index 181e9ef..d4e61ca 100644 --- a/tests/ModelParser/ReflectionParserTest.php +++ b/tests/ModelParser/ReflectionParserTest.php @@ -9,6 +9,7 @@ use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyTypeClass; use Liip\MetadataParser\Metadata\PropertyTypePrimitive; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use Liip\MetadataParser\ModelParser\NamingStrategy\SnakeCasePropertyNamingStrategy; use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection; @@ -134,7 +135,7 @@ public function testTypedPropertiesUnion(): void $this->assertPropertyCollection('property2', 1, $props[1]); $property2 = $props[1]->getVariations()[0]; $this->assertProperty('property2', true, false, $property2); - $this->assertPropertyType($property2->getType(), PropertyTypeUnknown::class, 'mixed', true); + $this->assertPropertyType($property2->getType(), PropertyTypeUnion::class, 'null|array|false|int|string', true); } public function testTypedPropertiesIntersection(): void From c4b839a81cbed82df3e391ea15b3b5e1ae1ea1f9 Mon Sep 17 00:00:00 2001 From: Martin Parsiegla Date: Fri, 7 Nov 2025 16:06:50 +0100 Subject: [PATCH 6/7] Support `UnionDiscriminator` attribute from JMS One can now use this attribute to define more complex union types. --- src/ModelParser/JMSParser.php | 48 +++++++++++++++- src/TypeParser/JMSTypeParser.php | 4 +- tests/BuilderTest.php | 33 +++++++++++ tests/ModelParser/JMSParserTest.php | 56 ++++++++++++++++++- .../Model/ClassUsingUnionDiscriminator.php | 13 +++++ .../Model/ClassUsingUnionTyping.php | 13 +++++ .../ModelParser/Model/DiscriminatorAuthor.php | 17 ++++++ .../Model/DiscriminatorComment.php | 17 ++++++ tests/ParserTest.php | 2 +- 9 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 tests/ModelParser/Model/ClassUsingUnionDiscriminator.php create mode 100644 tests/ModelParser/Model/ClassUsingUnionTyping.php create mode 100644 tests/ModelParser/Model/DiscriminatorAuthor.php create mode 100644 tests/ModelParser/Model/DiscriminatorComment.php diff --git a/src/ModelParser/JMSParser.php b/src/ModelParser/JMSParser.php index 05a762f..831a2ac 100644 --- a/src/ModelParser/JMSParser.php +++ b/src/ModelParser/JMSParser.php @@ -18,6 +18,7 @@ use JMS\Serializer\Annotation\SerializedName; use JMS\Serializer\Annotation\Since; use JMS\Serializer\Annotation\Type; +use JMS\Serializer\Annotation\UnionDiscriminator; use JMS\Serializer\Annotation\Until; use JMS\Serializer\Annotation\VirtualProperty; use JMS\Serializer\Annotation\XmlAttribute; @@ -32,6 +33,8 @@ use Liip\MetadataParser\Exception\ParseException; use Liip\MetadataParser\Metadata\PropertyAccessor; use Liip\MetadataParser\Metadata\PropertyType; +use Liip\MetadataParser\Metadata\PropertyTypePrimitive; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use Liip\MetadataParser\ModelParser\NamingStrategy\PropertyNamingStrategyInterface; use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection; @@ -96,7 +99,7 @@ private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $ } $property = $this->getProperty($classMetadata, $reflProperty, $attributes, $propertyNamingStrategy); - $this->parsePropertyAttributes($classMetadata, $property, $attributes); + $this->parsePropertyAttributes($classMetadata, $reflProperty, $property, $attributes); } } @@ -126,7 +129,7 @@ private function parseMethods(\ReflectionClass $reflClass, RawClassMetadata $cla $property->setType($this->getReturnType($property, $reflMethod, $reflClass)); $property->setAccessor(new PropertyAccessor($reflMethod->getName(), null)); - $this->parsePropertyAttributes($classMetadata, $property, $attributes); + $this->parsePropertyAttributes($classMetadata, $reflMethod, $property, $attributes); } if ($this->isPostDeserializeMethod($attributes)) { @@ -216,7 +219,7 @@ private function gatherClassAttributes(\ReflectionClass $reflectionClass): array return $map; } - private function parsePropertyAttributes(RawClassMetadata $classMetadata, PropertyVariationMetadata $property, array $attributes): void + private function parsePropertyAttributes(RawClassMetadata $classMetadata, \ReflectionProperty|\ReflectionMethod $reflection, PropertyVariationMetadata $property, array $attributes): void { foreach ($attributes as $attribute) { switch (true) { @@ -271,6 +274,32 @@ private function parsePropertyAttributes(RawClassMetadata $classMetadata, Proper case $attribute instanceof MaxDepth: $property->setMaxDepth($attribute->depth); break; + case $attribute instanceof UnionDiscriminator: + $types = []; + $isNullable = $this->isNullable($reflection); + if ($isNullable) { + $types[] = new PropertyTypePrimitive('null', true); + } + + foreach ($attribute->map as $value) { + $types[] = $this->jmsTypeParser->parse($value, true); + } + + $type = new PropertyTypeUnion($types, $isNullable); + $type->setFieldName($attribute->field); + $type->setTypeMap($attribute->map); + + if ($property->getType() instanceof PropertyTypeUnknown) { + $property->setType($type); + } else { + try { + $property->setType($property->getType()->merge($type)); + } catch (\UnexpectedValueException $e) { + throw ParseException::propertyTypeConflict((string) $classMetadata, (string) $property, (string) $property->getType(), (string) $type, $e); + } + } + + break; case $attribute instanceof VirtualProperty: // we handle this separately @@ -408,4 +437,17 @@ private function getMethodName(array $attributes, \ReflectionMethod $reflMethod) return $name; } + + private function isNullable(\ReflectionProperty|\ReflectionMethod $reflection): bool + { + if ($reflection instanceof \ReflectionMethod) { + return false; + } + + if (!$reflection->hasType()) { + return true; + } + + return $reflection->getType()->allowsNull(); + } } diff --git a/src/TypeParser/JMSTypeParser.php b/src/TypeParser/JMSTypeParser.php index 9eef060..fe75249 100644 --- a/src/TypeParser/JMSTypeParser.php +++ b/src/TypeParser/JMSTypeParser.php @@ -34,13 +34,13 @@ public function __construct() $this->jmsTypeParser = new Parser(); } - public function parse(string $rawType): PropertyType + public function parse(string $rawType, bool $isSubType = false): PropertyType { if ('' === $rawType) { return new PropertyTypeUnknown(true); } - return $this->parseType($this->jmsTypeParser->parse($rawType)); + return $this->parseType($this->jmsTypeParser->parse($rawType), $isSubType); } private function parseType(array $typeInfo, bool $isSubType = false): PropertyType diff --git a/tests/BuilderTest.php b/tests/BuilderTest.php index a4b7535..a90e6dd 100644 --- a/tests/BuilderTest.php +++ b/tests/BuilderTest.php @@ -11,6 +11,7 @@ use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyTypeClass; use Liip\MetadataParser\Metadata\PropertyTypePrimitive; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\ModelParser\JMSParser; use Liip\MetadataParser\ModelParser\PhpDocParser; use Liip\MetadataParser\ModelParser\ReflectionParser; @@ -19,6 +20,8 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Tests\Liip\MetadataParser\ModelParser\Model\Car; +use Tests\Liip\MetadataParser\ModelParser\Model\ClassUsingUnionDiscriminator; +use Tests\Liip\MetadataParser\ModelParser\Model\ClassUsingUnionTyping; use Tests\Liip\MetadataParser\ModelParser\Model\Moped; use Tests\Liip\MetadataParser\ModelParser\Model\Nested; @@ -106,6 +109,36 @@ public function testDiscriminatorClassMetadataList(): void $this->assertNotNull($discriminatorMetadata->getMetadataForClass(Moped::class)); } + public function testUnionDiscriminatorClassMetadataList(): void + { + if (\PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Intersection property types are only supported in PHP 8.1 or newer'); + } + + $classMetadata = $this->builder->build(ClassUsingUnionDiscriminator::class); + + $props = $classMetadata->getProperties(); + $this->assertCount(1, $props, 'Number of properties should match'); + + $this->assertProperty('property', 'property', true, false, $props[0]); + $this->assertPropertyType($props[0]->getType(), PropertyTypeUnion::class, 'null|Tests\Liip\MetadataParser\ModelParser\Model\DiscriminatorComment|Tests\Liip\MetadataParser\ModelParser\Model\DiscriminatorAuthor', true); + } + + public function testUnionTypingClassMetadataList(): void + { + if (\PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Intersection property types are only supported in PHP 8.1 or newer'); + } + + $classMetadata = $this->builder->build(ClassUsingUnionTyping::class); + + $props = $classMetadata->getProperties(); + $this->assertCount(1, $props, 'Number of properties should match'); + + $this->assertProperty('property', 'property', true, false, $props[0]); + $this->assertPropertyType($props[0]->getType(), PropertyTypeUnion::class, 'Tests\Liip\MetadataParser\ModelParser\Model\DiscriminatorComment|Tests\Liip\MetadataParser\ModelParser\Model\DiscriminatorAuthor', false); + } + private function assertProperty(string $name, string $serializedName, bool $public, bool $readOnly, PropertyMetadata $property): void { $this->assertSame($name, $property->getName(), 'Name of property should match'); diff --git a/tests/ModelParser/JMSParserTest.php b/tests/ModelParser/JMSParserTest.php index 73eb5ae..f3b4a75 100644 --- a/tests/ModelParser/JMSParserTest.php +++ b/tests/ModelParser/JMSParserTest.php @@ -14,6 +14,7 @@ use Liip\MetadataParser\Metadata\PropertyTypeDateTime; use Liip\MetadataParser\Metadata\PropertyTypeIterable; use Liip\MetadataParser\Metadata\PropertyTypePrimitive; +use Liip\MetadataParser\Metadata\PropertyTypeUnion; use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use Liip\MetadataParser\ModelParser\JMSParser; use Liip\MetadataParser\ModelParser\NamingStrategy\IdenticalPropertyNamingStrategy; @@ -23,7 +24,18 @@ use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata; use PHPUnit\Framework\TestCase; use Tests\Liip\MetadataParser\ModelParser\Model\BaseModel; +use Tests\Liip\MetadataParser\ModelParser\Model\Boat; +use Tests\Liip\MetadataParser\ModelParser\Model\CabinCruiser; +use Tests\Liip\MetadataParser\ModelParser\Model\Car; +use Tests\Liip\MetadataParser\ModelParser\Model\ClassUsingUnionDiscriminator; +use Tests\Liip\MetadataParser\ModelParser\Model\ClassUsingUnionTyping; +use Tests\Liip\MetadataParser\ModelParser\Model\DiscriminatorAuthor; +use Tests\Liip\MetadataParser\ModelParser\Model\DiscriminatorComment; +use Tests\Liip\MetadataParser\ModelParser\Model\DiscriminatorWithFieldProperty; +use Tests\Liip\MetadataParser\ModelParser\Model\Ferry; +use Tests\Liip\MetadataParser\ModelParser\Model\Moped; use Tests\Liip\MetadataParser\ModelParser\Model\Nested; +use Tests\Liip\MetadataParser\ModelParser\Model\Vehicle; /** * @small @@ -1487,7 +1499,7 @@ public function testDiscriminator(): void { $classMetadata = new RawClassMetadata(Car::class); - $this->parser->parse($classMetadata); + $this->parser->parse($classMetadata, new SnakeCasePropertyNamingStrategy()); $this->assertSame(Vehicle::class, $classMetadata->getDiscriminatorMetadata()->baseClass); $this->assertFalse($classMetadata->getDiscriminatorMetadata()->disabled); @@ -1503,7 +1515,7 @@ public function testOverriddenDiscriminator(): void { $classMetadata = new RawClassMetadata(CabinCruiser::class); - $this->parser->parse($classMetadata); + $this->parser->parse($classMetadata, new SnakeCasePropertyNamingStrategy()); $this->assertSame(Boat::class, $classMetadata->getDiscriminatorMetadata()->baseClass); $this->assertFalse($classMetadata->getDiscriminatorMetadata()->disabled); @@ -1522,7 +1534,45 @@ public function testExistingDiscriminatorPropertyThrowsException(): void $classMetadata = new RawClassMetadata(DiscriminatorWithFieldProperty::class); - $this->parser->parse($classMetadata); + $this->parser->parse($classMetadata, new SnakeCasePropertyNamingStrategy()); + } + + public function testUnionDiscriminator(): void + { + if (\PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Intersection property types are only supported in PHP 8.1 or newer'); + } + + $classMetadata = new RawClassMetadata(ClassUsingUnionDiscriminator::class); + + $this->parser->parse($classMetadata, new SnakeCasePropertyNamingStrategy()); + + $props = $classMetadata->getPropertyCollections(); + $this->assertCount(1, $props); + + $this->assertPropertyCollection('property', 1, $props[0]); + + $property = $props[0]->getVariations()[0]; + $this->assertPropertyType(PropertyTypeUnion::class, 'null|'.DiscriminatorComment::class.'|'.DiscriminatorAuthor::class, true, $property->getType()); + } + + public function testUnionDiscriminatorWithTyping(): void + { + if (\PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Intersection property types are only supported in PHP 8.1 or newer'); + } + + $classMetadata = new RawClassMetadata(ClassUsingUnionTyping::class); + + $this->parser->parse($classMetadata, new SnakeCasePropertyNamingStrategy()); + + $props = $classMetadata->getPropertyCollections(); + $this->assertCount(1, $props); + + $this->assertPropertyCollection('property', 1, $props[0]); + + $property = $props[0]->getVariations()[0]; + $this->assertPropertyType(PropertyTypeUnion::class, DiscriminatorComment::class.'|'.DiscriminatorAuthor::class, false, $property->getType()); } protected function assertPropertyCollection(string $serializedName, int $variations, PropertyCollection $prop): void diff --git a/tests/ModelParser/Model/ClassUsingUnionDiscriminator.php b/tests/ModelParser/Model/ClassUsingUnionDiscriminator.php new file mode 100644 index 0000000..8cc4cff --- /dev/null +++ b/tests/ModelParser/Model/ClassUsingUnionDiscriminator.php @@ -0,0 +1,13 @@ + DiscriminatorComment::class, 'author' => DiscriminatorAuthor::class])] + public $property; +} diff --git a/tests/ModelParser/Model/ClassUsingUnionTyping.php b/tests/ModelParser/Model/ClassUsingUnionTyping.php new file mode 100644 index 0000000..b15b8ae --- /dev/null +++ b/tests/ModelParser/Model/ClassUsingUnionTyping.php @@ -0,0 +1,13 @@ + DiscriminatorComment::class, 'author' => DiscriminatorAuthor::class])] + public DiscriminatorComment|DiscriminatorAuthor $property; +} diff --git a/tests/ModelParser/Model/DiscriminatorAuthor.php b/tests/ModelParser/Model/DiscriminatorAuthor.php new file mode 100644 index 0000000..446bfcb --- /dev/null +++ b/tests/ModelParser/Model/DiscriminatorAuthor.php @@ -0,0 +1,17 @@ +parser->parse(ClassWithVehicleProperty::class); - $classMetadata = $classMetadataList[0]; + $classMetadata = $classMetadataList[1]; $this->assertSame(Vehicle::class, $classMetadata->getDiscriminatorMetadata()->baseClass); $this->assertFalse($classMetadata->getDiscriminatorMetadata()->disabled); From 15aa30527424f8f75133ace0da4abf0f8a8bd0c7 Mon Sep 17 00:00:00 2001 From: Martin Parsiegla Date: Mon, 1 Dec 2025 11:13:30 +0100 Subject: [PATCH 7/7] Update changelog for (union) discrminiator support --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5ebac3..817ecdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The library provides two implementations for the property naming strategy: * `IdenticalPropertyNamingStrategy` * `SnakeCasePropertyNamingStrategy` (default). +* Add support for (union) discriminators and their related JMS attributes `#[UnionDiscriminator]` and `#[Discriminator]` # Version 1.x