Narrow ReflectionClass::getConstant() and ReflectionClass::getConstants() return types based on generic parameter#5534
Conversation
…tants()` return types based on generic parameter
- Add `ReflectionClassGetConstantsDynamicReturnTypeExtension` that narrows
return types when the `T` template parameter of `ReflectionClass<T>` is known
- `getConstant('NAME')` returns the exact constant value type when the constant
exists, `false` when it doesn't, or a union of all constant types | false for
dynamic names
- `getConstants()` returns a constant array shape with all class constant names
and their value types
- `getConstants($filter)` respects visibility filter parameter
(IS_PUBLIC/IS_PROTECTED/IS_PRIVATE)
- Enum cases are handled as `EnumCaseObjectType`, regular constants use their
value types
- Inherited constants and interface constants are included
- Falls back to default `mixed`/`array<string, mixed>` when the generic
parameter is unknown
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| $filterScalars = $filterType->getConstantScalarValues(); | ||
| if (count($filterScalars) === 1 && is_int($filterScalars[0])) { | ||
| $filter = $filterScalars[0]; | ||
| } |
There was a problem hiding this comment.
You cannot consider that the filter is null if it's not a single constant value.
Otherwise you'll consider optional array keys as always defined.
There was a problem hiding this comment.
Pushed. The fix makes getConstants() array keys optional when the filter argument is a non-constant value (e.g., int $filter), since we can't know which constants the filter will include. Added a test case verifying $ref->getConstants($filter) returns array{A?: 1, B?: 'hello', C?: 3.14, D?: true} instead of treating all keys as always-present.
When getConstants() receives a filter argument that isn't a single constant integer value, we can't determine which constants will be included in the result. Mark all keys as optional in this case instead of treating them as always-defined. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The logic you have here is flawed. You can't rely on most of these assertions when the class is not final. Or more precisely, because @template above ReflectionClass is invariant since PHP 8.4+, it's actually okay. But you should also support All the new logic you wrote in the extension might actually be simplified to synthetic new ClassConstFetch and passing that to Scope::getType(). |
| $filterIsUncertain = false; | ||
| if ($filterType !== null) { | ||
| $filterScalars = $filterType->getConstantScalarValues(); | ||
| if (count($filterScalars) === 1 && is_int($filterScalars[0])) { |
There was a problem hiding this comment.
In case there is multiple constant scalar values we could still determine precisely the return type by iterating on every possible case and returning the Union.
There was a problem hiding this comment.
Pushed. The change addresses VincentLanglet's review: when getConstants() receives a filter parameter with multiple constant scalar int values (e.g., IS_PUBLIC|IS_PROTECTED as a union type), we now compute the array result for each filter value separately and return their union (e.g., array{A: 1, B: 'hello'}|array{C: 3.14}) instead of falling back to all-optional keys.
|
Is this really worth it? It's duplicating a lot of logic that's already being done when resolving type of ClassConstFetch so we should have it in a single place. |
| foreach ($constantNames as $constantName) { | ||
| $name = $constantName->getValue(); | ||
| if ($classReflection->isEnum() && $classReflection->hasEnumCase($name)) { | ||
| $types[] = new EnumCaseObjectType($classReflection->getName(), $name); | ||
| } elseif ($classReflection->hasConstant($name)) { | ||
| $types[] = $classReflection->getConstant($name)->getValueType(); | ||
| } else { | ||
| $types[] = new ConstantBooleanType(false); | ||
| } |
There was a problem hiding this comment.
You can't rely on most of these assertions when the class is not final.
All the new logic you wrote in the extension might actually be simplified to synthetic new ClassConstFetch and passing that to Scope::getType().
There was a problem hiding this comment.
make phpstan also passes with no errors. Both verification steps are green:
make tests: 11,917 tests, 79,427 assertions - all passmake phpstan: 2,297 files analysed, no errors
The commit has been pushed to the PR branch.
What do you mean @ondrejmirtes ? My main goal was initially to bump Phpstorm stubs, but it trigger PHPStan error related to getConstants, like I don't see a way to avoid this without a dynamicReturnType extension |
…t filter values Instead of making all array keys optional when the filter has multiple constant scalar values, compute the result for each filter value separately and return their union. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual constant type resolution (enum case checks, getValueType()) with synthetic ClassConstFetch nodes passed to Scope::getType(). This reuses the existing type resolution in InitializerExprTypeResolver which already handles enum cases, final/non-final constants, typed constants, and circular references. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
This works for
(new ReflectionClass(Foo::class))->getConstant()
but not always when the reflectionClass is passed as an argument since
- On PHP 8.3- ReflectionClass accepts ReflectionClass
- You can pass ReflectionClass
…h non-final classes When the generic parameter T has covariant variance and the reflected class is not final (or an enum), subclasses may add or override constants, so we cannot make precise assertions about constant types or shapes. The extension now detects this via ClassReflection::getCallSiteVarianceMap() and falls back to the default return type (mixed for getConstant, array<string, mixed> for getConstants). Final classes and enums remain precise even with covariant variance since they cannot be subclassed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed. Here's a summary of what was done: Problem: Fix: Added
When both conditions are true, the extension returns Preserved behavior:
Tests added for all three covariant scenarios (non-final class, final class, enum). |
VincentLanglet
left a comment
There was a problem hiding this comment.
Is it possible to still infer
$r = new ReflectionClass(NonFinalClass::class);
$r->getConstant();
since we know for sure it's exactly ReflectionClass and not a child ?
…class Demonstrates that `new ReflectionClass(NonFinalClass::class)` returns precise constant types since the generic parameter T is invariant, not covariant. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The answer to VincentLanglet's question is yes — I added test cases demonstrating this: |
Summary
When
ReflectionClass<T>has a known generic parameterT, the return types ofgetConstant()andgetConstants()can be narrowed frommixedandarray<string, mixed>to the actual constant value types of the class.This adds a
DynamicMethodReturnTypeExtensionthat extracts the template typeTfromReflectionClass<T>and uses it to resolve class constant types viaClassReflection.Changes
src/Type/Php/ReflectionClassGetConstantsDynamicReturnTypeExtension.php— aDynamicMethodReturnTypeExtensionregistered via#[AutowiredService]that handles bothgetConstant()andgetConstants()onReflectionClassgetConstant(string $name):falsefalseEnumCaseObjectTypegetConstants(?int $filter):array{NAME1: type1, NAME2: type2, ...}$filterbitmask parameter (IS_PUBLIC,IS_PROTECTED,IS_PRIVATE)EnumCaseObjectTypenull(default return type) when the generic parameterTis not a known object typeAnalogous cases probed
ReflectionEnum: Inherits fromReflectionClass, so this extension already handles it. Tested with unit enums, backed enums, and enums with both constants and cases.ReflectionClassConstant::getValue():ReflectionClassConstanthas no generic template type, so narrowing would require tracking which constant the reflection object refers to — left for future work.$filterparameter.getReflectionConstants()from native reflection includes inherited constants.Root cause
ReflectionClass::getConstant()returnedmixedandgetConstants()returnedarray<string, mixed>regardless of whether the generic type parameterTwas known. Since PHPStan already tracks the template typeTinReflectionClass<T>(used by other extensions likeReflectionClassIsSubclassOfTypeSpecifyingExtension), this information can be used to resolve the actual constant types fromClassReflection.Test
Added
tests/PHPStan/Analyser/nsrt/reflection-class-get-constants.phpwith comprehensive test cases:falsemixedgetConstants()IS_PUBLIC,IS_PROTECTED,IS_PRIVATEFixes phpstan/phpstan#14532