Ship a PHPStan extension for type narrowing via assert()/check()
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 6k
- Forks
- 773
- Avg merge
- 10h 6m
- Merged PRs (30d)
- 17
Description
After calling assert() or check() on a validation chain, PHPStan should be able to narrow the type of the asserted value. Currently, PHPStan cannot do this because the method chain goes through @mixin + __callStatic/__call, which prevents PHPStan from tracking type information across the call chain.
Shipping a bundled MethodTypeSpecifyingExtension (auto-discovered via phpstan/extension-installer) would let PHPStan understand that passing validation implies a type guarantee — the same way phpstan-phpunit handles assertInstanceOf(), assertIsString(), etc.
Type narrowing by prefix
Validation has a consistent method naming pattern with prefixes (all*, nullOr*, not*, key*, property*) applied to base validators. Each prefix modifies the semantics and could produce different PHPStan type narrowing:
Base type validators
The core *Type() and instance() methods narrow to a single type:
ValidatorBuilder::stringType()->assert($input); // $input is string
ValidatorBuilder::arrayType()->assert($input); // $input is array
ValidatorBuilder::objectType()->assert($input); // $input is object
ValidatorBuilder::instance(Foo::class)->assert($input); // $input is Foo
Applies to: arrayType, boolType, callableType, floatType, intType, iterableType, nullType, objectType, resourceType, stringType, instance.
all* prefix — array element narrowing
Narrows the input to array<T>:
ValidatorBuilder::allStringType()->assert($input); // $input is array<string>
ValidatorBuilder::allInstance(Foo::class)->assert($input); // $input is array<Foo>
ValidatorBuilder::allBoolType()->check($input); // $input is array<bool>
nullOr* prefix — nullable narrowing
Narrows the input to T|null:
ValidatorBuilder::nullOrStringType()->assert($input); // $input is string|null
ValidatorBuilder::nullOrInstance(Foo::class)->assert($input); // $input is Foo|null
ValidatorBuilder::nullOrObjectType()->assert($input); // $input is object|null
not* prefix — negative narrowing
Excludes a type from the input (useful when the input is already a union):
/** @param int|string $input */
function example(int|string $input): string {
ValidatorBuilder::notIntType()->assert($input);
return $input; // $input is string
}
Chained calls
The extension should walk up the method chain to find the relevant type-checking method, regardless of intermediate validators:
ValidatorBuilder::positive()->intType()->assert($input);
ValidatorBuilder::allStringType()->unique()->assert($input);
ValidatorBuilder::objectType()->instance(Foo::class)->assert($input);
Both assert() and check() should be supported since both throw on failure.
Out of scope (for now)
key*Type/property*Type: Narrowing types at specific array keys or object properties (e.g.,keyStringType('name')) would require PHPStan's offset/property type system and is significantly more complex.length*,min*,max*: These validate numeric ranges/lengths, not types.undefOr*: "Undef" isnull|''in this library, which doesn't map cleanly to a PHPStan type.
Implementation approach
Use MethodTypeSpecifyingExtension + TypeSpecifierAwareExtension:
- Walk up the
MethodCall/StaticCallchain fromassert()/check()to find a type-checking method - Create the corresponding AST expression (
Instanceof_,FuncCall('is_string'), etc.) or construct a PHPStanTypedirectly (forall*which needsArrayType) - Pass to
TypeSpecifier::specifyTypesInCondition()orTypeSpecifier::create()to narrow the type
Ship as extension.neon at the package root, auto-discovered via extra.phpstan.includes in composer.json.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the MethodTypeSpecifyingExtension and TypeSpecifierAwareExtension approach described in the issue, then inspect the validation method chain and the package's composer.json. Define the bundled extension in extension.neon and configure auto-discovery through extra.phpstan.includes. Done means assert() and check() narrow base, all*, nullOr*, and not* validators, including chained calls, while the listed key/property and range validators remain out of scope.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- devtools, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100