-
Notifications
You must be signed in to change notification settings - Fork 775
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 FooApplies 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|nullnot* 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.