Skip to content

PHPStan

Overview

PHPStan is a powerful static analysis tool for PHP that finds bugs in your code without running it. It catches whole classes of bugs even before you write tests for the code, helping maintain code quality and preventing runtime errors.


Why PHPStan?

PHPStan provides several benefits:

  • Early bug detection - Finds bugs before runtime
  • Type safety - Ensures type consistency across your codebase
  • Zero configuration - Works out of the box with sensible defaults
  • Fast feedback - Analyzes code in seconds
  • IDE integration - Works with PHPStorm, VS Code, and other editors
  • Framework support - Excellent Laravel support via Larastan

Installation

Install PHPStan with Larastan

bash
# Install PHPStan and Larastan (Laravel extension)
composer require --dev phpstan/phpstan larastan/larastan

# Larastan provides Laravel-specific rules and better understanding
# of Laravel's magic methods, facades, and helpers

Configuration

Create a phpstan.neon file in your project root:

neon
includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    paths:
        - app
        - config
        - database
        - routes

    # Rule level (0-9, higher is stricter)
    level: 5

    # Exclude paths from analysis
    excludePaths:
        - app/Providers/TelescopeServiceProvider.php
        - database/migrations/*

    # Check missing type hints
    checkMissingIterableValueType: false
    checkGenericClassInNonGenericObjectType: false

    # Laravel specific
    treatPhpDocTypesAsCertain: false

Rule Levels

PHPStan has 10 rule levels (0-9):

Level 0

  • Basic checks
  • Unknown classes
  • Unknown functions
  • Unknown methods on $this
  • Wrong number of arguments

Level 1

  • Possibly undefined variables
  • Unknown magic methods and properties

Level 2

  • Unknown methods on all expressions
  • Validating PHPDocs

Level 3

  • Return types
  • Types assigned to properties

Level 4

  • Basic dead code checking
  • Always defined variables

Level 5

  • Checking argument types

Level 6

  • Report missing typehints

Level 7

  • Report partially wrong union types

Level 8

  • Report calling methods on nullable types

Level 9 (Max)

  • Mixed types
  • Strictest checks

Recommendation: Start with level 5, gradually increase to 8.


Running PHPStan

Basic Usage

bash
# Run PHPStan on configured paths
./vendor/bin/phpstan analyse

# Run on specific path
./vendor/bin/phpstan analyse app/Services

# Run with specific level
./vendor/bin/phpstan analyse --level=6

# Generate baseline (ignore existing errors)
./vendor/bin/phpstan analyse --generate-baseline

# Run without baseline
./vendor/bin/phpstan analyse --no-baseline

# Clear result cache
./vendor/bin/phpstan clear-result-cache

Memory Limit

For large projects:

bash
# Increase memory limit
./vendor/bin/phpstan analyse --memory-limit=2G

Common Issues and Fixes

Issue: Property Access on Nullable Type

php
// ❌ PHPStan error: Cannot access property on nullable type
$user = User::find($id);
$name = $user->name; // User might be null

// ✅ Fix with null check
$user = User::find($id);
if ($user !== null) {
    $name = $user->name;
}

// ✅ Or use findOrFail
$user = User::findOrFail($id);
$name = $user->name;

// ✅ Or use optional helper
$name = User::find($id)?->name;

Issue: Missing Return Type

php
// ❌ PHPStan error: Method has no return type
public function getUser($id) {
    return User::find($id);
}

// ✅ Add return type
public function getUser(int $id): ?User {
    return User::find($id);
}

Issue: Parameter Type Mismatch

php
// ❌ PHPStan error: Parameter type mismatch
public function createPost(array $data): Post {
    return Post::create($data['title']); // Wrong type
}

// ✅ Fix parameter usage
public function createPost(array $data): Post {
    return Post::create($data); // Correct
}

Issue: Unknown Property on Model

php
// ❌ PHPStan error: Access to undefined property
$user->custom_field;

// ✅ Add PHPDoc to model
/**
 * @property string $custom_field
 */
class User extends Model {
    // ...
}

Type Hints and PHPDocs

Basic Type Hints

php
<?php

// Scalar types
function processData(string $name, int $age, bool $active): void {
    // ...
}

// Return types
function getUser(int $id): ?User {
    return User::find($id);
}

// Array types (use PHPDoc for specific structure)
/**
 * @param array<string, mixed> $data
 * @return array<int, User>
 */
function processUsers(array $data): array {
    // ...
}

Collections

php
<?php

use Illuminate\Database\Eloquent\Collection;

/**
 * @return Collection<int, User>
 */
function getActiveUsers(): Collection {
    return User::where('active', true)->get();
}

/**
 * @param Collection<int, Post> $posts
 */
function processPosts(Collection $posts): void {
    // ...
}

Generic Types

php
<?php

/**
 * @template T
 * @param class-string<T> $class
 * @return T
 */
function make(string $class) {
    return new $class();
}

$user = make(User::class); // PHPStan knows this is User

Laravel-Specific Patterns

Facades

php
<?php

use Illuminate\Support\Facades\Cache;

// PHPStan understands Laravel facades with Larastan
$value = Cache::get('key'); // Type is inferred correctly

Eloquent Relationships

php
<?php

class User extends Model {
    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany<Post>
     */
    public function posts() {
        return $this->hasMany(Post::class);
    }
}

// PHPStan knows $user->posts is Collection<Post>
$posts = $user->posts;

Request Validation

php
<?php

use Illuminate\Http\Request;

class PostController extends Controller {
    public function store(Request $request) {
        $validated = $request->validate([
            'title' => 'required|string',
            'content' => 'required|string',
        ]);

        // PHPStan knows $validated is array
        Post::create($validated);
    }
}

Advanced Configuration

Ignoring Errors

neon
parameters:
    ignoreErrors:
        # Ignore specific error message
        - '#Call to an undefined method App\\Models\\User::customMethod\(\)#'

        # Ignore errors in specific path
        -
            message: '#Unsafe usage of new static\(\)#'
            path: app/Models/BaseModel.php

        # Ignore by identifier
        -
            identifier: argument.type
            path: app/Legacy/*

Custom Rules

neon
parameters:
    # Require strict comparison
    checkAlwaysTrueCheckTypeFunctionCall: true
    checkAlwaysTrueInstanceof: true
    checkAlwaysTrueStrictComparison: true

    # Type coverage
    checkMissingCallableSignature: true
    checkUninitializedProperties: true

    # Bleeding edge features
    polluteScopeWithLoopInitialAssignments: false
    polluteScopeWithAlwaysIterableForeach: false

CI/CD Integration

GitHub Actions

yaml
name: PHPStan

on: [push, pull_request]

jobs:
  phpstan:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.2
          coverage: none

      - name: Install dependencies
        run: composer install --prefer-dist --no-interaction

      - name: Run PHPStan
        run: ./vendor/bin/phpstan analyse --error-format=github

GitLab CI

yaml
phpstan:
  stage: test
  script:
    - composer install --prefer-dist --no-interaction
    - ./vendor/bin/phpstan analyse --no-progress
  only:
    - merge_requests
    - main

Baseline Management

Creating a Baseline

When introducing PHPStan to an existing project:

bash
# Generate baseline of current errors
./vendor/bin/phpstan analyse --generate-baseline

# This creates phpstan-baseline.neon
# Add to phpstan.neon:
neon
includes:
    - phpstan-baseline.neon

Updating Baseline

bash
# Regenerate baseline after fixes
./vendor/bin/phpstan analyse --generate-baseline

# Run without baseline to see all errors
./vendor/bin/phpstan analyse --no-baseline

IDE Integration

PHPStorm

  1. Go to Settings → PHP → Quality Tools → PHPStan
  2. Set PHPStan path: vendor/bin/phpstan
  3. Set configuration file: phpstan.neon
  4. Enable "Show inspection results inline"

VS Code

Install the PHPStan extension:

json
{
    "phpstan.enabled": true,
    "phpstan.configFile": "phpstan.neon",
    "phpstan.path": "vendor/bin/phpstan"
}

Best Practices

1. Start Early

php
// Add PHPStan to new projects from the start
// Easier than retrofitting to legacy code

2. Use Strict Types

php
<?php

declare(strict_types=1);

// Enables strict type checking for the file

3. Type Everything

php
<?php

// ✅ Good - Full type coverage
public function createUser(string $name, string $email): User {
    return User::create([
        'name' => $name,
        'email' => $email,
    ]);
}

// ❌ Bad - No types
public function createUser($name, $email) {
    return User::create([
        'name' => $name,
        'email' => $email,
    ]);
}

4. Document Complex Types

php
<?php

/**
 * @param array{name: string, email: string, age: int} $data
 * @return array{user: User, token: string}
 */
public function register(array $data): array {
    // ...
}

5. Gradually Increase Level

bash
# Start at level 5
level: 5

# Fix all errors, then increase
level: 6

# Continue until level 8
level: 8

Common Patterns

Service Classes

php
<?php

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Collection;

class UserService
{
    /**
     * @param array{name: string, email: string} $data
     */
    public function createUser(array $data): User
    {
        return User::create($data);
    }

    /**
     * @return Collection<int, User>
     */
    public function getActiveUsers(): Collection
    {
        return User::where('active', true)->get();
    }

    public function findUser(int $id): ?User
    {
        return User::find($id);
    }
}

DTOs (Data Transfer Objects)

php
<?php

namespace App\DTO;

class CreateUserDTO
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly ?string $phone = null,
    ) {}

    /**
     * @param array{name: string, email: string, phone?: string} $data
     */
    public static function fromArray(array $data): self
    {
        return new self(
            name: $data['name'],
            email: $data['email'],
            phone: $data['phone'] ?? null,
        );
    }
}

Troubleshooting

Cache Issues

bash
# Clear PHPStan cache
./vendor/bin/phpstan clear-result-cache

# Clear Laravel cache
php artisan cache:clear
php artisan config:clear

Memory Issues

neon
parameters:
    # Increase memory in config
    tmpDir: var/phpstan
bash
# Or via command line
./vendor/bin/phpstan analyse --memory-limit=2G

False Positives

php
<?php

// Use PHPStan ignore comment
/** @phpstan-ignore-next-line */
$result = $this->legacyMethod();

// Or inline
$result = $this->legacyMethod(); // @phpstan-ignore-line


Quick Reference

Common Commands

bash
# Run analysis
./vendor/bin/phpstan analyse

# Specific path
./vendor/bin/phpstan analyse app/Services

# With level
./vendor/bin/phpstan analyse --level=8

# Generate baseline
./vendor/bin/phpstan analyse --generate-baseline

# Clear cache
./vendor/bin/phpstan clear-result-cache

Configuration Template

neon
includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    paths:
        - app
    level: 5
    checkMissingIterableValueType: false

Common Type Annotations

php
// Nullable
?User

// Array of users
User[]

// Collection of users
Collection<int, User>

// Array with specific structure
array{name: string, age: int}

// Union types
string|int

// Generic class
@template T

CPR - Clinical Patient Records