Skip to content

Commit 7da7f2b

Browse files
authored
Merge pull request #22 from liip/php-type-reflection-parser
handle type declarations in reflection parser
2 parents a1f4712 + db99d90 commit 7da7f2b

File tree

10 files changed

+184
-15
lines changed

10 files changed

+184
-15
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 0.4.0
2+
3+
* Handle property type declarations in reflection parser.
4+
* [Bugfix] Upgrade array type with `@var Type[]` annotation
5+
* [Bugfix] When extending class redefines a property, use phpdoc from extending class rather than base class
6+
* [Bugfix] Use correct context for relative class names in inherited properties/methods
7+
18
# 0.3.0
29

310
* Support PHP 8, drop support for PHP 7.1

src/Metadata/AbstractPropertyMetadata.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public function hasCustomInformation(string $key): bool
108108
*
109109
* @return mixed The information in whatever format it has been set
110110
*/
111-
public function getCustomInformation(string $key): mixed
111+
public function getCustomInformation(string $key)
112112
{
113113
if (!\array_key_exists($key, $this->customInformation)) {
114114
throw new \InvalidArgumentException(sprintf('Property %s has no custom information %s', $this->name, $key));

src/ModelParser/JMSParser.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ private function getReturnType(PropertyVariationMetadata $property, \ReflectionM
324324
}
325325

326326
try {
327-
$docBlockType = $this->getReturnTypeOfMethod($reflMethod, $reflClass);
327+
$docBlockType = $this->getReturnTypeOfMethod($reflMethod);
328328
} catch (InvalidTypeException $e) {
329329
throw ParseException::propertyTypeError($reflClass->getName(), (string) $property, $e);
330330
}
@@ -340,7 +340,7 @@ private function getReturnType(PropertyVariationMetadata $property, \ReflectionM
340340
}
341341
}
342342

343-
private function getReturnTypeOfMethod(\ReflectionMethod $reflMethod, \ReflectionClass $reflClass): ?PropertyType
343+
private function getReturnTypeOfMethod(\ReflectionMethod $reflMethod): ?PropertyType
344344
{
345345
$docComment = $reflMethod->getDocComment();
346346
if (false === $docComment) {
@@ -349,7 +349,7 @@ private function getReturnTypeOfMethod(\ReflectionMethod $reflMethod, \Reflectio
349349

350350
foreach (explode("\n", $docComment) as $line) {
351351
if (1 === preg_match('/@return ([^ ]+)/', $line, $matches)) {
352-
return $this->phpTypeParser->parseAnnotationType($matches[1], $reflClass);
352+
return $this->phpTypeParser->parseAnnotationType($matches[1], $reflMethod->getDeclaringClass());
353353
}
354354
}
355355

src/ModelParser/PhpDocParser.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Liip\MetadataParser\Exception\ParseException;
88
use Liip\MetadataParser\Metadata\PropertyType;
99
use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
10+
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection;
1011
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata;
1112
use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata;
1213
use Liip\MetadataParser\TypeParser\PhpTypeParser;
@@ -34,11 +35,21 @@ public function parse(RawClassMetadata $classMetadata): void
3435
$this->parseProperties($reflClass, $classMetadata);
3536
}
3637

37-
private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
38+
/**
39+
* @return string[] the property names that have been added
40+
*/
41+
private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): array
3842
{
43+
$existingProperties = array_map(static function (PropertyCollection $prop): string {
44+
return (string) $prop;
45+
}, $classMetadata->getPropertyCollections());
46+
47+
$addedProperties = [];
48+
$parentProperties = [];
3949
if ($reflParentClass = $reflClass->getParentClass()) {
40-
$this->parseProperties($reflParentClass, $classMetadata);
50+
$parentProperties = $this->parseProperties($reflParentClass, $classMetadata);
4151
}
52+
$parentPropertiesLookup = array_flip($parentProperties);
4253

4354
foreach ($reflClass->getProperties() as $reflProperty) {
4455
if ($classMetadata->hasPropertyVariation($reflProperty->getName())) {
@@ -49,24 +60,37 @@ private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $
4960
}
5061

5162
$docComment = $reflProperty->getDocComment();
52-
if (false !== $docComment && $property->getType() instanceof PropertyTypeUnknown) {
63+
if (false !== $docComment) {
5364
try {
54-
$type = $this->getPropertyTypeFromDocComment($docComment, $reflClass);
65+
$type = $this->getPropertyTypeFromDocComment($docComment, $reflProperty);
5566
} catch (ParseException $e) {
5667
throw ParseException::propertyTypeError((string) $classMetadata, (string) $property, $e);
5768
}
58-
if (null !== $type) {
69+
if (null === $type) {
70+
continue;
71+
}
72+
73+
if ($property->getType() instanceof PropertyTypeUnknown || \array_key_exists((string) $property, $parentPropertiesLookup)) {
5974
$property->setType($type);
75+
} else {
76+
try {
77+
$property->setType($property->getType()->merge($type));
78+
} catch (\UnexpectedValueException $e) {
79+
throw ParseException::propertyTypeConflict((string) $classMetadata, (string) $property, (string) $property->getType(), (string) $type, $e);
80+
}
6081
}
82+
$addedProperties[] = (string) $property;
6183
}
6284
}
85+
86+
return array_values(array_diff(array_unique(array_merge($parentProperties, $addedProperties)), $existingProperties));
6387
}
6488

65-
private function getPropertyTypeFromDocComment(string $docComment, \ReflectionClass $reflClass): ?PropertyType
89+
private function getPropertyTypeFromDocComment(string $docComment, \ReflectionProperty $reflProperty): ?PropertyType
6690
{
6791
foreach (explode("\n", $docComment) as $line) {
6892
if (1 === preg_match('/@var ([^ ]+)/', $line, $matches)) {
69-
return $this->typeParser->parseAnnotationType($matches[1], $reflClass);
93+
return $this->typeParser->parseAnnotationType($matches[1], $reflProperty->getDeclaringClass());
7094
}
7195
}
7296

src/ModelParser/ReflectionParser.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,28 @@
88
use Liip\MetadataParser\Metadata\ParameterMetadata;
99
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata;
1010
use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata;
11+
use Liip\MetadataParser\TypeParser\PhpTypeParser;
1112

1213
final class ReflectionParser implements ModelParserInterface
1314
{
15+
/**
16+
* @var PhpTypeParser
17+
*/
18+
private $typeParser;
19+
20+
/**
21+
* Whether the PHP reflections support property type declarations.
22+
*
23+
* @var bool
24+
*/
25+
private $reflectionSupportsPropertyType;
26+
27+
public function __construct()
28+
{
29+
$this->typeParser = new PhpTypeParser();
30+
$this->reflectionSupportsPropertyType = version_compare(PHP_VERSION, '7.4', '>=');
31+
}
32+
1433
public function parse(RawClassMetadata $classMetadata): void
1534
{
1635
try {
@@ -30,11 +49,26 @@ private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $
3049
}
3150

3251
foreach ($reflClass->getProperties() as $reflProperty) {
52+
$type = null;
53+
$reflectionType = $this->reflectionSupportsPropertyType ? $reflProperty->getType() : null;
54+
if ($reflectionType instanceof \ReflectionNamedType) {
55+
// If the field has a union type (since PHP 8.0) or intersection type (since PHP 8.1),
56+
// the type would be a different kind of ReflectionType than ReflectionNamedType.
57+
// We don't have support in the metadata model to handle multiple types.
58+
$type = $this->typeParser->parseReflectionType($reflectionType);
59+
}
3360
if ($classMetadata->hasPropertyVariation($reflProperty->getName())) {
3461
$property = $classMetadata->getPropertyVariation($reflProperty->getName());
3562
$property->setPublic($reflProperty->isPublic());
63+
if ($type) {
64+
$property->setType($type);
65+
}
3666
} else {
37-
$classMetadata->addPropertyVariation($reflProperty->getName(), PropertyVariationMetadata::fromReflection($reflProperty));
67+
$property = PropertyVariationMetadata::fromReflection($reflProperty);
68+
if ($type) {
69+
$property->setType($type);
70+
}
71+
$classMetadata->addPropertyVariation($reflProperty->getName(), $property);
3872
}
3973
}
4074
}

src/TypeParser/PhpTypeParser.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function __construct()
4040
/**
4141
* @throws InvalidTypeException if an invalid type or multiple types were defined
4242
*/
43-
public function parseAnnotationType(string $rawType, \ReflectionClass $reflClass): PropertyType
43+
public function parseAnnotationType(string $rawType, \ReflectionClass $declaringClass): PropertyType
4444
{
4545
if ('' === $rawType) {
4646
return new PropertyTypeUnknown(true);
@@ -63,7 +63,7 @@ public function parseAnnotationType(string $rawType, \ReflectionClass $reflClass
6363
throw new InvalidTypeException(sprintf('Multiple types are not supported (%s)', $rawType));
6464
}
6565

66-
return $this->createType($types[0], $nullable, $reflClass);
66+
return $this->createType($types[0], $nullable, $declaringClass);
6767
}
6868

6969
/**
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Tests\Liip\MetadataParser\ModelParser\Fixtures;
4+
5+
use Tests\Liip\MetadataParser\ModelParser\ReflectionParserTest;
6+
7+
class TypeDeclarationModel
8+
{
9+
private string $property1;
10+
public ?int $property2;
11+
protected ReflectionParserTest $property3;
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Tests\Liip\MetadataParser\ModelParser\Fixtures;
4+
5+
use Liip\MetadataParser\ModelParser\ReflectionParser;
6+
use Tests\Liip\MetadataParser\ModelParser\ReflectionParserTest;
7+
8+
class UnionTypeDeclarationModel
9+
{
10+
protected ReflectionParserTest|ReflectionParser $property1;
11+
public int|string|null $property2;
12+
}

tests/ModelParser/PhpDocParserTest.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Liip\MetadataParser\Exception\InvalidTypeException;
88
use Liip\MetadataParser\Exception\ParseException;
99
use Liip\MetadataParser\Metadata\PropertyType;
10+
use Liip\MetadataParser\Metadata\PropertyTypeArray;
1011
use Liip\MetadataParser\Metadata\PropertyTypeClass;
1112
use Liip\MetadataParser\Metadata\PropertyTypePrimitive;
1213
use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
@@ -176,6 +177,30 @@ public function testPrefilledClassMetadata(): void
176177
$this->assertPropertyType(PropertyTypePrimitive::class, 'int', false, $property->getType());
177178
}
178179

180+
public function testUpgradeArrayOfUnknown(): void
181+
{
182+
$c = new class() {
183+
/**
184+
* @var string[]
185+
*/
186+
private $property;
187+
};
188+
189+
$classMetadata = new RawClassMetadata(\get_class($c));
190+
$propertyMetadata = new PropertyVariationMetadata('property', false, true);
191+
$propertyMetadata->setType(new PropertyTypeArray(new PropertyTypeUnknown(false), false, false));
192+
$classMetadata->addPropertyVariation('property', $propertyMetadata);
193+
$this->parser->parse($classMetadata);
194+
195+
$props = $classMetadata->getPropertyCollections();
196+
$this->assertCount(1, $props, 'Number of properties should match');
197+
198+
$this->assertPropertyCollection('property', 1, $props[0]);
199+
$property = $props[0]->getVariations()[0];
200+
$this->assertProperty('property', true, false, $property);
201+
$this->assertPropertyType(PropertyTypeArray::class, 'string[]', false, $property->getType());
202+
}
203+
179204
public function testInheritedProperty(): void
180205
{
181206
$c = new class() extends BaseModel {
@@ -204,7 +229,7 @@ public function testInheritedProperty(): void
204229
$this->assertPropertyCollection('parent_property2', 1, $props[1]);
205230
$property = $props[1]->getVariations()[0];
206231
$this->assertProperty('parentProperty2', false, false, $property);
207-
$this->assertPropertyType(PropertyTypePrimitive::class, 'bool', false, $property->getType());
232+
$this->assertPropertyType(PropertyTypePrimitive::class, 'string', false, $property->getType());
208233

209234
$this->assertPropertyCollection('property1', 1, $props[2]);
210235
$property = $props[2]->getVariations()[0];

tests/ModelParser/ReflectionParserTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
use Liip\MetadataParser\Exception\ParseException;
88
use Liip\MetadataParser\Metadata\ParameterMetadata;
99
use Liip\MetadataParser\Metadata\PropertyType;
10+
use Liip\MetadataParser\Metadata\PropertyTypeClass;
11+
use Liip\MetadataParser\Metadata\PropertyTypePrimitive;
1012
use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
1113
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection;
1214
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata;
1315
use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata;
1416
use Liip\MetadataParser\ModelParser\ReflectionParser;
1517
use PHPUnit\Framework\TestCase;
18+
use Tests\Liip\MetadataParser\ModelParser\Fixtures\TypeDeclarationModel;
19+
use Tests\Liip\MetadataParser\ModelParser\Fixtures\UnionTypeDeclarationModel;
1620
use Tests\Liip\MetadataParser\ModelParser\Model\ReflectionBaseModel;
1721

1822
/**
@@ -80,6 +84,57 @@ public function testProperties(): void
8084
$this->assertPropertyType($property3->getType(), PropertyTypeUnknown::class, 'mixed', true);
8185
}
8286

87+
public function testTypedProperties(): void
88+
{
89+
if (version_compare(PHP_VERSION, '7.4.0', '<')) {
90+
$this->markTestSkipped('Primitive property types are only supported in PHP 7.4 or newer');
91+
}
92+
93+
$rawClassMetadata = new RawClassMetadata(TypeDeclarationModel::class);
94+
$this->parser->parse($rawClassMetadata);
95+
96+
$props = $rawClassMetadata->getPropertyCollections();
97+
$this->assertCount(3, $props, 'Number of class metadata properties should match');
98+
99+
$this->assertPropertyCollection('property1', 1, $props[0]);
100+
$property1 = $props[0]->getVariations()[0];
101+
$this->assertProperty('property1', false, false, $property1);
102+
$this->assertPropertyType($property1->getType(), PropertyTypePrimitive::class, 'string', false);
103+
104+
$this->assertPropertyCollection('property2', 1, $props[1]);
105+
$property2 = $props[1]->getVariations()[0];
106+
$this->assertProperty('property2', true, false, $property2);
107+
$this->assertPropertyType($property2->getType(), PropertyTypePrimitive::class, 'int|null', true);
108+
109+
$this->assertPropertyCollection('property3', 1, $props[2]);
110+
$property3 = $props[2]->getVariations()[0];
111+
$this->assertProperty('property3', false, false, $property3);
112+
$this->assertPropertyType($property3->getType(), PropertyTypeClass::class, ReflectionParserTest::class, false);
113+
}
114+
115+
public function testTypedPropertiesUnion(): void
116+
{
117+
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
118+
$this->markTestSkipped('Union property types are only supported in PHP 8.0 or newer');
119+
}
120+
121+
$rawClassMetadata = new RawClassMetadata(UnionTypeDeclarationModel::class);
122+
$this->parser->parse($rawClassMetadata);
123+
124+
$props = $rawClassMetadata->getPropertyCollections();
125+
$this->assertCount(2, $props, 'Number of class metadata properties should match');
126+
127+
$this->assertPropertyCollection('property1', 1, $props[0]);
128+
$property1 = $props[0]->getVariations()[0];
129+
$this->assertProperty('property1', false, false, $property1);
130+
$this->assertPropertyType($property1->getType(), PropertyTypeUnknown::class, 'mixed', true);
131+
132+
$this->assertPropertyCollection('property2', 1, $props[1]);
133+
$property2 = $props[1]->getVariations()[0];
134+
$this->assertProperty('property2', true, false, $property2);
135+
$this->assertPropertyType($property2->getType(), PropertyTypeUnknown::class, 'mixed', true);
136+
}
137+
83138
public function testPrefilledClassMetadata(): void
84139
{
85140
$c = new class() {

0 commit comments

Comments
 (0)