|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace yii2\extensions\phpstan\method; |
| 6 | + |
| 7 | +use PHPStan\Analyser\OutOfClassScope; |
| 8 | +use PHPStan\Reflection\{ |
| 9 | + ClassReflection, |
| 10 | + MethodReflection, |
| 11 | + MethodsClassReflectionExtension, |
| 12 | + ReflectionProvider, |
| 13 | +}; |
| 14 | +use yii\base\Component; |
| 15 | +use yii2\extensions\phpstan\ServiceMap; |
| 16 | + |
| 17 | +/** |
| 18 | + * Provides method reflection for Yii Behavior in PHPStan analysis. |
| 19 | + * |
| 20 | + * Integrates Yii Behavior with PHPStan method reflection extension, enabling detection and resolution of methods |
| 21 | + * provided by attached behaviors on {@see Component} subclasses during static analysis. |
| 22 | + * |
| 23 | + * This extension inspects the behaviors attached to a given class and determines if any of them provide the requested |
| 24 | + * method, allowing PHPStan to recognize available methods as if they were natively declared. |
| 25 | + * |
| 26 | + * Key features. |
| 27 | + * - Delegates to the native {@see Component} method if not found in behaviors. |
| 28 | + * - Detects methods provided by behaviors attached to {@see Component} subclasses. |
| 29 | + * - Ensures compatibility with PHPStan strict static analysis and autocompletion. |
| 30 | + * - Integrates with {@see ServiceMap} for efficient behavior lookup by class name. |
| 31 | + * |
| 32 | + * @see Component for Yii Component class. |
| 33 | + * @see MethodsClassReflectionExtension for custom methods class reflection extension contract. |
| 34 | + * |
| 35 | + * @copyright Copyright (C) 2023 Terabytesoftw. |
| 36 | + * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License. |
| 37 | + */ |
| 38 | +final class BehaviorMethodsClassReflectionExtension implements MethodsClassReflectionExtension |
| 39 | +{ |
| 40 | + /** |
| 41 | + * Creates a new instance of the {@see BehaviorMethodsClassReflectionExtension} class. |
| 42 | + * |
| 43 | + * @param ReflectionProvider $reflectionProvider Reflection provider for class and property lookups. |
| 44 | + * @param ServiceMap $serviceMap Service map for resolving component classes by ID. |
| 45 | + */ |
| 46 | + public function __construct( |
| 47 | + private readonly ReflectionProvider $reflectionProvider, |
| 48 | + private readonly ServiceMap $serviceMap, |
| 49 | + ) {} |
| 50 | + |
| 51 | + /** |
| 52 | + * Retrieves the method reflection for a given method name, including those provided by attached behaviors. |
| 53 | + * |
| 54 | + * Resolves the {@see MethodReflection} for the specified method name on the given class, searching first among |
| 55 | + * methods provided by behaviors attached to the class. If the method is not found in any behavior, it delegates |
| 56 | + * to the native {@see Component} method resolution. |
| 57 | + * |
| 58 | + * This enables PHPStan to recognize available methods from behaviors as if they were natively declared on the |
| 59 | + * component class, supporting accurate static analysis and autocompletion. |
| 60 | + * |
| 61 | + * @param ClassReflection $classReflection Reflection of the class being analyzed. |
| 62 | + * @param string $methodName Name of the method to resolve. |
| 63 | + * |
| 64 | + * @return MethodReflection Reflection instance for the resolved method. |
| 65 | + */ |
| 66 | + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection |
| 67 | + { |
| 68 | + $behaviorMethod = $this->findMethodInBehaviors($classReflection, $methodName); |
| 69 | + |
| 70 | + assert($behaviorMethod !== null); |
| 71 | + |
| 72 | + return $behaviorMethod; |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Determines whether the specified method exists on the given class, including methods provided by attached |
| 77 | + * behaviors. |
| 78 | + * |
| 79 | + * Checks if the class is a subclass of {@see Component} and doesn't already declare the method natively. If so, |
| 80 | + * inspect all behaviors attached to the class to determine if any provide the requested method. |
| 81 | + * |
| 82 | + * This enables PHPStan to recognize available methods from behaviors as if they were natively declared on the |
| 83 | + * component class, supporting accurate static analysis and autocompletion. |
| 84 | + * |
| 85 | + * @param ClassReflection $classReflection Reflection of the class being analyzed. |
| 86 | + * @param string $methodName Name of the method to check for existence. |
| 87 | + * |
| 88 | + * @return bool `true` if the method exists on the class via an attached behavior; `false` otherwise. |
| 89 | + */ |
| 90 | + public function hasMethod(ClassReflection $classReflection, string $methodName): bool |
| 91 | + { |
| 92 | + if ($classReflection->isSubclassOfClass($this->reflectionProvider->getClass(Component::class)) === false) { |
| 93 | + return false; |
| 94 | + } |
| 95 | + |
| 96 | + if ($classReflection->hasNativeMethod($methodName)) { |
| 97 | + return false; |
| 98 | + } |
| 99 | + |
| 100 | + return $this->findMethodInBehaviors($classReflection, $methodName) !== null; |
| 101 | + } |
| 102 | + |
| 103 | + /** |
| 104 | + * Searches for a method provided by behaviors attached to the specified class. |
| 105 | + * |
| 106 | + * Iterates over all behaviors attached to the given class and checks if any of them declare the requested method. |
| 107 | + * |
| 108 | + * This enables method resolution for behaviors in PHPStan static analysis, allowing detection of methods that |
| 109 | + * aren't natively declared on the component class but are available via attached behaviors. |
| 110 | + * |
| 111 | + * @param ClassReflection $classReflection Reflection of the class being analyzed. |
| 112 | + * @param string $methodName Name of the method to search for in attached behaviors. |
| 113 | + * |
| 114 | + * @return MethodReflection|null Reflection instance for the resolved method if found in a behavior; {@see null} |
| 115 | + * otherwise. |
| 116 | + */ |
| 117 | + private function findMethodInBehaviors(ClassReflection $classReflection, string $methodName): MethodReflection|null |
| 118 | + { |
| 119 | + $behaviors = $this->serviceMap->getBehaviorsByClassName($classReflection->getName()); |
| 120 | + |
| 121 | + foreach ($behaviors as $behaviorClass) { |
| 122 | + if ($this->reflectionProvider->hasClass($behaviorClass)) { |
| 123 | + $behaviorReflection = $this->reflectionProvider->getClass($behaviorClass); |
| 124 | + |
| 125 | + if ($behaviorReflection->hasMethod($methodName)) { |
| 126 | + return $behaviorReflection->getMethod($methodName, new OutOfClassScope()); |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + return null; |
| 132 | + } |
| 133 | +} |
0 commit comments