Respect / Respect/Validation

Ship a PHPStan extension for type narrowing via assert()/check()

Open
#1,681 2 comments 1 reaction 0 assignees View on GitHub

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" is null|'' in this library, which doesn't map cleanly to a PHPStan type.

Implementation approach

Use MethodTypeSpecifyingExtension + TypeSpecifierAwareExtension:

  1. Walk up the MethodCall/StaticCall chain from assert()/check() to find a type-checking method
  2. Create the corresponding AST expression (Instanceof_, FuncCall('is_string'), etc.) or construct a PHPStan Type directly (for all* which needs ArrayType)
  3. 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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.