Skip to content

Commit 449ba39

Browse files
fix: Enhance property reflection in UserPropertiesClassReflectionExtension to support identity class resolution and improve type inference for user component properties. (#34)
1 parent c390b9b commit 449ba39

File tree

7 files changed

+193
-28
lines changed

7 files changed

+193
-28
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
- Enh #25: Add support for `PHPStan` Extension Installer (@samuelrajan747)
66
- Enh #26: Add `PHPStan` extension installer instructions and improve `ServiceMap` configuration handling (@terabytesoftw)
7-
- Bug #27: Enhance error handling in `ServiceMap` for invalid configuration structures and add corresponding test cases (@terabytesoftw)
7+
- Bug #27: Fix error handling in `ServiceMap` for invalid configuration structures and add corresponding test cases (@terabytesoftw)
88
- Bug #28: Refactor component handling in `ServiceMap` to improve variable naming and streamline logic (@terabytesoftw)
99
- Enh #29: Enable strict rules and bleeding edge analysis, and update `README.md` with strict configuration examples (@terabytesoftw)
10-
- Bug #33: Enhance `ActiveRecordDynamicStaticMethodReturnTypeExtension` type inference for `ActiveQuery` support, and fix phpstan errors max lvl in tests (@terabytesoftw)
10+
- Bug #33: Fix `ActiveRecordDynamicStaticMethodReturnTypeExtension` type inference for `ActiveQuery` support, and fix phpstan errors max lvl in tests (@terabytesoftw)
11+
- Bug #34: Fix property reflection in `UserPropertiesClassReflectionExtension` to support `identityClass` resolution and improve type inference for `user` component properties (@terabytesoftw)
1112

1213
## 0.2.2 June 04, 2025
1314

src/ServiceMap.php

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ final class ServiceMap
7676
*/
7777
private array $components = [];
7878

79+
/**
80+
* Component definitions for Yii application analysis.
81+
*
82+
* @phpstan-var array<string, mixed>
83+
*/
84+
private array $componentsDefinitions = [];
85+
7986
/**
8087
* Creates a new instance of the {@see ServiceMap} class.
8188
*
@@ -123,6 +130,53 @@ public function getComponentClassById(string $id): string|null
123130
return $this->components[$id] ?? null;
124131
}
125132

133+
/**
134+
* Retrieves the component definition array by its identifier.
135+
*
136+
* Looks up the component definition registered under the specified component ID in the internal component
137+
* definitions map.
138+
*
139+
* This method provides access to the raw component configuration array, enabling static analysis tools and IDEs to
140+
* inspect component properties, dependencies, and configuration options for accurate type inference and reflection
141+
* analysis.
142+
*
143+
* @param string $id Component identifier to look up in the component definitions map.
144+
*
145+
* @return array|null Component definition array with configuration options, or `null` if not found.
146+
*
147+
* @phpstan-return array<array-key, mixed>|null
148+
*/
149+
public function getComponentDefinitionById(string $id): array|null
150+
{
151+
$definition = $this->componentsDefinitions[$id] ?? null;
152+
153+
return is_array($definition) ? $definition : null;
154+
}
155+
156+
/**
157+
* Retrieves the component definition for a given class.
158+
*
159+
* @param string $class Fully qualified class name to look up.
160+
*
161+
* @return array|null The component definition array, or null if not found.
162+
*
163+
* @phpstan-return array<array-key, mixed>|null
164+
*/
165+
public function getComponentDefinitionByClassName(string $class): array|null
166+
{
167+
foreach ($this->components as $id => $componentClass) {
168+
if (
169+
$componentClass === $class &&
170+
isset($this->componentsDefinitions[$id])
171+
&& is_array($this->componentsDefinitions[$id])
172+
) {
173+
return $this->componentsDefinitions[$id];
174+
}
175+
}
176+
177+
return null;
178+
}
179+
126180
/**
127181
* Resolves the fully qualified class name of a service from a PHP-Parser AST node.
128182
*
@@ -154,12 +208,12 @@ public function getServiceClassFromNode(Node $node): string|null
154208
*
155209
* This method is responsible for parsing the Yii application configuration, providing a normalized array for
156210
* further processing by the service and component mapping logic. It throws descriptive exceptions if the file is
157-
* missing, does not return an array, or contains invalid section types, ensuring robust error handling and
211+
* missing, doesn't return an array, or contains invalid section types, ensuring robust error handling and
158212
* predictable static analysis.
159213
*
160-
* @param string $configPath Path to the Yii application configuration file. If empty, returns an empty array.
214+
* @param string $configPath Path to the Yii application configuration file. If empty, return an empty array.
161215
*
162-
* @throws RuntimeException if the closure does not have a return type or the definition is unsupported.
216+
* @throws RuntimeException if a runtime error prevents the operation from completing successfully.
163217
*
164218
* @phpstan import-type ServiceType from ServiceMap
165219
* @phpstan-return array{}|ServiceType Normalized configuration array or empty array if no config is provided.
@@ -210,7 +264,7 @@ private function loadConfig(string $configPath): array
210264
* @param array|int|object|string $definition Service definition to normalize.
211265
*
212266
* @throws ReflectionException if the service definition is invalid or can't be resolved.
213-
* @throws RuntimeException if the closure does not have a return type or the definition is unsupported.
267+
* @throws RuntimeException if a runtime error prevents the operation from completing successfully.
214268
*
215269
* @phpstan-import-type DefinitionType from ServiceMap
216270
* @phpstan-param DefinitionType $definition
@@ -283,6 +337,10 @@ private function processComponents(array $config): void
283337

284338
if (isset($definition['class']) && is_string($definition['class']) && $definition['class'] !== '') {
285339
$this->components[$id] = $definition['class'];
340+
341+
unset($definition['class']);
342+
343+
$this->componentsDefinitions[$id] = $definition;
286344
}
287345
}
288346
}
@@ -356,7 +414,7 @@ private function processSingletons(array $config): void
356414
* Throws a {@see RuntimeException} when a configuration file section is not an array.
357415
*
358416
* This method is invoked when a required section of the Yii application configuration file (such as components,
359-
* container, container.definitions, or container.singletons) does not contain a valid array.
417+
* container, container.definitions, or container.singletons) doesn't contain a valid array.
360418
*
361419
* It ensures that only valid array structures are processed during configuration parsing, providing a clear and
362420
* descriptive error message for debugging and static analysis.
@@ -393,8 +451,8 @@ private function throwErrorWhenIdIsNotString(string ...$args): never
393451
/**
394452
* Throws a {@see RuntimeException} when a service or component definition is unsupported.
395453
*
396-
* This method is invoked when the provided definition for a service or component cannot be resolved to a valid
397-
* class name or does not match any supported configuration pattern.
454+
* This method is invoked when the provided definition for a service or component can't be resolved to a valid
455+
* class name or doesn't match any supported configuration pattern.
398456
*
399457
* It ensures that only valid and supported definitions are processed during service and component resolution,
400458
* providing a clear and descriptive error message for debugging and static analysis.

src/reflection/UserPropertiesClassReflectionExtension.php

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
MissingPropertyFromReflectionException,
1010
PropertiesClassReflectionExtension,
1111
PropertyReflection,
12+
ReflectionProvider,
1213
};
1314
use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension;
1415
use PHPStan\Reflection\Dummy\DummyPropertyReflection;
15-
use PHPStan\Type\ObjectType;
16+
use PHPStan\Type\Constant\ConstantBooleanType;
17+
use PHPStan\Type\{IntegerType, NullType, ObjectType, StringType, TypeCombinator};
1618
use yii\web\User;
1719
use yii2\extensions\phpstan\ServiceMap;
1820

@@ -49,20 +51,22 @@ final class UserPropertiesClassReflectionExtension implements PropertiesClassRef
4951
*
5052
* @param AnnotationsPropertiesClassReflectionExtension $annotationsProperties Extension for handling
5153
* annotation-based properties.
54+
* @param ReflectionProvider $reflectionProvider Reflection provider for class and property lookups.
55+
* @param ServiceMap $serviceMap Service map for resolving component classes by ID.
5256
*/
5357
public function __construct(
5458
private readonly AnnotationsPropertiesClassReflectionExtension $annotationsProperties,
59+
private readonly ReflectionProvider $reflectionProvider,
5560
private readonly ServiceMap $serviceMap,
5661
) {}
5762

5863
/**
5964
* Retrieves the property reflection for a given property on the Yii user component.
6065
*
6166
* Resolves the property reflection for the specified property name by checking for the dynamic
62-
* {@see User::identity] property, native properties, and annotation-based properties on the Yii user instance.
67+
* {@see User::identity} property, native properties, and annotation-based properties on the Yii user instance.
6368
*
64-
* This method ensures that the {@see User::identity] property and properties defined native or via annotations are
65-
* accessible for static analysis and IDE support.
69+
* For the 'identity' property, it resolves the type based on the configured identityClass in the user component.
6670
*
6771
* @param ClassReflection $classReflection Class reflection instance for the Yii user component.
6872
* @param string $propertyName Name of the property to retrieve.
@@ -73,10 +77,35 @@ public function __construct(
7377
*/
7478
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
7579
{
76-
if (
77-
$propertyName === 'identity' &&
78-
($componentClass = $this->serviceMap->getComponentClassById($propertyName)) !== null
79-
) {
80+
if (in_array($propertyName, ['id', 'identity', 'isGuest'], true) === true) {
81+
$identityClass = $this->getIdentityClass();
82+
83+
if ($propertyName === 'identity' && $identityClass !== null) {
84+
return new ComponentPropertyReflection(
85+
new DummyPropertyReflection($propertyName),
86+
new ObjectType($identityClass),
87+
$classReflection,
88+
);
89+
}
90+
91+
if ($propertyName === 'id') {
92+
return new ComponentPropertyReflection(
93+
new DummyPropertyReflection($propertyName),
94+
TypeCombinator::union(new IntegerType(), new StringType(), new NullType()),
95+
$classReflection,
96+
);
97+
}
98+
99+
if ($propertyName === 'isGuest') {
100+
return new ComponentPropertyReflection(
101+
new DummyPropertyReflection($propertyName),
102+
new ConstantBooleanType(true),
103+
$classReflection,
104+
);
105+
}
106+
}
107+
108+
if (($componentClass = $this->serviceMap->getComponentClassById($propertyName)) !== null) {
80109
return new ComponentPropertyReflection(
81110
new DummyPropertyReflection($propertyName),
82111
new ObjectType($componentClass),
@@ -94,30 +123,46 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa
94123
/**
95124
* Determines whether the specified property exists on the Yii user component.
96125
*
97-
* Checks for the existence of a property on the user instance by considering native properties and
98-
* annotation-based properties.
99-
*
100-
* This method ensures compatibility with the user component context, enabling accurate property reflection for
101-
* static analysis and IDE autocompletion.
126+
* Checks for the existence of a property on the user instance by considering native properties,
127+
* annotation-based properties, and the special 'identity' property.
102128
*
103129
* @param ClassReflection $classReflection Class reflection instance for the Yii user component.
104130
* @param string $propertyName Name of the property to check for existence.
105131
*
106-
* @return bool `true` if the property exists as a native or annotated property; `false` otherwise.
132+
* @return bool `true` if the property exists as a native, annotated, or identity property; `false` otherwise.
107133
*/
108134
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
109135
{
110136
if (
111137
$classReflection->getName() !== User::class &&
112-
$classReflection->isSubclassOf(User::class) === false) {
138+
$classReflection->isSubclassOfClass($this->reflectionProvider->getClass(User::class)) === false
139+
) {
113140
return false;
114141
}
115142

116-
if ($propertyName === 'identity' && $this->serviceMap->getComponentClassById($propertyName) !== null) {
117-
return true;
143+
return in_array($propertyName, ['id', 'identity', 'isGuest'], true)
144+
? $this->getIdentityClass() !== null
145+
: $this->serviceMap->getComponentClassById($propertyName) !== null;
146+
}
147+
148+
/**
149+
* Attempts to resolve the identity class from the user component configuration.
150+
*
151+
* This method tries to determine the identityClass configured for the user component
152+
* by looking at the service map's user component configuration.
153+
*
154+
* @return string|null The fully qualified identity class name, or null if not found.
155+
*/
156+
private function getIdentityClass(): string|null
157+
{
158+
$identityClass = null;
159+
160+
$definition = $this->serviceMap->getComponentDefinitionByClassName(User::class);
161+
162+
if (isset($definition['identityClass']) && is_string($definition['identityClass'])) {
163+
$identityClass = $definition['identityClass'];
118164
}
119165

120-
return $classReflection->hasNativeProperty($propertyName)
121-
|| $this->annotationsProperties->hasProperty($classReflection, $propertyName);
166+
return $identityClass;
122167
}
123168
}

tests/ServiceMapTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use SplObjectStorage;
1313
use SplStack;
1414
use yii\base\InvalidArgumentException;
15+
use yii\web\User;
1516
use yii2\extensions\phpstan\ServiceMap;
1617
use yii2\extensions\phpstan\tests\stub\MyActiveRecord;
1718

@@ -111,6 +112,24 @@ public function testItLoadsServicesAndComponents(): void
111112
$serviceMap->getComponentClassById('customComponent'),
112113
'ServiceMap should resolve component id \'customComponent\' to \'MyActiveRecord::class\'.',
113114
);
115+
self::assertSame(
116+
[
117+
'identityClass' => 'yii2\extensions\phpstan\tests\stub\User',
118+
],
119+
$serviceMap->getComponentDefinitionByClassName(User::class),
120+
'ServiceMap should return the component definition for \'yii\web\User\'.',
121+
);
122+
self::assertNull(
123+
$serviceMap->getComponentDefinitionByClassName('nonExistentComponent'),
124+
'ServiceMap should return \'null\' for a \'non-existent\' component class.',
125+
);
126+
self::assertSame(
127+
[
128+
'identityClass' => 'yii2\extensions\phpstan\tests\stub\User',
129+
],
130+
$serviceMap->getComponentDefinitionById('user'),
131+
'ServiceMap should return the component definition for \'user\'.',
132+
);
114133
self::assertNull(
115134
$serviceMap->getComponentClassById('nonExistentComponent'),
116135
'ServiceMap should return \'null\' for a \'non-existent\' component id.',

tests/fixture/config.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types=1);
44

55
use yii2\extensions\phpstan\tests\stub\MyActiveRecord;
6+
use yii2\extensions\phpstan\tests\stub\User;
67

78
return [
89
'components' => [
@@ -13,6 +14,10 @@
1314
'class' => MyActiveRecord::class,
1415
],
1516
'customInitializedComponent' => new MyActiveRecord(),
17+
'user' => [
18+
'class' => 'yii\web\User',
19+
'identityClass' => User::class,
20+
],
1621
],
1722
'container' => [
1823
'singletons' => [

tests/stub/MyController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ public function actionMy(): void
9191
Yii::$app->response->data = Yii::$app->request->rawBody;
9292

9393
$guest = Yii::$app->user->isGuest;
94+
$id = Yii::$app->user->id;
9495

9596
Yii::$app->user->identity->getAuthKey();
9697
Yii::$app->user->identity->doSomething();

tests/stub/User.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace yii2\extensions\phpstan\tests\stub;
6+
7+
use yii\db\ActiveRecord;
8+
use yii\web\IdentityInterface;
9+
10+
class User extends ActiveRecord implements IdentityInterface
11+
{
12+
public static function findIdentity($id)
13+
{
14+
return new static();
15+
}
16+
17+
public static function findIdentityByAccessToken($token, $type = null)
18+
{
19+
return new static();
20+
}
21+
22+
public function getId()
23+
{
24+
return 1;
25+
}
26+
27+
public function getAuthKey()
28+
{
29+
return '';
30+
}
31+
32+
public function validateAuthKey($authKey)
33+
{
34+
return true;
35+
}
36+
}

0 commit comments

Comments
 (0)