Skip to content

Commit 44943c8

Browse files
committed
Fixing PHP-DI/PHP-DI #335
1 parent 21dce5e commit 44943c8

File tree

8 files changed

+199
-57
lines changed

8 files changed

+199
-57
lines changed

src/PhpDocReader/PhpDocReader.php

Lines changed: 87 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
namespace PhpDocReader;
44

55
use PhpDocReader\PhpParser\UseStatementParser;
6+
use ReflectionClass;
7+
use ReflectionMethod;
68
use ReflectionParameter;
79
use ReflectionProperty;
10+
use Reflector;
811

912
/**
1013
* PhpDoc reader
@@ -95,35 +98,9 @@ public function getPropertyClass(ReflectionProperty $property)
9598

9699
// If the class name is not fully qualified (i.e. doesn't start with a \)
97100
if ($type[0] !== '\\') {
98-
$alias = (false === $pos = strpos($type, '\\')) ? $type : substr($type, 0, $pos);
99-
$loweredAlias = strtolower($alias);
101+
$resolvedType = $this->tryResolveFqn($type, $class, $property);
100102

101-
// Retrieve "use" statements
102-
$uses = $this->parser->parseUseStatements($property->getDeclaringClass());
103-
104-
$found = false;
105-
106-
if (isset($uses[$loweredAlias])) {
107-
// Imported classes
108-
if (false !== $pos) {
109-
$type = $uses[$loweredAlias] . substr($type, $pos);
110-
} else {
111-
$type = $uses[$loweredAlias];
112-
}
113-
$found = true;
114-
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
115-
$type = $class->getNamespaceName() . '\\' . $type;
116-
$found = true;
117-
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
118-
// Class namespace
119-
$type = $uses['__NAMESPACE__'] . '\\' . $type;
120-
$found = true;
121-
} elseif ($this->classExists($type)) {
122-
// No namespace
123-
$found = true;
124-
}
125-
126-
if (!$found && !$this->ignorePhpDocErrors) {
103+
if (!$resolvedType && !$this->ignorePhpDocErrors) {
127104
throw new AnnotationException(sprintf(
128105
'The @var annotation on %s::%s contains a non existent class "%s". '
129106
. 'Did you maybe forget to add a "use" statement for this annotation?',
@@ -132,6 +109,8 @@ public function getPropertyClass(ReflectionProperty $property)
132109
$type
133110
));
134111
}
112+
113+
$type = $resolvedType;
135114
}
136115

137116
if (!$this->classExists($type) && !$this->ignorePhpDocErrors) {
@@ -203,35 +182,9 @@ public function getParameterClass(ReflectionParameter $parameter)
203182

204183
// If the class name is not fully qualified (i.e. doesn't start with a \)
205184
if ($type[0] !== '\\') {
206-
$alias = (false === $pos = strpos($type, '\\')) ? $type : substr($type, 0, $pos);
207-
$loweredAlias = strtolower($alias);
208-
209-
// Retrieve "use" statements
210-
$uses = $this->parser->parseUseStatements($class);
211-
212-
$found = false;
213-
214-
if (isset($uses[$loweredAlias])) {
215-
// Imported classes
216-
if (false !== $pos) {
217-
$type = $uses[$loweredAlias] . substr($type, $pos);
218-
} else {
219-
$type = $uses[$loweredAlias];
220-
}
221-
$found = true;
222-
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
223-
$type = $class->getNamespaceName() . '\\' . $type;
224-
$found = true;
225-
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
226-
// Class namespace
227-
$type = $uses['__NAMESPACE__'] . '\\' . $type;
228-
$found = true;
229-
} elseif ($this->classExists($type)) {
230-
// No namespace
231-
$found = true;
232-
}
233-
234-
if (!$found && !$this->ignorePhpDocErrors) {
185+
$resolvedType = $this->tryResolveFqn($type, $class, $parameter);
186+
187+
if (!$resolvedType && !$this->ignorePhpDocErrors) {
235188
throw new AnnotationException(sprintf(
236189
'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s". '
237190
. 'Did you maybe forget to add a "use" statement for this annotation?',
@@ -241,6 +194,8 @@ public function getParameterClass(ReflectionParameter $parameter)
241194
$type
242195
));
243196
}
197+
198+
$type = $resolvedType;
244199
}
245200

246201
if (!$this->classExists($type) && !$this->ignorePhpDocErrors) {
@@ -259,6 +214,81 @@ public function getParameterClass(ReflectionParameter $parameter)
259214
return $type;
260215
}
261216

217+
/**
218+
* Attempts to resolve the FQN of the provided $type based on the $class and $member context.
219+
*
220+
* @param string $type
221+
* @param ReflectionClass $class
222+
* @param Reflector $member
223+
*
224+
* @return string|null Fully qualified name of the type, or null if it could not be resolved
225+
*/
226+
private function tryResolveFqn($type, ReflectionClass $class, Reflector $member)
227+
{
228+
$alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos);
229+
$loweredAlias = strtolower($alias);
230+
231+
// Retrieve "use" statements
232+
$uses = $this->parser->parseUseStatements($class);
233+
234+
if (isset($uses[$loweredAlias])) {
235+
// Imported classes
236+
if ($pos !== false) {
237+
return $uses[$loweredAlias] . substr($type, $pos);
238+
} else {
239+
return $uses[$loweredAlias];
240+
}
241+
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
242+
return $class->getNamespaceName() . '\\' . $type;
243+
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
244+
// Class namespace
245+
return $uses['__NAMESPACE__'] . '\\' . $type;
246+
} elseif ($this->classExists($type)) {
247+
// No namespace
248+
return $type;
249+
}
250+
251+
return $this->tryResolveFqnInTraits($type, $class, $member);
252+
}
253+
254+
/**
255+
* Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
256+
* through the traits that are used by the provided $class.
257+
*
258+
* @param string $type
259+
* @param ReflectionClass $class
260+
* @param Reflector $member
261+
*
262+
* @return string|null Fully qualified name of the type, or null if it could not be resolved
263+
*/
264+
private function tryResolveFqnInTraits($type, ReflectionClass $class, Reflector $member)
265+
{
266+
/** @var ReflectionClass[] $traits */
267+
$traits = [];
268+
269+
while ($class) {
270+
$traits = array_merge($traits, $class->getTraits());
271+
$class = $class->getParentClass();
272+
}
273+
274+
foreach ($traits as $trait) {
275+
if ($member instanceof ReflectionProperty && !$trait->hasProperty($member->name)) {
276+
continue;
277+
} elseif ($member instanceof ReflectionMethod && !$trait->hasMethod($member->name)) {
278+
continue;
279+
} elseif ($member instanceof ReflectionParameter && !$trait->hasMethod($member->getDeclaringFunction()->name)) {
280+
continue;
281+
}
282+
283+
$resolvedType = $this->tryResolveFqn($type, $trait, $member);
284+
285+
if ($resolvedType) {
286+
return $resolvedType;
287+
}
288+
}
289+
return null;
290+
}
291+
262292
/**
263293
* @param string $class
264294
* @return bool

tests/FixturesIssue335/Class1.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
class Class1
6+
{
7+
use Trait1;
8+
}

tests/FixturesIssue335/Class2.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
class Class2 extends Class1
6+
{
7+
8+
}

tests/FixturesIssue335/Class3.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
class Class3 extends Class2
6+
{
7+
use Trait2;
8+
}

tests/FixturesIssue335/ClassX.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
class ClassX
6+
{
7+
8+
}

tests/FixturesIssue335/Trait1.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as Foo;
6+
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as MethodFoo;
7+
8+
trait Trait1
9+
{
10+
/**
11+
* @var Foo $propTrait1
12+
*/
13+
protected $propTrait1;
14+
15+
/**
16+
* @param MethodFoo $parameter
17+
*/
18+
public function methodTrait1($parameter)
19+
{
20+
21+
}
22+
}

tests/FixturesIssue335/Trait2.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as Bar;
6+
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as MethodBar;
7+
8+
trait Trait2
9+
{
10+
/**
11+
* @var Bar $propTrait2
12+
*/
13+
protected $propTrait2;
14+
15+
/**
16+
* @param MethodBar $parameter
17+
*/
18+
public function methodTrait2($parameter)
19+
{
20+
21+
}
22+
}

tests/Issue335Test.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader;
4+
5+
use PhpDocReader\PhpDocReader;
6+
use PHPUnit_Framework_TestCase;
7+
use ReflectionClass;
8+
use UnitTest\PhpDocReader\FixturesIssue335\Class3;
9+
10+
/**
11+
* @see https://github.com/PHP-DI/PHP-DI/issues/335
12+
*/
13+
class Issue335Test extends PHPUnit_Framework_TestCase
14+
{
15+
/**
16+
* This test ensures that namespaces are properly resolved for aliases that are defined in traits.
17+
* @see https://github.com/PHP-DI/PHP-DI/issues/335
18+
*/
19+
public function testNamespaceResolutionForTraits()
20+
{
21+
$parser = new PhpDocReader();
22+
23+
$target = new Class3();
24+
25+
$class = new ReflectionClass($target);
26+
27+
$this->assertEquals('UnitTest\PhpDocReader\FixturesIssue335\ClassX', $parser->getPropertyClass($class->getProperty("propTrait1")));
28+
$this->assertEquals('UnitTest\PhpDocReader\FixturesIssue335\ClassX', $parser->getPropertyClass($class->getProperty("propTrait2")));
29+
30+
$params = $class->getMethod("methodTrait1")->getParameters();
31+
$this->assertEquals('UnitTest\PhpDocReader\FixturesIssue335\ClassX', $parser->getParameterClass($params[0]));
32+
33+
$params = $class->getMethod("methodTrait2")->getParameters();
34+
$this->assertEquals('UnitTest\PhpDocReader\FixturesIssue335\ClassX', $parser->getParameterClass($params[0]));
35+
}
36+
}

0 commit comments

Comments
 (0)