Introduction
PHPStan performs static type analysis and attempts to narrow union types based on runtime checks like instanceof, is_string(), and assertions. However, in complex code paths, PHPStan may not recognize that a type has been narrowed, reporting errors like Cannot call method X on type Y|null even after a null check. These false positives can be frustrating and may lead developers to suppress legitimate warnings.
Symptoms
Cannot call method getName() on App\Model|undefined.after already checking existenceParameter #1 $user of function process expects App\User, App\User|null given.after null check- PHPStan level 7+ errors on code that is actually type-safe
instanceofcheck not narrowing the type in a different scope- Errors in callback functions that use outer-scope narrowed variables
``` Line src/Service/UserService.php ------ --------------------------------------------------------------- 45 Cannot call method getEmail() on App\Entity\User|null. 52 Parameter #1 $email of method App\Mailer::send() expects string, string|null given.
// But the code has: // if ($user === null) { throw new Exception('User not found'); } // $user->getEmail(); // PHPStan still thinks it could be null ```
Common Causes
- Type narrowing not recognized across function boundaries
- PHPStan not understanding custom assertion methods
- Complex conditional logic confusing the type inference engine
- Closures/callbacks not inheriting narrowed types from outer scope
- Magic methods and dynamic properties not understood by PHPStan
Step-by-Step Fix
- 1.Use @phpstan-assert to teach PHPStan about custom assertions:
- 2.```php
- 3.class UserValidator {
- 4./**
- 5.* @phpstan-assert !null $user
- 6.* @phpstan-assert User $user
- 7.*/
- 8.public function ensureUser(?User $user): void {
- 9.if ($user === null) {
- 10.throw new InvalidArgumentException('User is required');
- 11.}
- 12.}
- 13.}
// Now PHPStan knows $user is User after this call $validator->ensureUser($user); echo $user->getEmail(); // No error - PHPStan narrowed the type ```
- 1.Use explicit type annotations for complex narrowing:
- 2.```php
- 3./** @var User $user */ // Tell PHPStan explicitly
- 4.$user = $this->findUser($id);
// Or use assert() - PHPStan understands native assert() assert($user instanceof User); echo $user->getEmail(); // OK ```
- 1.Handle closure type narrowing:
- 2.```php
- 3.// WRONG - PHPStan loses type inside closure
- 4.function process(?User $user): void {
- 5.if ($user === null) return;
array_map(function() use ($user) { echo $user->getEmail(); // Error: User|null }, [1, 2, 3]); }
// CORRECT - narrow before closure function process(?User $user): void { if ($user === null) return;
$activeUser = $user; // Assign to new variable array_map(function() use ($activeUser) { echo $activeUser->getEmail(); // OK }, [1, 2, 3]); } ```
- 1.Configure PHPStan baseline for known false positives:
- 2.```bash
- 3.# Generate baseline for existing false positives
- 4.vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon
# This creates phpstan-baseline.neon with ignored errors # Include it in phpstan.neon: # includes: # - phpstan-baseline.neon
# Fix false positives over time, don't let baseline grow indefinitely ```
- 1.Use @phpstan-if and @phpstan-assert-if-true:
- 2.```php
- 3.class Repository {
- 4./**
- 5.* @phpstan-assert-if-true User $subject
- 6.*/
- 7.public function isValidUser(mixed $subject): bool {
- 8.return $subject instanceof User && $subject->isActive();
- 9.}
- 10.}
$repo = new Repository(); if ($repo->isValidUser($user)) { echo $user->getEmail(); // OK - PHPStan narrowed via assert-if-true } ```
Prevention
- Use
@phpstan-assertannotations on custom validation methods - Prefer early returns over nested conditionals for clearer type narrowing
- Use
assert()for runtime type checks that PHPStan understands - Configure PHPStan at level 5 first, gradually increase to level 8
- Keep the baseline file small and fix false positives regularly
- Use
phpstan/phpstan-strict-rulesfor more rigorous checking - Write type-specific tests that exercise narrowed code paths