From a75dcd9e007838411f5b0f11f4bb0e0178506656 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Sun, 8 Jun 2025 10:11:05 -0400 Subject: [PATCH 1/5] fix: Enhance property reflection in `UserPropertiesClassReflectionExtension` to support identity class resolution and improve type inference for user component properties. --- CHANGELOG.md | 5 +- src/ServiceMap.php | 72 ++++++++++++++++-- ...UserPropertiesClassReflectionExtension.php | 74 +++++++++++++------ tests/ServiceMapTest.php | 19 +++++ tests/fixture/config.php | 5 ++ tests/stub/MyController.php | 1 + tests/stub/User.php | 36 +++++++++ 7 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 tests/stub/User.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a732e9b..ba72613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,11 @@ - Enh #25: Add support for `PHPStan` Extension Installer (@samuelrajan747) - Enh #26: Add `PHPStan` extension installer instructions and improve `ServiceMap` configuration handling (@terabytesoftw) -- Bug #27: Enhance error handling in `ServiceMap` for invalid configuration structures and add corresponding test cases (@terabytesoftw) +- Bug #27: Fix error handling in `ServiceMap` for invalid configuration structures and add corresponding test cases (@terabytesoftw) - Bug #28: Refactor component handling in `ServiceMap` to improve variable naming and streamline logic (@terabytesoftw) - Enh #29: Enable strict rules and bleeding edge analysis, and update `README.md` with strict configuration examples (@terabytesoftw) -- Bug #33: Enhance `ActiveRecordDynamicStaticMethodReturnTypeExtension` type inference for `ActiveQuery` support, and fix phpstan errors max lvl in tests (@terabytesoftw) +- Bug #33: Fix `ActiveRecordDynamicStaticMethodReturnTypeExtension` type inference for `ActiveQuery` support, and fix phpstan errors max lvl in tests (@terabytesoftw) +- Bug #34: Fix property reflection in `UserPropertiesClassReflectionExtension` to support `identityClass` resolution and improve type inference for `user` component properties (@terabytesoftw) ## 0.2.2 June 04, 2025 diff --git a/src/ServiceMap.php b/src/ServiceMap.php index cd8a058..ff3434c 100644 --- a/src/ServiceMap.php +++ b/src/ServiceMap.php @@ -76,6 +76,13 @@ final class ServiceMap */ private array $components = []; + /** + * Component definitions for Yii application analysis. + * + * @phpstan-var array + */ + private array $componentsDefinitions = []; + /** * Creates a new instance of the {@see ServiceMap} class. * @@ -123,6 +130,53 @@ public function getComponentClassById(string $id): string|null return $this->components[$id] ?? null; } + /** + * Retrieves the component definition array by its identifier. + * + * Looks up the component definition registered under the specified component ID in the internal component + * definitions map. + * + * This method provides access to the raw component configuration array, enabling static analysis tools and IDEs to + * inspect component properties, dependencies, and configuration options for accurate type inference and reflection + * analysis. + * + * @param string $id Component identifier to look up in the component definitions map. + * + * @return array|null Component definition array with configuration options, or `null` if not found. + * + * @phpstan-return array|null + */ + public function getComponentDefinitionById(string $id): array|null + { + $definition = $this->componentsDefinitions[$id] ?? null; + + return is_array($definition) ? $definition : null; + } + + /** + * Retrieves the component definition for a given class. + * + * @param string $class Fully qualified class name to look up. + * + * @return array|null The component definition array, or null if not found. + * + * @phpstan-return array|null + */ + public function getComponentDefinitionByClassName(string $class): array|null + { + foreach ($this->components as $id => $componentClass) { + if ( + $componentClass === $class && + isset($this->componentsDefinitions[$id]) + && is_array($this->componentsDefinitions[$id]) + ) { + return $this->componentsDefinitions[$id]; + } + } + + return null; + } + /** * Resolves the fully qualified class name of a service from a PHP-Parser AST node. * @@ -154,12 +208,12 @@ public function getServiceClassFromNode(Node $node): string|null * * This method is responsible for parsing the Yii application configuration, providing a normalized array for * further processing by the service and component mapping logic. It throws descriptive exceptions if the file is - * missing, does not return an array, or contains invalid section types, ensuring robust error handling and + * missing, doesn't return an array, or contains invalid section types, ensuring robust error handling and * predictable static analysis. * - * @param string $configPath Path to the Yii application configuration file. If empty, returns an empty array. + * @param string $configPath Path to the Yii application configuration file. If empty, return an empty array. * - * @throws RuntimeException if the closure does not have a return type or the definition is unsupported. + * @throws RuntimeException if a runtime error prevents the operation from completing successfully. * * @phpstan import-type ServiceType from ServiceMap * @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 * @param array|int|object|string $definition Service definition to normalize. * * @throws ReflectionException if the service definition is invalid or can't be resolved. - * @throws RuntimeException if the closure does not have a return type or the definition is unsupported. + * @throws RuntimeException if a runtime error prevents the operation from completing successfully. * * @phpstan-import-type DefinitionType from ServiceMap * @phpstan-param DefinitionType $definition @@ -283,6 +337,10 @@ private function processComponents(array $config): void if (isset($definition['class']) && is_string($definition['class']) && $definition['class'] !== '') { $this->components[$id] = $definition['class']; + + unset($definition['class']); + + $this->componentsDefinitions[$id] = $definition; } } } @@ -356,7 +414,7 @@ private function processSingletons(array $config): void * Throws a {@see RuntimeException} when a configuration file section is not an array. * * This method is invoked when a required section of the Yii application configuration file (such as components, - * container, container.definitions, or container.singletons) does not contain a valid array. + * container, container.definitions, or container.singletons) doesn't contain a valid array. * * It ensures that only valid array structures are processed during configuration parsing, providing a clear and * descriptive error message for debugging and static analysis. @@ -393,8 +451,8 @@ private function throwErrorWhenIdIsNotString(string ...$args): never /** * Throws a {@see RuntimeException} when a service or component definition is unsupported. * - * This method is invoked when the provided definition for a service or component cannot be resolved to a valid - * class name or does not match any supported configuration pattern. + * This method is invoked when the provided definition for a service or component can't be resolved to a valid + * class name or doesn't match any supported configuration pattern. * * It ensures that only valid and supported definitions are processed during service and component resolution, * providing a clear and descriptive error message for debugging and static analysis. diff --git a/src/reflection/UserPropertiesClassReflectionExtension.php b/src/reflection/UserPropertiesClassReflectionExtension.php index 8bcf982..5cfd7e7 100644 --- a/src/reflection/UserPropertiesClassReflectionExtension.php +++ b/src/reflection/UserPropertiesClassReflectionExtension.php @@ -9,6 +9,7 @@ MissingPropertyFromReflectionException, PropertiesClassReflectionExtension, PropertyReflection, + ReflectionProvider, }; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\Dummy\DummyPropertyReflection; @@ -49,9 +50,12 @@ final class UserPropertiesClassReflectionExtension implements PropertiesClassRef * * @param AnnotationsPropertiesClassReflectionExtension $annotationsProperties Extension for handling * annotation-based properties. + * @param ReflectionProvider $reflectionProvider Reflection provider for class and property lookups. + * @param ServiceMap $serviceMap Service map for resolving component classes by ID. */ public function __construct( private readonly AnnotationsPropertiesClassReflectionExtension $annotationsProperties, + private readonly ReflectionProvider $reflectionProvider, private readonly ServiceMap $serviceMap, ) {} @@ -59,10 +63,9 @@ public function __construct( * Retrieves the property reflection for a given property on the Yii user component. * * Resolves the property reflection for the specified property name by checking for the dynamic - * {@see User::identity] property, native properties, and annotation-based properties on the Yii user instance. + * {@see User::identity} property, native properties, and annotation-based properties on the Yii user instance. * - * This method ensures that the {@see User::identity] property and properties defined native or via annotations are - * accessible for static analysis and IDE support. + * For the 'identity' property, it resolves the type based on the configured identityClass in the user component. * * @param ClassReflection $classReflection Class reflection instance for the Yii user component. * @param string $propertyName Name of the property to retrieve. @@ -73,15 +76,24 @@ public function __construct( */ public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { - if ( - $propertyName === 'identity' && - ($componentClass = $this->serviceMap->getComponentClassById($propertyName)) !== null - ) { - return new ComponentPropertyReflection( - new DummyPropertyReflection($propertyName), - new ObjectType($componentClass), - $classReflection, - ); + if (in_array($propertyName, ['id', 'identity', 'isGuest'], true) === true) { + $identityClass = $this->getIdentityClass(); + + if ($identityClass !== null) { + return new ComponentPropertyReflection( + new DummyPropertyReflection($propertyName), + new ObjectType($identityClass), + $classReflection, + ); + } + + if (($componentClass = $this->serviceMap->getComponentClassById($propertyName)) !== null) { + return new ComponentPropertyReflection( + new DummyPropertyReflection($propertyName), + new ObjectType($componentClass), + $classReflection, + ); + } } if ($classReflection->hasNativeProperty($propertyName)) { @@ -94,30 +106,46 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa /** * Determines whether the specified property exists on the Yii user component. * - * Checks for the existence of a property on the user instance by considering native properties and - * annotation-based properties. - * - * This method ensures compatibility with the user component context, enabling accurate property reflection for - * static analysis and IDE autocompletion. + * Checks for the existence of a property on the user instance by considering native properties, + * annotation-based properties, and the special 'identity' property. * * @param ClassReflection $classReflection Class reflection instance for the Yii user component. * @param string $propertyName Name of the property to check for existence. * - * @return bool `true` if the property exists as a native or annotated property; `false` otherwise. + * @return bool `true` if the property exists as a native, annotated, or identity property; `false` otherwise. */ public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { if ( $classReflection->getName() !== User::class && - $classReflection->isSubclassOf(User::class) === false) { + $classReflection->isSubclassOfClass($this->reflectionProvider->getClass(User::class)) === false + ) { return false; } - if ($propertyName === 'identity' && $this->serviceMap->getComponentClassById($propertyName) !== null) { - return true; + return + $this->getIdentityClass() !== null || + $this->serviceMap->getComponentClassById($propertyName) !== null; + } + + /** + * Attempts to resolve the identity class from the user component configuration. + * + * This method tries to determine the identityClass configured for the user component + * by looking at the service map's user component configuration. + * + * @return string|null The fully qualified identity class name, or null if not found. + */ + private function getIdentityClass(): string|null + { + $identityClass = null; + + $definition = $this->serviceMap->getComponentDefinitionByClassName(User::class); + + if (isset($definition['identityClass']) && is_string($definition['identityClass'])) { + $identityClass = $definition['identityClass']; } - return $classReflection->hasNativeProperty($propertyName) - || $this->annotationsProperties->hasProperty($classReflection, $propertyName); + return $identityClass; } } diff --git a/tests/ServiceMapTest.php b/tests/ServiceMapTest.php index 6682c51..8de7408 100644 --- a/tests/ServiceMapTest.php +++ b/tests/ServiceMapTest.php @@ -12,6 +12,7 @@ use SplObjectStorage; use SplStack; use yii\base\InvalidArgumentException; +use yii\web\User; use yii2\extensions\phpstan\ServiceMap; use yii2\extensions\phpstan\tests\stub\MyActiveRecord; @@ -111,6 +112,24 @@ public function testItLoadsServicesAndComponents(): void $serviceMap->getComponentClassById('customComponent'), 'ServiceMap should resolve component id \'customComponent\' to \'MyActiveRecord::class\'.', ); + self::assertSame( + [ + 'identityClass' => 'yii2\extensions\phpstan\tests\stub\User', + ], + $serviceMap->getComponentDefinitionByClassName(User::class), + 'ServiceMap should return the component definition for \'yii\web\User\'.', + ); + self::assertNull( + $serviceMap->getComponentDefinitionByClassName('nonExistentComponent'), + 'ServiceMap should return \'null\' for a \'non-existent\' component class.', + ); + self::assertSame( + [ + 'identityClass' => 'yii2\extensions\phpstan\tests\stub\User', + ], + $serviceMap->getComponentDefinitionById('user'), + 'ServiceMap should return the component definition for \'user\'.', + ); self::assertNull( $serviceMap->getComponentClassById('nonExistentComponent'), 'ServiceMap should return \'null\' for a \'non-existent\' component id.', diff --git a/tests/fixture/config.php b/tests/fixture/config.php index e128e91..8f3f251 100644 --- a/tests/fixture/config.php +++ b/tests/fixture/config.php @@ -3,6 +3,7 @@ declare(strict_types=1); use yii2\extensions\phpstan\tests\stub\MyActiveRecord; +use yii2\extensions\phpstan\tests\stub\User; return [ 'components' => [ @@ -13,6 +14,10 @@ 'class' => MyActiveRecord::class, ], 'customInitializedComponent' => new MyActiveRecord(), + 'user' => [ + 'class' => 'yii\web\User', + 'identityClass' => User::class, + ], ], 'container' => [ 'singletons' => [ diff --git a/tests/stub/MyController.php b/tests/stub/MyController.php index 3ac7d6b..ca8d31e 100644 --- a/tests/stub/MyController.php +++ b/tests/stub/MyController.php @@ -91,6 +91,7 @@ public function actionMy(): void Yii::$app->response->data = Yii::$app->request->rawBody; $guest = Yii::$app->user->isGuest; + $id = Yii::$app->user->id; Yii::$app->user->identity->getAuthKey(); Yii::$app->user->identity->doSomething(); diff --git a/tests/stub/User.php b/tests/stub/User.php new file mode 100644 index 0000000..8884ad2 --- /dev/null +++ b/tests/stub/User.php @@ -0,0 +1,36 @@ + Date: Sun, 8 Jun 2025 10:32:54 -0400 Subject: [PATCH 2/5] fix: Improve type inference for user properties in `UserPropertiesClassReflectionExtension` and update `User` class method signatures to remove type hints. --- ...UserPropertiesClassReflectionExtension.php | 26 +++++++++++++++++-- tests/stub/User.php | 10 +++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/reflection/UserPropertiesClassReflectionExtension.php b/src/reflection/UserPropertiesClassReflectionExtension.php index 5cfd7e7..549808e 100644 --- a/src/reflection/UserPropertiesClassReflectionExtension.php +++ b/src/reflection/UserPropertiesClassReflectionExtension.php @@ -13,7 +13,13 @@ }; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\Dummy\DummyPropertyReflection; -use PHPStan\Type\ObjectType; +use PHPStan\Type\{ + BooleanType, + IntegerType, + ObjectType, + StringType, + TypeCombinator, +}; use yii\web\User; use yii2\extensions\phpstan\ServiceMap; @@ -79,7 +85,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa if (in_array($propertyName, ['id', 'identity', 'isGuest'], true) === true) { $identityClass = $this->getIdentityClass(); - if ($identityClass !== null) { + if ($propertyName === 'identity' && $identityClass !== null) { return new ComponentPropertyReflection( new DummyPropertyReflection($propertyName), new ObjectType($identityClass), @@ -87,6 +93,22 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa ); } + if ($propertyName === 'id') { + return new ComponentPropertyReflection( + new DummyPropertyReflection($propertyName), + TypeCombinator::union(new IntegerType(), new StringType()), + $classReflection, + ); + } + + if ($propertyName === 'isGuest') { + return new ComponentPropertyReflection( + new DummyPropertyReflection($propertyName), + new BooleanType(), + $classReflection, + ); + } + if (($componentClass = $this->serviceMap->getComponentClassById($propertyName)) !== null) { return new ComponentPropertyReflection( new DummyPropertyReflection($propertyName), diff --git a/tests/stub/User.php b/tests/stub/User.php index 8884ad2..e0db8c8 100644 --- a/tests/stub/User.php +++ b/tests/stub/User.php @@ -9,27 +9,27 @@ class User extends ActiveRecord implements IdentityInterface { - public static function findIdentity($id): IdentityInterface + public static function findIdentity($id) { return new static(); } - public static function findIdentityByAccessToken($token, $type = null): IdentityInterface + public static function findIdentityByAccessToken($token, $type = null) { return new static(); } - public function getId(): int + public function getId() { return 1; } - public function getAuthKey(): string + public function getAuthKey() { return ''; } - public function validateAuthKey($authKey): bool + public function validateAuthKey($authKey) { return true; } From e9268100b8165bd80ff5604ec572f9741da73832 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Sun, 8 Jun 2025 10:39:33 -0400 Subject: [PATCH 3/5] fix: Update type handling in `UserPropertiesClassReflectionExtension` to use `ConstantBooleanType` for 'isGuest' property. --- .../UserPropertiesClassReflectionExtension.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/reflection/UserPropertiesClassReflectionExtension.php b/src/reflection/UserPropertiesClassReflectionExtension.php index 549808e..19d8a4a 100644 --- a/src/reflection/UserPropertiesClassReflectionExtension.php +++ b/src/reflection/UserPropertiesClassReflectionExtension.php @@ -13,13 +13,8 @@ }; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\Dummy\DummyPropertyReflection; -use PHPStan\Type\{ - BooleanType, - IntegerType, - ObjectType, - StringType, - TypeCombinator, -}; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\{IntegerType, NullType, ObjectType, StringType, TypeCombinator}; use yii\web\User; use yii2\extensions\phpstan\ServiceMap; @@ -96,7 +91,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa if ($propertyName === 'id') { return new ComponentPropertyReflection( new DummyPropertyReflection($propertyName), - TypeCombinator::union(new IntegerType(), new StringType()), + TypeCombinator::union(new IntegerType(), new StringType(), new NullType()), $classReflection, ); } @@ -104,7 +99,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa if ($propertyName === 'isGuest') { return new ComponentPropertyReflection( new DummyPropertyReflection($propertyName), - new BooleanType(), + new ConstantBooleanType(true), $classReflection, ); } From d5cf4137fdaa1b795b34f7ca9008cee85a641bfb Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Sun, 8 Jun 2025 10:41:19 -0400 Subject: [PATCH 4/5] fix: Refactor conditional structure in `UserPropertiesClassReflectionExtension` for improved readability and maintainability. --- .../UserPropertiesClassReflectionExtension.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/reflection/UserPropertiesClassReflectionExtension.php b/src/reflection/UserPropertiesClassReflectionExtension.php index 19d8a4a..aabf241 100644 --- a/src/reflection/UserPropertiesClassReflectionExtension.php +++ b/src/reflection/UserPropertiesClassReflectionExtension.php @@ -103,14 +103,14 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa $classReflection, ); } + } - if (($componentClass = $this->serviceMap->getComponentClassById($propertyName)) !== null) { - return new ComponentPropertyReflection( - new DummyPropertyReflection($propertyName), - new ObjectType($componentClass), - $classReflection, - ); - } + if (($componentClass = $this->serviceMap->getComponentClassById($propertyName)) !== null) { + return new ComponentPropertyReflection( + new DummyPropertyReflection($propertyName), + new ObjectType($componentClass), + $classReflection, + ); } if ($classReflection->hasNativeProperty($propertyName)) { From 9fa35ae8e880e3740c051352dd91b742dc9f2f23 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Sun, 8 Jun 2025 10:43:45 -0400 Subject: [PATCH 5/5] fix: Optimize property existence check in `UserPropertiesClassReflectionExtension` for improved clarity and performance. --- src/reflection/UserPropertiesClassReflectionExtension.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reflection/UserPropertiesClassReflectionExtension.php b/src/reflection/UserPropertiesClassReflectionExtension.php index aabf241..ba08feb 100644 --- a/src/reflection/UserPropertiesClassReflectionExtension.php +++ b/src/reflection/UserPropertiesClassReflectionExtension.php @@ -140,9 +140,9 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa return false; } - return - $this->getIdentityClass() !== null || - $this->serviceMap->getComponentClassById($propertyName) !== null; + return in_array($propertyName, ['id', 'identity', 'isGuest'], true) + ? $this->getIdentityClass() !== null + : $this->serviceMap->getComponentClassById($propertyName) !== null; } /**