Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -51,6 +52,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);
Expand Down Expand Up @@ -82,5 +85,26 @@ 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
{
if (null === $classMetadata->getDiscriminatorMetadata()) {
return;
}

$classes = array_values($classMetadata->getDiscriminatorMetadata()->classMap);
$discriminatorMetadataList = [];
foreach ($classes as $class) {
$discriminatorMetadataList[] = $classMetadataList[$class];
}

$classMetadata->getDiscriminatorMetadata()->setClassMetadataList($discriminatorMetadataList);
}
}
53 changes: 53 additions & 0 deletions src/Metadata/ClassDiscriminatorMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Liip\MetadataParser\Metadata;

class ClassDiscriminatorMetadata
{
public string $baseClass;
public string $propertyName;
public string $value;
public bool $disabled = false;

/**
* @var string[]
*/
public array $classMap = [];

/**
* @var string[]
*/
public array $groups = [];

/**
* @var ClassMetadata[]
*/
private array $classMetadataList = [];

/**
* @param ClassMetadata[] $classMetadataList
*/
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));
}

$this->classMetadataList[$classMetadata->getClassName()] = $classMetadata;
}
}

public function getClassMetadataList(): array
{
return $this->classMetadataList;
}

public function getMetadataForClass(string $className): ?ClassMetadata
{
return $this->classMetadataList[$className] ?? null;
}
}
13 changes: 11 additions & 2 deletions src/Metadata/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -72,7 +75,8 @@ public static function fromRawClassMetadata(RawClassMetadata $rawClassMetadata,
$rawClassMetadata->getClassName(),
$properties,
$rawClassMetadata->getConstructorParameters(),
$rawClassMetadata->getPostDeserializeMethods()
$rawClassMetadata->getPostDeserializeMethods(),
$rawClassMetadata->getDiscriminatorMetadata()
);
}

Expand Down Expand Up @@ -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.
*
Expand Down
7 changes: 7 additions & 0 deletions src/Metadata/PropertyTypePrimitive.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ final class PropertyTypePrimitive extends AbstractPropertyType
'int',
'float',
'bool',
'null',
'true',
'false',
];

/**
Expand All @@ -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();
}

Expand Down
150 changes: 150 additions & 0 deletions src/Metadata/PropertyTypeUnion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

declare(strict_types=1);

namespace Liip\MetadataParser\Metadata;

final class PropertyTypeUnion extends AbstractPropertyType
{
private const DEFAULT_ORDER = 8;

/**
* @var array<string, string>
*/
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);
}
}
Loading