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 existence
  • Parameter #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
  • instanceof check 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. 1.Use @phpstan-assert to teach PHPStan about custom assertions:
  2. 2.```php
  3. 3.class UserValidator {
  4. 4./**
  5. 5.* @phpstan-assert !null $user
  6. 6.* @phpstan-assert User $user
  7. 7.*/
  8. 8.public function ensureUser(?User $user): void {
  9. 9.if ($user === null) {
  10. 10.throw new InvalidArgumentException('User is required');
  11. 11.}
  12. 12.}
  13. 13.}

// Now PHPStan knows $user is User after this call $validator->ensureUser($user); echo $user->getEmail(); // No error - PHPStan narrowed the type ```

  1. 1.Use explicit type annotations for complex narrowing:
  2. 2.```php
  3. 3./** @var User $user */ // Tell PHPStan explicitly
  4. 4.$user = $this->findUser($id);

// Or use assert() - PHPStan understands native assert() assert($user instanceof User); echo $user->getEmail(); // OK ```

  1. 1.Handle closure type narrowing:
  2. 2.```php
  3. 3.// WRONG - PHPStan loses type inside closure
  4. 4.function process(?User $user): void {
  5. 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. 1.Configure PHPStan baseline for known false positives:
  2. 2.```bash
  3. 3.# Generate baseline for existing false positives
  4. 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. 1.Use @phpstan-if and @phpstan-assert-if-true:
  2. 2.```php
  3. 3.class Repository {
  4. 4./**
  5. 5.* @phpstan-assert-if-true User $subject
  6. 6.*/
  7. 7.public function isValidUser(mixed $subject): bool {
  8. 8.return $subject instanceof User && $subject->isActive();
  9. 9.}
  10. 10.}

$repo = new Repository(); if ($repo->isValidUser($user)) { echo $user->getEmail(); // OK - PHPStan narrowed via assert-if-true } ```

Prevention

  • Use @phpstan-assert annotations 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-rules for more rigorous checking
  • Write type-specific tests that exercise narrowed code paths