55namespace yii2 \extensions \phpstan ;
66
77use Closure ;
8- use InvalidArgumentException ;
98use PhpParser \Node ;
109use ReflectionException ;
1110use ReflectionFunction ;
1211use ReflectionNamedType ;
1312use RuntimeException ;
14- use yii \base \BaseObject ;
13+ use yii \base \{ BaseObject , InvalidArgumentException } ;
1514
1615use function class_exists ;
1716use function define ;
1817use function defined ;
18+ use function file_exists ;
1919use function get_class ;
2020use function is_array ;
2121use function is_object ;
2222use function is_string ;
2323use function is_subclass_of ;
2424use function sprintf ;
2525
26+ /**
27+ * Provides service and component class resolution for Yii application analysis in PHPStan.
28+ *
29+ * Integrates Yii's dependency injection and component configuration with PHPStan's static analysis, enabling accurate
30+ * type inference, autocompletion, and service/component resolution for dynamic application services and components.
31+ *
32+ * This class parses the Yii application configuration to extract service and component definitions mapping service IDs
33+ * and component IDs to their corresponding class names.
34+ *
35+ * It supports both singleton and definition-based service registration, as well as component configuration via arrays
36+ * or instantiated objects.
37+ *
38+ * The implementation provides lookup methods for resolving the class name of a service or component by its ID, which
39+ * are used by PHPStan reflection extensions to enable static analysis and IDE support for dynamic properties and
40+ * dependency-injected services.
41+ *
42+ * Key features.
43+ * - Handles both array and object component configuration.
44+ * - Integrates with PHPStan reflection and type extensions for accurate analysis.
45+ * - Maps service and component IDs to their fully qualified class names.
46+ * - Parses Yii application config for service and component definitions.
47+ * - Provides lookup methods for service and component class resolution by ID.
48+ * - Supports singleton, definition, and closure-based service registration.
49+ * - Throws descriptive exceptions for invalid or unsupported definitions.
50+ *
51+ * @copyright Copyright (C) 2023 Terabytesoftw.
52+ * @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
53+ */
2654final class ServiceMap
2755{
2856 /**
29- * @var string[]
57+ * Service definitions map for Yii application analysis.
58+ *
59+ * @phpstan-var string[]
3060 */
3161 private array $ services = [];
3262
3363 /**
34- * @var array<string, string>
64+ * Component definitions map for Yii application analysis.
65+ *
66+ * @phpstan-var array<string, string>
3567 */
3668 private array $ components = [];
3769
3870 /**
39- * @throws ReflectionException
71+ * Creates a new instance of the {@see ServiceMap} class.
72+ *
73+ * @param string $configPath Path to the Yii application configuration file.
74+ *
75+ * @throws InvalidArgumentException If the provided config path doesn't exist.
76+ * @throws ReflectionException If the service definitions can't be resolved or are invalid.
77+ * @throws RuntimeException If the provided configuration path doesn't exist or is invalid.
4078 */
4179 public function __construct (string $ configPath )
4280 {
43- if (! file_exists ($ configPath )) {
81+ if (file_exists ($ configPath ) === false ) {
4482 throw new InvalidArgumentException (sprintf ('Provided config path %s must exist ' , $ configPath ));
4583 }
4684
@@ -50,6 +88,7 @@ public function __construct(string $configPath)
5088 defined ('YII_ENV_TEST ' ) || define ('YII_ENV_TEST ' , true );
5189
5290 $ config = require $ configPath ;
91+
5392 foreach ($ config ['container ' ]['singletons ' ] ?? [] as $ id => $ service ) {
5493 $ this ->addServiceDefinition ($ id , $ service );
5594 }
@@ -61,49 +100,108 @@ public function __construct(string $configPath)
61100 foreach ($ config ['components ' ] ?? [] as $ id => $ component ) {
62101 if (is_object ($ component )) {
63102 $ this ->components [$ id ] = get_class ($ component );
103+
64104 continue ;
65105 }
66106
67- if (! is_array ($ component )) {
107+ if (is_array ($ component ) === false ) {
68108 throw new RuntimeException (
69109 sprintf ('Invalid value for component with id %s. Expected object or array. ' , $ id ),
70110 );
71111 }
72112
73- if (null !== $ class = $ component ['class ' ] ?? null ) {
74- $ this ->components [$ id ] = $ class ;
113+ if (isset ( $ component [ ' class ' ]) && is_string ( $ component [ ' class ' ]) && $ component ['class ' ] !== '' ) {
114+ $ this ->components [$ id ] = $ component [ ' class ' ] ;
75115 }
76116 }
77117 }
78118
79- public function getServiceClassFromNode (Node $ node ): ?string
119+ /**
120+ * Registers a service definition in the service map for Yii application analysis.
121+ *
122+ * Adds a service definition to the internal service map resolving the fully qualified class name for the specified
123+ * service ID.
124+ *
125+ * This method supports various service definition formats, including class names, arrays, closures, and integer
126+ * identifiers, enabling accurate type inference and autocompletion for dependency injected services in PHPStan
127+ * analysis.
128+ *
129+ * The method delegates the resolution of the service class to {@see guessServiceDefinition()} which determines the
130+ * appropriate class name based on the provided service definition.
131+ *
132+ * This ensures compatibility with Yii's flexible service registration mechanisms and supports both singleton and
133+ * definition-based services.
134+ *
135+ * @param string $id Service identifier to register in the service map.
136+ * @param array|Closure|int|string $service Service definition in supported format.
137+ *
138+ * @throws ReflectionException if the service definition is invalid or can't be resolved.
139+ *
140+ * @phpstan-param array<mixed>|Closure|string|int $service
141+ */
142+ private function addServiceDefinition (string $ id , array |string |Closure |int $ service ): void
80143 {
81- if ($ node instanceof Node \Scalar \String_ && isset ($ this ->services [$ node ->value ])) {
82- return $ this ->services [$ node ->value ];
83- }
84-
85- return null ;
144+ $ this ->services [$ id ] = $ this ->guessServiceDefinition ($ id , $ service );
86145 }
87146
88- public function getComponentClassById (string $ id ): ?string
147+ /**
148+ * Retrieves the fully qualified class name of a Yii application component by its identifier.
149+ *
150+ * Looks up the component class name registered under the specified component ID in the internal component map.
151+ *
152+ * This method enables static analysis tools and IDEs to resolve the actual class type of dynamic application
153+ * components for accurate type inference, autocompletion, and property reflection.
154+ *
155+ * @param string $id Component identifier to look up in the component map.
156+ *
157+ * @return string|null Fully qualified class name of the component, or `null` if not found.
158+ */
159+ public function getComponentClassById (string $ id ): string |null
89160 {
90161 return $ this ->components [$ id ] ?? null ;
91162 }
92163
93164 /**
94- * @throws ReflectionException
165+ * Resolves the fully qualified class name of a service from a PHP-Parser AST node.
95166 *
96- * @phpstan-param array<mixed>|string|Closure|int $service
167+ * Inspects the provided AST node to determine if it represents a string service identifier, and if so, look up
168+ * the corresponding class name in the internal service map.
169+ *
170+ * This method enables static analysis tools and IDEs to infer the actual class type of services referenced by
171+ * string IDs in Yii application code supporting accurate type inference, autocompletion, and dependency injection
172+ * analysis.
173+ *
174+ * @param Node $node PHP-Parser AST node representing a service identifier.
175+ *
176+ * @return string|null Fully qualified class name of the service, or `null` if not found.
97177 */
98- private function addServiceDefinition ( string $ id , array | string | Closure | int $ service ): void
178+ public function getServiceClassFromNode ( Node $ node ): ? string
99179 {
100- $ this ->services [$ id ] = $ this ->guessServiceDefinition ($ id , $ service );
180+ if ($ node instanceof Node \Scalar \String_ && isset ($ this ->services [$ node ->value ])) {
181+ return $ this ->services [$ node ->value ];
182+ }
183+
184+ return null ;
101185 }
102186
103187 /**
104- * @throws ReflectionException
188+ * Infers the fully qualified class name for a Yii service definition.
189+ *
190+ * Determines the class name associated with a service definition provided in various supported formats, including
191+ * class name strings, configuration arrays, closures, or integer identifiers.
105192 *
106- * @phpstan-param array<mixed>|string|Closure|int $service
193+ * This method enables static analysis tools and IDEs to resolve the actual class type of dependency injected
194+ * services for accurate type inference, autocompletion, and service resolution in PHPStan analysis.
195+ *
196+ * @param string $id Service identifier being resolved.
197+ * @param array|Closure|int|string $service Service definition in supported format.
198+ *
199+ * @throws ReflectionException if the service definition is invalid or can't be resolved.
200+ * @throws RuntimeException if the service definition format is unsupported or missing required information.
201+ *
202+ * @return string Fully qualified class name of the resolved service.
203+ *
204+ * @phpstan-param array<mixed>|Closure|string|int $service
107205 */
108206 private function guessServiceDefinition (string $ id , array |string |Closure |int $ service ): string
109207 {
@@ -113,22 +211,23 @@ private function guessServiceDefinition(string $id, array|string|Closure|int $se
113211
114212 if ($ service instanceof Closure || is_string ($ service )) {
115213 $ returnType = (new ReflectionFunction ($ service ))->getReturnType ();
116- if (!$ returnType instanceof ReflectionNamedType) {
214+
215+ if ($ returnType instanceof ReflectionNamedType === false ) {
117216 throw new RuntimeException (sprintf ('Please provide return type for %s service closure ' , $ id ));
118217 }
119218
120219 return $ returnType ->getName ();
121220 }
122221
123- if (! is_array ($ service )) {
222+ if (is_array ($ service ) === false ) {
124223 throw new RuntimeException (sprintf ('Unsupported service definition for %s ' , $ id ));
125224 }
126225
127- if (isset ($ service ['class ' ])) {
226+ if (isset ($ service ['class ' ]) && is_string ( $ service [ ' class ' ]) && $ service [ ' class ' ] !== '' ) {
128227 return $ service ['class ' ];
129228 }
130229
131- if (isset ($ service [0 ]['class ' ])) {
230+ if (isset ($ service [0 ]['class ' ]) && is_string ( $ service [ 0 ][ ' class ' ]) && $ service [ 0 ][ ' class ' ] !== '' ) {
132231 return $ service [0 ]['class ' ];
133232 }
134233
0 commit comments