Skip to content

Commit 2501a62

Browse files
authored
Merge pull request #54 from rebuy-de/suppport-discriminator
Add discriminator support
2 parents 8d6866c + 15aa305 commit 2501a62

31 files changed

+769
-111
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
The library provides two implementations for the property naming strategy:
1818
* `IdenticalPropertyNamingStrategy`
1919
* `SnakeCasePropertyNamingStrategy` (default).
20+
* Add support for (union) discriminators and their related JMS attributes `#[UnionDiscriminator]` and `#[Discriminator]`
2021

2122
# Version 1.x
2223

src/Builder.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Liip\MetadataParser\Metadata\PropertyType;
1010
use Liip\MetadataParser\Metadata\PropertyTypeClass;
1111
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
12+
use Liip\MetadataParser\Metadata\PropertyTypeUnion;
1213
use Liip\MetadataParser\Reducer\PropertyReducerInterface;
1314

1415
/**
@@ -51,6 +52,8 @@ public function build(string $className, array $reducers = []): ClassMetadata
5152
}
5253

5354
foreach ($classMetadataList as $classMetadata) {
55+
$this->setDiscriminatorClassMetadata($classMetadata, $classMetadataList);
56+
5457
foreach ($classMetadata->getProperties() as $property) {
5558
try {
5659
$this->setTypeClassMetadata($property->getType(), $classMetadataList);
@@ -82,5 +85,26 @@ private function setTypeClassMetadata(PropertyType $type, array $classMetadataLi
8285
if ($type instanceof PropertyTypeIterable) {
8386
$this->setTypeClassMetadata($type->getLeafType(), $classMetadataList);
8487
}
88+
89+
if ($type instanceof PropertyTypeUnion) {
90+
foreach ($type->getTypes() as $type) {
91+
$this->setTypeClassMetadata($type, $classMetadataList);
92+
}
93+
}
94+
}
95+
96+
private function setDiscriminatorClassMetadata(ClassMetadata $classMetadata, array $classMetadataList): void
97+
{
98+
if (null === $classMetadata->getDiscriminatorMetadata()) {
99+
return;
100+
}
101+
102+
$classes = array_values($classMetadata->getDiscriminatorMetadata()->classMap);
103+
$discriminatorMetadataList = [];
104+
foreach ($classes as $class) {
105+
$discriminatorMetadataList[] = $classMetadataList[$class];
106+
}
107+
108+
$classMetadata->getDiscriminatorMetadata()->setClassMetadataList($discriminatorMetadataList);
85109
}
86110
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Liip\MetadataParser\Metadata;
6+
7+
class ClassDiscriminatorMetadata
8+
{
9+
public string $baseClass;
10+
public string $propertyName;
11+
public string $value;
12+
public bool $disabled = false;
13+
14+
/**
15+
* @var string[]
16+
*/
17+
public array $classMap = [];
18+
19+
/**
20+
* @var string[]
21+
*/
22+
public array $groups = [];
23+
24+
/**
25+
* @var ClassMetadata[]
26+
*/
27+
private array $classMetadataList = [];
28+
29+
/**
30+
* @param ClassMetadata[] $classMetadataList
31+
*/
32+
public function setClassMetadataList(array $classMetadataList): void
33+
{
34+
$this->classMetadataList = [];
35+
foreach ($classMetadataList as $classMetadata) {
36+
if (!$classMetadata instanceof ClassMetadata) {
37+
throw new \InvalidArgumentException(\sprintf('Expected instance of %s', ClassMetadata::class));
38+
}
39+
40+
$this->classMetadataList[$classMetadata->getClassName()] = $classMetadata;
41+
}
42+
}
43+
44+
public function getClassMetadataList(): array
45+
{
46+
return $this->classMetadataList;
47+
}
48+
49+
public function getMetadataForClass(string $className): ?ClassMetadata
50+
{
51+
return $this->classMetadataList[$className] ?? null;
52+
}
53+
}

src/Metadata/ClassMetadata.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ final class ClassMetadata implements \JsonSerializable
3838
*/
3939
private $constructorParameters = [];
4040

41+
private ?ClassDiscriminatorMetadata $discriminatorMetadata = null;
42+
4143
/**
4244
* @param PropertyMetadata[] $properties
4345
* @param ParameterMetadata[] $constructorParameters
4446
* @param string[] $postDeserializeMethods
4547
*/
46-
public function __construct(string $className, array $properties, array $constructorParameters = [], array $postDeserializeMethods = [])
48+
public function __construct(string $className, array $properties, array $constructorParameters = [], array $postDeserializeMethods = [], ?ClassDiscriminatorMetadata $discriminatorMetadata = null)
4749
{
4850
\assert(array_reduce($constructorParameters, static function (bool $carry, $parameter): bool {
4951
return $carry && $parameter instanceof ParameterMetadata;
@@ -52,6 +54,7 @@ public function __construct(string $className, array $properties, array $constru
5254
$this->className = $className;
5355
$this->constructorParameters = $constructorParameters;
5456
$this->postDeserializeMethods = $postDeserializeMethods;
57+
$this->discriminatorMetadata = $discriminatorMetadata;
5558

5659
foreach ($properties as $property) {
5760
$this->addProperty($property);
@@ -72,7 +75,8 @@ public static function fromRawClassMetadata(RawClassMetadata $rawClassMetadata,
7275
$rawClassMetadata->getClassName(),
7376
$properties,
7477
$rawClassMetadata->getConstructorParameters(),
75-
$rawClassMetadata->getPostDeserializeMethods()
78+
$rawClassMetadata->getPostDeserializeMethods(),
79+
$rawClassMetadata->getDiscriminatorMetadata()
7680
);
7781
}
7882

@@ -127,6 +131,11 @@ public function getConstructorParameter(string $name): ParameterMetadata
127131
throw new \InvalidArgumentException(\sprintf('Class %s has no constructor parameter called "%s"', $this->className, $name));
128132
}
129133

134+
public function getDiscriminatorMetadata(): ?ClassDiscriminatorMetadata
135+
{
136+
return $this->discriminatorMetadata;
137+
}
138+
130139
/**
131140
* Returns a copy of the class metadata with the specified properties removed.
132141
*

src/Metadata/PropertyTypePrimitive.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ final class PropertyTypePrimitive extends AbstractPropertyType
1818
'int',
1919
'float',
2020
'bool',
21+
'null',
22+
'true',
23+
'false',
2124
];
2225

2326
/**
@@ -39,6 +42,10 @@ public function __construct(string $typeName, bool $nullable)
3942

4043
public function __toString(): string
4144
{
45+
if ('null' === $this->typeName) {
46+
return $this->typeName;
47+
}
48+
4249
return $this->typeName.parent::__toString();
4350
}
4451

src/Metadata/PropertyTypeUnion.php

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Liip\MetadataParser\Metadata;
6+
7+
final class PropertyTypeUnion extends AbstractPropertyType
8+
{
9+
private const DEFAULT_ORDER = 8;
10+
11+
/**
12+
* @var array<string, string>
13+
*/
14+
private array $typeMap = [];
15+
16+
private ?string $fieldName = null;
17+
18+
/**
19+
* @var PropertyType[]
20+
*/
21+
private array $types;
22+
23+
/**
24+
* @param PropertyType[] $types
25+
*/
26+
public function __construct(array $types, bool $nullable)
27+
{
28+
parent::__construct($nullable);
29+
30+
$this->setTypes($types);
31+
}
32+
33+
/**
34+
* @return PropertyType[]
35+
*/
36+
public function getTypes(): array
37+
{
38+
return $this->types;
39+
}
40+
41+
/**
42+
* @param PropertyType[] $types
43+
*/
44+
public function setTypes(array $types): void
45+
{
46+
$this->types = $this->reorderTypes($types);
47+
}
48+
49+
public function getTypeByClassName(string $className): ?PropertyTypeClass
50+
{
51+
foreach ($this->types as $type) {
52+
if (!$type instanceof PropertyTypeClass) {
53+
continue;
54+
}
55+
56+
if ($type->getClassName() === $className) {
57+
return $type;
58+
}
59+
}
60+
61+
return null;
62+
}
63+
64+
public function setTypeMap(array $typeMap): void
65+
{
66+
$this->typeMap = $typeMap;
67+
}
68+
69+
public function getTypeMap(): array
70+
{
71+
return $this->typeMap;
72+
}
73+
74+
public function setFieldName(string $fieldName): void
75+
{
76+
$this->fieldName = $fieldName;
77+
}
78+
79+
public function getFieldName(): ?string
80+
{
81+
return $this->fieldName;
82+
}
83+
84+
public function __toString(): string
85+
{
86+
$classTypes = array_map(
87+
static fn (PropertyType $type): string => $type->__toString(),
88+
$this->types
89+
);
90+
91+
return implode('|', $classTypes);
92+
}
93+
94+
public function merge(PropertyType $other): PropertyType
95+
{
96+
if (!$other instanceof self) {
97+
throw new \UnexpectedValueException(\sprintf('Can\'t merge type %s with %s, they must be the same', self::class, \get_class($other)));
98+
}
99+
100+
$mergedTypes = [...$this->getTypes(), ...$other->getTypes()];
101+
102+
$mergedPropertyType = new self($mergedTypes, $this->isNullable() && $other->isNullable());
103+
104+
$mergedTypeMap = [...$this->getTypeMap(), ...$other->getTypeMap()];
105+
$mergedPropertyType->setTypeMap($mergedTypeMap);
106+
107+
$fieldName = null;
108+
if (null === $other->getFieldName()) {
109+
$fieldName = $this->fieldName;
110+
} elseif (null === $this->getfieldName()) {
111+
$fieldName = $other->getFieldName();
112+
} elseif ($other->getFieldName() !== $this->getfieldName()) {
113+
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()));
114+
}
115+
116+
$mergedPropertyType->setFieldName($fieldName);
117+
118+
return $mergedPropertyType;
119+
}
120+
121+
/**
122+
* Sorting the types by primitives first and then by class will make it easier when (de-)serializing the values.
123+
*
124+
* @param PropertyType[] $types
125+
*
126+
* @return PropertyType[]
127+
*/
128+
private function reorderTypes(array $types): array
129+
{
130+
uasort($types, static function (PropertyType $first, PropertyType $second) {
131+
$order = ['null' => 0, 'array' => 1, 'true' => 2, 'false' => 3, 'bool' => 4, 'int' => 5, 'float' => 6, 'string' => 7];
132+
$firstTypeName = $first instanceof PropertyTypeIterable ? 'array' : null;
133+
$secondTypeName = $second instanceof PropertyTypeIterable ? 'array' : null;
134+
if ($first instanceof PropertyTypePrimitive) {
135+
$firstTypeName = $first->getTypeName();
136+
}
137+
138+
if ($second instanceof PropertyTypePrimitive) {
139+
$secondTypeName = $second->getTypeName();
140+
}
141+
142+
$firstOrder = $order[$firstTypeName] ?? self::DEFAULT_ORDER;
143+
$secondOrder = $order[$secondTypeName] ?? self::DEFAULT_ORDER;
144+
145+
return $firstOrder <=> $secondOrder;
146+
});
147+
148+
return array_values($types);
149+
}
150+
}

0 commit comments

Comments
 (0)