Skip to content

Commit 41520e7

Browse files
Better merging of collection types (#42)
1 parent ef441a2 commit 41520e7

19 files changed

+376
-87
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
# Version 1.x
44

5+
# 1.3.0 (unreleased)
6+
7+
* Added `PropertyTypeIterable`, which generalizes `PropertyTypeArray` to allow merging Collection informations like one would with arrays, including between interfaces and concrete classes
8+
* Deprecated `PropertyTypeArray`, please prefer using `PropertyTypeIterable` instead
9+
510
# 1.2.0 (unreleased)
611

712
* Added a model parser `VisibilityAwarePropertyAccessGuesser` that tries to guess getter and setter methods for non-public properties.

src/Builder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
use Liip\MetadataParser\Exception\ParseException;
88
use Liip\MetadataParser\Metadata\ClassMetadata;
99
use Liip\MetadataParser\Metadata\PropertyType;
10-
use Liip\MetadataParser\Metadata\PropertyTypeArray;
1110
use Liip\MetadataParser\Metadata\PropertyTypeClass;
11+
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
1212
use Liip\MetadataParser\Reducer\PropertyReducerInterface;
1313

1414
/**
@@ -85,7 +85,7 @@ private function setTypeClassMetadata(PropertyType $type, array $classMetadataLi
8585
return;
8686
}
8787

88-
if ($type instanceof PropertyTypeArray) {
88+
if ($type instanceof PropertyTypeIterable) {
8989
$this->setTypeClassMetadata($type->getLeafType(), $classMetadataList);
9090
}
9191
}

src/Metadata/PropertyTypeArray.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,25 @@
66

77
use Doctrine\Common\Collections\Collection;
88

9-
final class PropertyTypeArray extends AbstractPropertyType
9+
/**
10+
* @deprecated Please use {@see PropertyTypeIterable} instead
11+
*/
12+
class PropertyTypeArray extends AbstractPropertyType
1013
{
1114
/**
1215
* @var PropertyType
1316
*/
14-
private $subType;
17+
protected $subType;
1518

1619
/**
1720
* @var bool
1821
*/
19-
private $hashmap;
22+
protected $hashmap;
2023

2124
/**
2225
* @var bool
2326
*/
24-
private $isCollection;
27+
protected $isCollection;
2528

2629
public function __construct(PropertyType $subType, bool $hashmap, bool $nullable, bool $isCollection = false)
2730
{
@@ -65,6 +68,11 @@ public function isCollection(): bool
6568
return $this->isCollection;
6669
}
6770

71+
public function getCollectionClass(): ?string
72+
{
73+
return $this->isCollection() ? Collection::class : null;
74+
}
75+
6876
/**
6977
* Goes down the type until it is not an array or hashmap anymore.
7078
*/
@@ -85,6 +93,9 @@ public function merge(PropertyType $other): PropertyType
8593
if ($other instanceof PropertyTypeUnknown) {
8694
return new self($this->subType, $this->isHashmap(), $nullable);
8795
}
96+
if ($this->isCollection() && (($other instanceof PropertyTypeClass) && is_a($other->getClassName(), Collection::class, true))) {
97+
return new self($this->getSubType(), $this->isHashmap(), $nullable, true);
98+
}
8899
if (!$other instanceof self) {
89100
throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be the same or unknown', self::class, \get_class($other)));
90101
}

src/Metadata/PropertyTypeClass.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Liip\MetadataParser\Metadata;
66

7+
use Doctrine\Common\Collections\Collection;
78
use Liip\MetadataParser\Exception\InvalidTypeException;
89

910
final class PropertyTypeClass extends AbstractPropertyType
@@ -67,6 +68,9 @@ public function merge(PropertyType $other): PropertyType
6768
if ($other instanceof PropertyTypeUnknown) {
6869
return new self($this->className, $nullable);
6970
}
71+
if (is_a($this->getClassName(), Collection::class, true) && (($other instanceof PropertyTypeIterable) && $other->isCollection())) {
72+
return $other->merge($this);
73+
}
7074
if (!$other instanceof self) {
7175
throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be the same or unknown', self::class, \get_class($other)));
7276
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Liip\MetadataParser\Metadata;
6+
7+
use Doctrine\Common\Collections\Collection;
8+
9+
/**
10+
* This property type can be merged with PropertyTypeClass<T>, provided that T is, inherits from, or is a parent class of $this->collectionClass
11+
* This property type can be merged with PropertyTypeIterable, if :
12+
* - we're not merging a plain array PropertyTypeIterable into a hashmap one,
13+
* - and the collection classes of each are either not present on both sides, or are the same, or parent-child of one another
14+
*/
15+
final class PropertyTypeIterable extends PropertyTypeArray
16+
{
17+
/**
18+
* @var string
19+
*/
20+
private $collectionClass;
21+
22+
/**
23+
* @param class-string<\Traversable>|null $collectionClass
24+
*/
25+
public function __construct(PropertyType $subType, bool $hashmap, bool $nullable, string $collectionClass = null)
26+
{
27+
parent::__construct($subType, $hashmap, $nullable, null != $collectionClass);
28+
29+
$this->collectionClass = $collectionClass;
30+
}
31+
32+
public function __toString(): string
33+
{
34+
if ($this->subType instanceof PropertyTypeUnknown) {
35+
return 'array'.($this->isCollection() ? '|\\'.$this->collectionClass : '');
36+
}
37+
38+
$array = $this->isHashmap() ? '[string]' : '[]';
39+
if ($this->isCollection()) {
40+
$collectionType = $this->isHashmap() ? ', string' : '';
41+
$array .= sprintf('|\\%s<%s%s>', $this->collectionClass, $this->subType, $collectionType);
42+
}
43+
44+
return ((string) $this->subType).$array.AbstractPropertyType::__toString();
45+
}
46+
47+
public function getCollectionClass(): ?string
48+
{
49+
return $this->collectionClass;
50+
}
51+
52+
public function isCollection(): bool
53+
{
54+
return null != $this->getCollectionClass();
55+
}
56+
57+
public function merge(PropertyType $other): PropertyType
58+
{
59+
$nullable = $this->isNullable() && $other->isNullable();
60+
61+
if ($other instanceof PropertyTypeUnknown) {
62+
return new self($this->subType, $this->isHashmap(), $nullable, $this->getCollectionClass());
63+
}
64+
if ($this->isCollection() && (($other instanceof PropertyTypeClass) && is_a($other->getClassName(), Collection::class, true))) {
65+
return new self($this->getSubType(), $this->isHashmap(), $nullable, $this->findCommonCollectionClass($this->getCollectionClass(), $other->getClassName()));
66+
}
67+
if (!$other instanceof parent) {
68+
throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, they must be the same or unknown', self::class, \get_class($other)));
69+
}
70+
71+
/*
72+
* We allow converting array to hashmap (but not the other way round).
73+
*
74+
* PHPDoc has no clear definition for hashmaps with string indexes, but JMS Serializer annotations do.
75+
*/
76+
if ($this->isHashmap() && !$other->isHashmap()) {
77+
throw new \UnexpectedValueException(sprintf('Can\'t merge type %s with %s, can\'t change hashmap into plain array', self::class, \get_class($other)));
78+
}
79+
80+
if ($other->isCollection()) {
81+
$otherCollectionClass = ($other instanceof self) ? $other->getCollectionClass() : Collection::class;
82+
} else {
83+
$otherCollectionClass = null;
84+
}
85+
$hashmap = $this->isHashmap() || $other->isHashmap();
86+
$commonClass = $this->findCommonCollectionClass($this->getCollectionClass(), $otherCollectionClass);
87+
88+
if ($other->getSubType() instanceof PropertyTypeUnknown) {
89+
return new self($this->getSubType(), $hashmap, $nullable, $commonClass);
90+
}
91+
if ($this->getSubType() instanceof PropertyTypeUnknown) {
92+
return new self($other->getSubType(), $hashmap, $nullable, $commonClass);
93+
}
94+
95+
return new self($this->getSubType()->merge($other->getSubType()), $hashmap, $nullable, $commonClass);
96+
}
97+
98+
/**
99+
* Find the most derived class that doesn't deny both class hints, meaning the most derived
100+
* between left and right if one is a child of the other
101+
*/
102+
protected function findCommonCollectionClass(?string $left, ?string $right): ?string
103+
{
104+
if (null === $right) {
105+
return $left;
106+
}
107+
if (null === $left) {
108+
return $right;
109+
}
110+
111+
if (is_a($left, $right, true)) {
112+
return $left;
113+
}
114+
if (is_a($right, $left, true)) {
115+
return $right;
116+
}
117+
118+
throw new \UnexpectedValueException("Collection classes '{$left}' and '{$right}' do not match.");
119+
}
120+
}

src/Parser.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace Liip\MetadataParser;
66

77
use Liip\MetadataParser\Exception\ParseException;
8-
use Liip\MetadataParser\Metadata\PropertyTypeArray;
98
use Liip\MetadataParser\Metadata\PropertyTypeClass;
9+
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
1010
use Liip\MetadataParser\ModelParser\ModelParserInterface;
1111
use Liip\MetadataParser\ModelParser\ParserContext;
1212
use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata;
@@ -54,7 +54,7 @@ private function parseModel(string $className, ParserContext $context, RawClassM
5454

5555
foreach ($rawClassMetadata->getPropertyVariations() as $property) {
5656
$type = $property->getType();
57-
if ($type instanceof PropertyTypeArray) {
57+
if ($type instanceof PropertyTypeIterable) {
5858
$type = $type->getLeafType();
5959
}
6060
if ($type instanceof PropertyTypeClass) {

src/RecursionChecker.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
use Liip\MetadataParser\Exception\RecursionException;
88
use Liip\MetadataParser\Metadata\ClassMetadata;
9-
use Liip\MetadataParser\Metadata\PropertyTypeArray;
109
use Liip\MetadataParser\Metadata\PropertyTypeClass;
10+
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
1111
use Psr\Log\LoggerInterface;
1212

1313
/**
@@ -65,7 +65,7 @@ private function checkClassMetadata(ClassMetadata $classMetadata, RecursionConte
6565

6666
foreach ($classMetadata->getProperties() as $property) {
6767
$type = $property->getType();
68-
if ($type instanceof PropertyTypeArray) {
68+
if ($type instanceof PropertyTypeIterable) {
6969
$type = $type->getLeafType();
7070
}
7171

src/RecursionContext.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace Liip\MetadataParser;
66

77
use Liip\MetadataParser\Metadata\PropertyMetadata;
8-
use Liip\MetadataParser\Metadata\PropertyTypeArray;
98
use Liip\MetadataParser\Metadata\PropertyTypeClass;
9+
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
1010

1111
final class RecursionContext
1212
{
@@ -93,7 +93,7 @@ public function countClassNames(string $className): int
9393
}
9494
foreach ($this->stack as $property) {
9595
$type = $property->getType();
96-
if ($type instanceof PropertyTypeArray) {
96+
if ($type instanceof PropertyTypeIterable) {
9797
$type = $type->getLeafType();
9898
}
9999
if ($type instanceof PropertyTypeClass && $type->getClassName() === $className) {

src/TypeParser/JMSTypeParser.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
namespace Liip\MetadataParser\TypeParser;
66

7+
use Doctrine\Common\Collections\ArrayCollection;
78
use Doctrine\Common\Collections\Collection;
89
use JMS\Serializer\Type\Parser;
910
use Liip\MetadataParser\Exception\InvalidTypeException;
1011
use Liip\MetadataParser\Metadata\DateTimeOptions;
1112
use Liip\MetadataParser\Metadata\PropertyType;
12-
use Liip\MetadataParser\Metadata\PropertyTypeArray;
1313
use Liip\MetadataParser\Metadata\PropertyTypeClass;
1414
use Liip\MetadataParser\Metadata\PropertyTypeDateTime;
15+
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
1516
use Liip\MetadataParser\Metadata\PropertyTypePrimitive;
1617
use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
1718

@@ -54,7 +55,7 @@ private function parseType(array $typeInfo, bool $isSubType = false): PropertyTy
5455

5556
if (0 === \count($typeInfo['params'])) {
5657
if (self::TYPE_ARRAY === $typeInfo['name']) {
57-
return new PropertyTypeArray(new PropertyTypeUnknown(false), false, $nullable);
58+
return new PropertyTypeIterable(new PropertyTypeUnknown(false), false, $nullable);
5859
}
5960

6061
if (PropertyTypePrimitive::isTypePrimitive($typeInfo['name'])) {
@@ -67,13 +68,13 @@ private function parseType(array $typeInfo, bool $isSubType = false): PropertyTy
6768
return new PropertyTypeClass($typeInfo['name'], $nullable);
6869
}
6970

70-
$isCollection = $this->isCollection($typeInfo['name']);
71-
if (self::TYPE_ARRAY === $typeInfo['name'] || $isCollection) {
71+
$collectionClass = $this->getCollectionClass($typeInfo['name']);
72+
if (self::TYPE_ARRAY === $typeInfo['name'] || $collectionClass) {
7273
if (1 === \count($typeInfo['params'])) {
73-
return new PropertyTypeArray($this->parseType($typeInfo['params'][0], true), false, $nullable, $isCollection);
74+
return new PropertyTypeIterable($this->parseType($typeInfo['params'][0], true), false, $nullable, $collectionClass);
7475
}
7576
if (2 === \count($typeInfo['params'])) {
76-
return new PropertyTypeArray($this->parseType($typeInfo['params'][1], true), true, $nullable, $isCollection);
77+
return new PropertyTypeIterable($this->parseType($typeInfo['params'][1], true), true, $nullable, $collectionClass);
7778
}
7879

7980
throw new InvalidTypeException(sprintf('JMS property type array can\'t have more than 2 parameters (%s)', var_export($typeInfo, true)));
@@ -95,8 +96,13 @@ private function parseType(array $typeInfo, bool $isSubType = false): PropertyTy
9596
throw new InvalidTypeException(sprintf('Unknown JMS property found (%s)', var_export($typeInfo, true)));
9697
}
9798

98-
private function isCollection(string $name): bool
99+
private function getCollectionClass(string $name): ?string
99100
{
100-
return self::TYPE_ARRAY_COLLECTION === $name || is_a($name, Collection::class, true);
101+
switch ($name) {
102+
case self::TYPE_ARRAY_COLLECTION:
103+
return ArrayCollection::class;
104+
default:
105+
return is_a($name, Collection::class, true) ? $name : null;
106+
}
101107
}
102108
}

src/TypeParser/PhpTypeParser.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
use Doctrine\Common\Collections\Collection;
99
use Liip\MetadataParser\Exception\InvalidTypeException;
1010
use Liip\MetadataParser\Metadata\PropertyType;
11-
use Liip\MetadataParser\Metadata\PropertyTypeArray;
1211
use Liip\MetadataParser\Metadata\PropertyTypeClass;
1312
use Liip\MetadataParser\Metadata\PropertyTypeDateTime;
13+
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
1414
use Liip\MetadataParser\Metadata\PropertyTypePrimitive;
1515
use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
1616

@@ -57,12 +57,12 @@ public function parseAnnotationType(string $rawType, \ReflectionClass $declaring
5757
}
5858
}
5959

60-
$isCollection = false;
60+
$collectionClass = null;
6161
$filteredTypes = [];
6262
foreach ($types as $type) {
6363
$resolvedClass = $this->resolveClass($type, $declaringClass);
6464
if (is_a($resolvedClass, Collection::class, true)) {
65-
$isCollection = true;
65+
$collectionClass = $resolvedClass;
6666
} else {
6767
$filteredTypes[] = $type;
6868
}
@@ -75,7 +75,7 @@ public function parseAnnotationType(string $rawType, \ReflectionClass $declaring
7575
throw new InvalidTypeException(sprintf('Multiple types are not supported (%s)', $rawType));
7676
}
7777

78-
return $this->createType($filteredTypes[0], $nullable, $declaringClass, $isCollection);
78+
return $this->createType($filteredTypes[0], $nullable, $declaringClass, $collectionClass);
7979
}
8080

8181
/**
@@ -90,21 +90,21 @@ public function parseReflectionType(\ReflectionType $reflType): PropertyType
9090
throw new InvalidTypeException(sprintf('No type information found, got %s but expected %s', \ReflectionType::class, \ReflectionNamedType::class));
9191
}
9292

93-
private function createType(string $rawType, bool $nullable, \ReflectionClass $reflClass = null, bool $isCollection = false): PropertyType
93+
private function createType(string $rawType, bool $nullable, \ReflectionClass $reflClass = null, string $collectionClass = null): PropertyType
9494
{
9595
if (self::TYPE_ARRAY === $rawType) {
96-
return new PropertyTypeArray(new PropertyTypeUnknown(false), false, $nullable);
96+
return new PropertyTypeIterable(new PropertyTypeUnknown(false), false, $nullable);
9797
}
9898

9999
if (self::TYPE_ARRAY_SUFFIX === substr($rawType, -\strlen(self::TYPE_ARRAY_SUFFIX))) {
100100
$rawSubType = substr($rawType, 0, \strlen($rawType) - \strlen(self::TYPE_ARRAY_SUFFIX));
101101

102-
return new PropertyTypeArray($this->createType($rawSubType, false, $reflClass), false, $nullable, $isCollection);
102+
return new PropertyTypeIterable($this->createType($rawSubType, false, $reflClass), false, $nullable, $collectionClass);
103103
}
104104
if (self::TYPE_HASHMAP_SUFFIX === substr($rawType, -\strlen(self::TYPE_HASHMAP_SUFFIX))) {
105105
$rawSubType = substr($rawType, 0, \strlen($rawType) - \strlen(self::TYPE_HASHMAP_SUFFIX));
106106

107-
return new PropertyTypeArray($this->createType($rawSubType, false, $reflClass), true, $nullable, $isCollection);
107+
return new PropertyTypeIterable($this->createType($rawSubType, false, $reflClass), true, $nullable, $collectionClass);
108108
}
109109

110110
if (self::TYPE_RESOURCE === $rawType) {

0 commit comments

Comments
 (0)