33namespace PhpDocReader ;
44
55use PhpDocReader \PhpParser \UseStatementParser ;
6+ use ReflectionClass ;
7+ use ReflectionMethod ;
68use ReflectionParameter ;
79use ReflectionProperty ;
10+ use Reflector ;
811
912/**
1013 * PhpDoc reader
@@ -95,35 +98,10 @@ 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 );
100-
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- }
101+ // Try to resolve the FQN using the class context
102+ $ resolvedType = $ this ->tryResolveFqn ($ type , $ class , $ property );
125103
126- if (!$ found && !$ this ->ignorePhpDocErrors ) {
104+ if (!$ resolvedType && !$ this ->ignorePhpDocErrors ) {
127105 throw new AnnotationException (sprintf (
128106 'The @var annotation on %s::%s contains a non existent class "%s". '
129107 . 'Did you maybe forget to add a "use" statement for this annotation? ' ,
@@ -132,6 +110,8 @@ public function getPropertyClass(ReflectionProperty $property)
132110 $ type
133111 ));
134112 }
113+
114+ $ type = $ resolvedType ;
135115 }
136116
137117 if (!$ this ->classExists ($ type ) && !$ this ->ignorePhpDocErrors ) {
@@ -203,35 +183,10 @@ public function getParameterClass(ReflectionParameter $parameter)
203183
204184 // If the class name is not fully qualified (i.e. doesn't start with a \)
205185 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 ) {
186+ // Try to resolve the FQN using the class context
187+ $ resolvedType = $ this ->tryResolveFqn ($ type , $ class , $ parameter );
188+
189+ if (!$ resolvedType && !$ this ->ignorePhpDocErrors ) {
235190 throw new AnnotationException (sprintf (
236191 'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s". '
237192 . 'Did you maybe forget to add a "use" statement for this annotation? ' ,
@@ -241,6 +196,8 @@ public function getParameterClass(ReflectionParameter $parameter)
241196 $ type
242197 ));
243198 }
199+
200+ $ type = $ resolvedType ;
244201 }
245202
246203 if (!$ this ->classExists ($ type ) && !$ this ->ignorePhpDocErrors ) {
@@ -259,6 +216,89 @@ public function getParameterClass(ReflectionParameter $parameter)
259216 return $ type ;
260217 }
261218
219+ /**
220+ * Attempts to resolve the FQN of the provided $type based on the $class and $member context.
221+ *
222+ * @param string $type
223+ * @param ReflectionClass $class
224+ * @param Reflector $member
225+ *
226+ * @return string|null Fully qualified name of the type, or null if it could not be resolved
227+ */
228+ private function tryResolveFqn ($ type , ReflectionClass $ class , Reflector $ member )
229+ {
230+ $ alias = ($ pos = strpos ($ type , '\\' )) === false ? $ type : substr ($ type , 0 , $ pos );
231+ $ loweredAlias = strtolower ($ alias );
232+
233+ // Retrieve "use" statements
234+ $ uses = $ this ->parser ->parseUseStatements ($ class );
235+
236+ if (isset ($ uses [$ loweredAlias ])) {
237+ // Imported classes
238+ if ($ pos !== false ) {
239+ return $ uses [$ loweredAlias ] . substr ($ type , $ pos );
240+ } else {
241+ return $ uses [$ loweredAlias ];
242+ }
243+ } elseif ($ this ->classExists ($ class ->getNamespaceName () . '\\' . $ type )) {
244+ return $ class ->getNamespaceName () . '\\' . $ type ;
245+ } elseif (isset ($ uses ['__NAMESPACE__ ' ]) && $ this ->classExists ($ uses ['__NAMESPACE__ ' ] . '\\' . $ type )) {
246+ // Class namespace
247+ return $ uses ['__NAMESPACE__ ' ] . '\\' . $ type ;
248+ } elseif ($ this ->classExists ($ type )) {
249+ // No namespace
250+ return $ type ;
251+ }
252+
253+ if (version_compare (phpversion (), '5.4.0 ' , '< ' )) {
254+ return null ;
255+ } else {
256+ // If all fail, try resolving through related traits
257+ return $ this ->tryResolveFqnInTraits ($ type , $ class , $ member );
258+ }
259+ }
260+
261+ /**
262+ * Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
263+ * through the traits that are used by the provided $class.
264+ *
265+ * @param string $type
266+ * @param ReflectionClass $class
267+ * @param Reflector $member
268+ *
269+ * @return string|null Fully qualified name of the type, or null if it could not be resolved
270+ */
271+ private function tryResolveFqnInTraits ($ type , ReflectionClass $ class , Reflector $ member )
272+ {
273+ /** @var ReflectionClass[] $traits */
274+ $ traits = array ();
275+
276+ // Get traits for the class and its parents
277+ while ($ class ) {
278+ $ traits = array_merge ($ traits , $ class ->getTraits ());
279+ $ class = $ class ->getParentClass ();
280+ }
281+
282+ foreach ($ traits as $ trait ) {
283+ // Eliminate traits that don't have the property/method/parameter
284+ if ($ member instanceof ReflectionProperty && !$ trait ->hasProperty ($ member ->name )) {
285+ continue ;
286+ } elseif ($ member instanceof ReflectionMethod && !$ trait ->hasMethod ($ member ->name )) {
287+ continue ;
288+ } elseif ($ member instanceof ReflectionParameter && !$ trait ->hasMethod ($ member ->getDeclaringFunction ()->name )) {
289+ continue ;
290+ }
291+
292+ // Run the resolver again with the ReflectionClass instance for the trait
293+ $ resolvedType = $ this ->tryResolveFqn ($ type , $ trait , $ member );
294+
295+ if ($ resolvedType ) {
296+ return $ resolvedType ;
297+ }
298+ }
299+ return null ;
300+ }
301+
262302 /**
263303 * @param string $class
264304 * @return bool
0 commit comments