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" is null|'' in this library, which doesn't map cleanly to a PHPStan type.
Implementation approach
Use MethodTypeSpecifyingExtension + TypeSpecifierAwareExtension:
- Walk up the
MethodCall/StaticCall chain from assert()/check() to find a type-checking method
- Create the corresponding AST expression (
Instanceof_, FuncCall('is_string'), etc.) or construct a PHPStan Type directly (for all* which needs ArrayType)
- Pass to
TypeSpecifier::specifyTypesInCondition() or TypeSpecifier::create() to narrow the type
Ship as extension.neon at the package root, auto-discovered via extra.phpstan.includes in composer.json.
After calling
assert()orcheck()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 viaphpstan/extension-installer) would let PHPStan understand that passing validation implies a type guarantee — the same wayphpstan-phpunithandlesassertInstanceOf(),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()andinstance()methods narrow to a single type:Applies to:
arrayType,boolType,callableType,floatType,intType,iterableType,nullType,objectType,resourceType,stringType,instance.all*prefix — array element narrowingNarrows the input to
array<T>:nullOr*prefix — nullable narrowingNarrows the input to
T|null:not*prefix — negative narrowingExcludes a type from the input (useful when the input is already a union):
Chained calls
The extension should walk up the method chain to find the relevant type-checking method, regardless of intermediate validators:
Both
assert()andcheck()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:MethodCall/StaticCallchain fromassert()/check()to find a type-checking methodInstanceof_,FuncCall('is_string'), etc.) or construct a PHPStanTypedirectly (forall*which needsArrayType)TypeSpecifier::specifyTypesInCondition()orTypeSpecifier::create()to narrow the typeShip as
extension.neonat the package root, auto-discovered viaextra.phpstan.includesincomposer.json.