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 helpersConfiguration
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: falseRule 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-cacheMemory Limit
For large projects:
bash
# Increase memory limit
./vendor/bin/phpstan analyse --memory-limit=2GCommon 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 UserLaravel-Specific Patterns
Facades
php
<?php
use Illuminate\Support\Facades\Cache;
// PHPStan understands Laravel facades with Larastan
$value = Cache::get('key'); // Type is inferred correctlyEloquent 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: falseCI/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=githubGitLab CI
yaml
phpstan:
stage: test
script:
- composer install --prefer-dist --no-interaction
- ./vendor/bin/phpstan analyse --no-progress
only:
- merge_requests
- mainBaseline 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.neonUpdating Baseline
bash
# Regenerate baseline after fixes
./vendor/bin/phpstan analyse --generate-baseline
# Run without baseline to see all errors
./vendor/bin/phpstan analyse --no-baselineIDE Integration
PHPStorm
- Go to Settings → PHP → Quality Tools → PHPStan
- Set PHPStan path:
vendor/bin/phpstan - Set configuration file:
phpstan.neon - 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 code2. Use Strict Types
php
<?php
declare(strict_types=1);
// Enables strict type checking for the file3. 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: 8Common 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:clearMemory Issues
neon
parameters:
# Increase memory in config
tmpDir: var/phpstanbash
# Or via command line
./vendor/bin/phpstan analyse --memory-limit=2GFalse Positives
php
<?php
// Use PHPStan ignore comment
/** @phpstan-ignore-next-line */
$result = $this->legacyMethod();
// Or inline
$result = $this->legacyMethod(); // @phpstan-ignore-lineRelated Documentation
- Laravel Pint - Code formatting
- Testing Overview - Testing strategy
- Best Practices - Coding standards
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-cacheConfiguration Template
neon
includes:
- vendor/larastan/larastan/extension.neon
parameters:
paths:
- app
level: 5
checkMissingIterableValueType: falseCommon 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