Introduction

PHPStan performs static analysis on PHP code, detecting type errors, impossible conditions, and strict comparison issues that PHP itself does not catch at runtime. When PHPStan reaches higher analysis levels (7-9), it reports errors like Strict comparison using === between X and Y will always evaluate to false, Cannot call method on mixed, and Parameter #1 expects string, int given. These errors indicate genuine type safety issues in the codebase that could cause runtime errors or unexpected behavior. Understanding PHPStan's type inference and learning to properly narrow types is essential for achieving high analysis levels.

Symptoms

bash
Strict comparison using === between 'active'|'inactive' and 'pending' will always evaluate to false.

Or:

bash
Cannot call method process() on mixed.

Or:

bash
Parameter #1 $data of method Processor::process() expects array, mixed given.

Common Causes

  • Impossible comparison: Comparing values that PHPStan's type inference knows cannot match
  • Mixed type not narrowed: Function returns mixed but code does not verify actual type
  • Missing type declarations: No return types or parameter types for PHPStan to analyze
  • Dead code: Conditions that can never be true based on type information
  • Incorrect PHPDoc types: PHPDoc does not match actual runtime types
  • Generic type parameters missing: Missing type arguments for collection classes

Step-by-Step Fix

Step 1: Use type guards and narrowing

```php /** * @param mixed $value */ function processValue($value): string { // Narrow mixed to specific types if (is_string($value)) { return strtoupper($value); // PHPStan knows $value is string here }

if (is_int($value)) { return (string) $value; // PHPStan knows $value is int here }

if (is_array($value)) { return json_encode($value); // PHPStan knows $value is array here }

throw new InvalidArgumentException('Unsupported type'); } ```

Step 2: Use assert for type narrowing

```php class UserService { public function getUser(mixed $id): ?User { // Assert narrows type for PHPStan analysis if (!is_int($id) && !is_string($id)) { throw new InvalidArgumentException('ID must be int or string'); }

// PHPStan now knows $id is int|string return $this->repository->findById($id); }

public function processEntity(object $entity): void { // instanceof narrows type if ($entity instanceof User) { $entity->getName(); // PHPStan knows this is safe } elseif ($entity instanceof Order) { $entity->getTotal(); // PHPStan knows this is safe } } } ```

Step 3: Configure PHPStan levels gradually

```neon # phpstan.neon parameters: level: 8 # Start at 8, aim for 9 paths: - src/ excludePaths: - src/legacy/* # Exclude legacy code initially checkMissingIterableValueType: false # Relax for gradual adoption reportMaybesInMethodSignatures: true

# Add baseline for existing issues reportUnmatchedIgnoredErrors: false ```

```bash # Generate baseline for existing errors vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon

# Gradually reduce baseline as you fix issues vendor/bin/phpstan analyse ```

Prevention

  • Add proper type declarations to all function parameters and return types
  • Use @phpstan-param and @phpstan-return for complex types PHP cannot express
  • Narrow mixed types with is_* checks before using the value
  • Use instanceof to narrow object types in polymorphic code
  • Run PHPStan in CI at the highest level your codebase supports
  • Use baseline files to adopt PHPStan incrementally without blocking CI
  • Enable strict_types=1 in all PHP files for consistent type behavior