Skip to content

Commit 7c43584

Browse files
feat: Add ServiceLocatorDynamicMethodReturnTypeExtension to provide precise type inference for get() method. (#56)
1 parent cc5204b commit 7c43584

File tree

7 files changed

+405
-6
lines changed

7 files changed

+405
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- Bug #53: Update documentation for consistency and clarity; change section titles and add strict types declaration (@terabytesoftw)
2424
- Bug #54: Update `PHPStan` `tmpDir` config; move `runtime` directory to `root`; update docs (@terabytesoftw)
2525
- Bug #55: Remove `OS` and `PHP` version specifications from workflow files for simplification (@terabytesoftw)
26+
- Enh #56: Add `ServiceLocatorDynamicMethodReturnTypeExtension` to provide precise type inference for `get()` method (@terabytesoftw)
2627

2728
## 0.2.3 June 09, 2025
2829

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ inference, dynamic method resolution, and comprehensive property reflection.
5757
- Stub files for different application types (web, console, base).
5858
- Support for Yii2 constants (`YII_DEBUG`, `YII_ENV_*`).
5959

60+
**Service Locator Component Resolution**
61+
- Automatic fallback to mixed type for unknown component identifiers.
62+
- Dynamic return type inference for `ServiceLocator::get()` calls.
63+
- Priority-based resolution: ServiceMap components > ServiceMap services > Real classes > Mixed type.
64+
- Support for all Service Locator subclasses (Application, Module, custom classes).
65+
- Type inference with string variables and class name constants.
66+
6067
## Quick start
6168

6269
### Installation
@@ -159,7 +166,22 @@ $container = new Container();
159166

160167
// ✅ Type-safe service resolution
161168
$service = $container->get(MyService::class); // MyService
162-
$logger = $container->get('logger'); // LoggerInterface (if configured)
169+
$logger = $container->get('logger'); // LoggerInterface (if configured) or mixed
170+
```
171+
172+
#### Service locator
173+
174+
```php
175+
$serviceLocator = new ServiceLocator();
176+
177+
// ✅ Get component with type inference with class
178+
$mailer = $serviceLocator->get(Mailer::class); // MailerInterface
179+
180+
// ✅ Get component with string identifier and without configuration in ServiceMap
181+
$mailer = $serviceLocator->get('mailer'); // MailerInterface (if configured) or mixed
182+
183+
// ✅ User component with proper type inference in Action or Controller
184+
$user = $this->controller->module->get('user'); // UserInterface
163185
```
164186

165187
## Documentation

docs/examples.md

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class PostRepository
8686

8787
public function getLatestPost(): Post|null
8888
{
89-
// ✅ PHPStan knows this returns Post|null
89+
// ✅ PHPStan knows this return Post|null
9090
return Post::find()
9191
->where(['status' => 'published'])
9292
->orderBy('created_at DESC')
@@ -114,13 +114,13 @@ class UserModel extends \yii\db\ActiveRecord
114114
{
115115
public function getPosts(): \yii\db\ActiveQuery
116116
{
117-
// ✅ PHPStan knows this returns ActiveQuery<Post>
117+
// ✅ PHPStan knows this return ActiveQuery<Post>
118118
return $this->hasMany(Post::class, ['author_id' => 'id']);
119119
}
120120

121121
public function getProfile(): \yii\db\ActiveQuery
122122
{
123-
// ✅ PHPStan knows this returns ActiveQuery<UserProfile>
123+
// ✅ PHPStan knows this return ActiveQuery<UserProfile>
124124
return $this->hasOne(UserProfile::class, ['user_id' => 'id']);
125125
}
126126
}
@@ -194,7 +194,7 @@ class Post extends \yii\db\ActiveRecord
194194
{
195195
public static function find(): PostQuery
196196
{
197-
// ✅ PHPStan knows this returns PostQuery<Post>
197+
// ✅ PHPStan knows this return PostQuery<Post>
198198
return new PostQuery(get_called_class());
199199
}
200200
}
@@ -429,7 +429,7 @@ class ServiceManager
429429

430430
public function getPaymentService(): PaymentService
431431
{
432-
// ✅ PHPStan knows this returns PaymentService
432+
// ✅ PHPStan knows this return PaymentService
433433
return $this->container->get(PaymentService::class);
434434
}
435435

@@ -454,6 +454,50 @@ class ServiceManager
454454
}
455455
```
456456

457+
### Service locator in custom classes
458+
459+
```php
460+
<?php
461+
462+
declare(strict_types=1);
463+
464+
use yii\di\ServiceLocator;
465+
use app\services\{EmailService, LoggerService, CacheService};
466+
467+
class CustomServiceManager extends ServiceLocator
468+
{
469+
public function sendNotification(string $message): bool
470+
{
471+
// ✅ PHPStan knows these are the correct service types
472+
$email = $this->get('emailService'); // EmailService
473+
$logger = $this->get('loggerService'); // LoggerService
474+
$cache = $this->get('cacheService'); // CacheService
475+
476+
try {
477+
$result = $email->send($message);
478+
$logger->info('Notification sent successfully');
479+
$cache->delete('pending_notifications');
480+
481+
return $result;
482+
} catch (\Exception $e) {
483+
$logger->error('Failed to send notification: ' . $e->getMessage());
484+
return false;
485+
}
486+
}
487+
488+
public function getServicesByType(): array
489+
{
490+
// ✅ Different ways to resolve services
491+
return [
492+
'email_by_id' => $this->get('emailService'), // EmailService
493+
'email_by_class' => $this->get(EmailService::class), // EmailService
494+
'logger_by_id' => $this->get('loggerService'), // LoggerService
495+
'logger_by_class' => $this->get(LoggerService::class), // LoggerService
496+
];
497+
}
498+
}
499+
```
500+
457501
### Service configuration examples
458502

459503
```php

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ services:
4747
-
4848
class: yii2\extensions\phpstan\type\HeaderCollectionDynamicMethodReturnTypeExtension
4949
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
50+
-
51+
class: yii2\extensions\phpstan\type\ServiceLocatorDynamicMethodReturnTypeExtension
52+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
5053
-
5154
class: yii2\extensions\phpstan\StubFilesExtension
5255
tags:
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace yii2\extensions\phpstan\type;
6+
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\{MethodReflection, ParametersAcceptorSelector, ReflectionProvider};
11+
use PHPStan\Type\{DynamicMethodReturnTypeExtension, MixedType, ObjectType, Type};
12+
use yii\di\ServiceLocator;
13+
use yii2\extensions\phpstan\ServiceMap;
14+
15+
/**
16+
* Provides dynamic return type extension for Yii Service Locator component resolution in PHPStan analysis.
17+
*
18+
* Integrates the Yii Service Locator service {@see ServiceLocator} with PHPStan dynamic method return type extension
19+
* system, enabling precise type inference for {@see ServiceLocator::get()} calls based on component ID and the
20+
* {@see ServiceMap}.
21+
*
22+
* This extension analyzes the first argument of {@see ServiceLocator::get()} to determine the most accurate return
23+
* type, returning an {@see ObjectType} for known component classes or a {@see MixedType} for unknown or dynamic ID.
24+
*
25+
* Key features:
26+
* - Accurate return type inference for {@see ServiceLocator::get()} based on component ID string.
27+
* - Compatible with PHPStan strict static analysis and autocompletion.
28+
* - Falls back to method signature return type for unsupported or invalid calls.
29+
* - Supports Yii modules, applications, and any class extending {@see ServiceLocator}.
30+
* - Uses {@see ServiceMap} to resolve component class names.
31+
*
32+
* @see DynamicMethodReturnTypeExtension for PHPStan dynamic return type extension contract.
33+
* @see ServiceMap for service and component map for Yii Application static analysis.
34+
*
35+
* @copyright Copyright (C) 2023 Terabytesoftw.
36+
* @license https://opensource.org/license/bsd-3-clause BSD 3-Clause License.
37+
*/
38+
final class ServiceLocatorDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
39+
{
40+
/**
41+
* Creates a new instance of the {@see ServiceLocatorDynamicMethodReturnTypeExtension} class.
42+
*
43+
* @param ReflectionProvider $reflectionProvider Reflection provider for class and property lookups.
44+
* @param ServiceMap $serviceMap Service and component map for Yii Application static analysis.
45+
*/
46+
public function __construct(
47+
private readonly ReflectionProvider $reflectionProvider,
48+
private readonly ServiceMap $serviceMap,
49+
) {}
50+
51+
/**
52+
* Returns the class name for which this dynamic return type extension applies.
53+
*
54+
* Specifies the fully qualified class name of the Yii ServiceLocator {@see ServiceLocator} that this extension
55+
* target for dynamic return type inference in PHPStan analysis.
56+
*
57+
* This method enables PHPStan to associate the extension with the {@see ServiceLocator} class and all its
58+
* subclasses (like Module and Application), ensuring that dynamic return type logic is applied to component
59+
* resolution calls.
60+
*
61+
* @return string Fully qualified class name of the supported ServiceLocator class.
62+
*
63+
* @phpstan-return class-string
64+
*/
65+
public function getClass(): string
66+
{
67+
return ServiceLocator::class;
68+
}
69+
70+
/**
71+
* Infers the return type for a {@see ServiceLocator::get()} method call based on the provided component ID
72+
* argument.
73+
*
74+
* Determines the most accurate return type for component resolution by analyzing the first argument of the
75+
* {@see ServiceLocator::get()} call.
76+
*
77+
* - If the argument is a constant string and matches a known component in the {@see ServiceMap}, returns an
78+
* {@see ObjectType} for the resolved class.
79+
* - If the argument is a class name known to the {@see ReflectionProvider}, returns an {@see ObjectType} for that
80+
* class.
81+
* - Otherwise, returns a {@see MixedType} to indicate an unknown or dynamic component type.
82+
*
83+
* Falls back to the default method signature return type for unsupported or invalid calls, ensuring compatibility
84+
* with PHPStan static analysis and IDE autocompletion.
85+
*
86+
* @param MethodReflection $methodReflection Reflection instance for the method being analyzed.
87+
* @param MethodCall $methodCall AST node for the method call expression.
88+
* @param Scope $scope PHPStan analysis scope for type resolution.
89+
*
90+
* @return Type Inferred return type for the component resolution call.
91+
*/
92+
public function getTypeFromMethodCall(
93+
MethodReflection $methodReflection,
94+
MethodCall $methodCall,
95+
Scope $scope,
96+
): Type {
97+
if (isset($methodCall->args[0]) === false || $methodCall->args[0]::class !== Arg::class) {
98+
return ParametersAcceptorSelector::selectFromArgs(
99+
$scope,
100+
$methodCall->getArgs(),
101+
$methodReflection->getVariants(),
102+
)->getReturnType();
103+
}
104+
105+
$argType = $scope->getType($methodCall->args[0]->value);
106+
$constantStrings = $argType->getConstantStrings();
107+
108+
if (count($constantStrings) === 1) {
109+
$value = $constantStrings[0]->getValue();
110+
111+
$componentClass = $this->serviceMap->getComponentClassById($value);
112+
113+
if ($componentClass !== null) {
114+
return new ObjectType($componentClass);
115+
}
116+
117+
$serviceClass = $this->serviceMap->getServiceById($value);
118+
119+
if ($serviceClass !== null) {
120+
return new ObjectType($serviceClass);
121+
}
122+
123+
if ($this->reflectionProvider->hasClass($value)) {
124+
return new ObjectType($value);
125+
}
126+
}
127+
128+
return new MixedType();
129+
}
130+
131+
/**
132+
* Determines whether the specified method is supported for dynamic return type inference.
133+
*
134+
* Checks if the method name is {@see ServiceLocator::get}, which is the only method supported by this extension for
135+
* dynamic return type analysis.
136+
*
137+
* This enables PHPStan to apply custom type inference logic exclusively to component resolution calls on the Yii
138+
* Service Locator {@see ServiceLocator} and its subclasses (Module, Application).
139+
*
140+
* @param MethodReflection $methodReflection Reflection instance for the method being analyzed.
141+
*
142+
* @return bool `true` if the method is {@see ServiceLocator::get}; `false` otherwise.
143+
*/
144+
public function isMethodSupported(MethodReflection $methodReflection): bool
145+
{
146+
return $methodReflection->getName() === 'get';
147+
}
148+
}

0 commit comments

Comments
 (0)