PHPStan - Static Analysis Tool
Find bugs without running code, improve code quality and type safety
Table of Contents
- Introduction
- Installation and Configuration
- Configuration File
- Level Progression Strategy
- Usage
- Common Issues and Solutions
- CI Integration
- Best Practices
Introduction
What is PHPStan?
PHPStan is a static analysis tool for PHP that finds potential bugs, type errors, and logic issues without running the code.
Key Features:
- 10 strictness levels (0-9)
- Type inference and checking
- Dead code detection
- Unused variable detection
- Extensible rule system
- Generics and advanced type support
What Can PHPStan Find?
<?php
declare(strict_types=1);
// PHPStan will find these issues:
// 1. Type errors
function calculateTotal(int $price): int
{
return $price * 1.1; // Returns float, but declared as int
}
// 2. Undefined variables
function processOrder(): void
{
echo $orderId; // $orderId is undefined
}
// 3. Calling non-existent methods
$user = new User();
$user->getName(); // User class has no getName method
// 4. Null pointer exceptions
function getUser(?User $user): string
{
return $user->name; // $user might be null
}
// 5. Non-existent array keys
$data = ['name' => 'John'];
echo $data['email']; // Key 'email' doesn't exist
// 6. Dead code
function example(): void
{
return;
echo 'Never executed'; // Will never execute
}Installation and Configuration
Installation
# Install PHPStan
composer require --dev phpstan/phpstan
# Install extensions (optional)
composer require --dev phpstan/extension-installer
composer require --dev phpstan/phpstan-strict-rules
composer require --dev phpstan/phpstan-deprecation-rules
# Verify installation
./vendor/bin/phpstan --versionRecommended Extensions
| Extension | Description |
|---|---|
phpstan/phpstan-strict-rules | Stricter rules |
phpstan/phpstan-deprecation-rules | Detect deprecated code |
larastan/larastan | Laravel support |
phpstan/phpstan-phpunit | PHPUnit support |
Configure Composer Scripts
Add to composer.json:
{
"scripts": {
"stan": "phpstan analyse --memory-limit=2G",
"stan:baseline": "phpstan analyse --generate-baseline",
"stan:clear": "phpstan clear-result-cache"
},
"scripts-descriptions": {
"stan": "Run PHPStan static analysis",
"stan:baseline": "Generate baseline for existing errors",
"stan:clear": "Clear PHPStan result cache"
}
}Configuration File
phpstan.neon
Create phpstan.neon in the project root:
Basic Configuration
parameters:
level: 5
paths:
- app
excludePaths:
- app/view
- app/support/helper
tmpDir: runtime/cache/phpstan
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: falseComplete Configuration Example
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
parameters:
# Analysis level (0-9)
level: 5
# Paths to analyze
paths:
- app/controller
- app/service
- app/domain
- app/infrastructure
- app/contract
# Excluded paths
excludePaths:
- app/view
- app/support/helper
- */tests/*
# Cache directory
tmpDir: runtime/cache/phpstan
# Ignored error patterns
ignoreErrors:
# Ignore specific error messages
- '#Call to an undefined method Webman\\Config::get\(\)#'
# Ignore errors in specific files
-
message: '#Access to an undefined property#'
path: app/model/eloquent/*
# Ignore third-party library issues
-
message: '#.*#'
path: vendor/*
# Type checking configuration
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
checkAlwaysTrueCheckTypeFunctionCall: true
checkAlwaysTrueInstanceof: true
checkAlwaysTrueStrictComparison: true
checkExplicitMixedMissingReturn: true
checkFunctionNameCase: true
checkInternalClassCaseSensitivity: true
# Report unused variables and parameters
reportUnmatchedIgnoredErrors: true
checkMissingCallableSignature: true
checkUninitializedProperties: true
# Autoloading
bootstrapFiles:
- vendor/autoload.php
# Scan files
scanFiles:
- support/helpers.php
# Scan directories
scanDirectories:
- appConfiguration Explanation
Level Description
| Level | Checks |
|---|---|
| 0 | Basic: unknown classes, functions |
| 1 | Unknown methods, properties |
| 2 | Unknown magic methods, properties |
| 3 | Return type checks |
| 4 | Dead code detection |
| 5 | Parameter type checks |
| 6 | Missing type hints |
| 7 | Partial union type checks |
| 8 | Nullable type checks |
| 9 | Mixed type checks (strictest) |
Level Progression Strategy
Recommended Strategy: Start from Level 5
Why start from Level 5?
- Level 0-4: Too lenient, can't find most issues
- Level 5: Balance point, finds most real issues
- Level 6-9: Requires complete type annotations, suitable for mature projects
Progression Roadmap
Phase 1: Level 5 (Recommended Starting Point)
parameters:
level: 5
paths:
- app/domain
- app/serviceGoals:
- Fix all Level 5 errors
- Establish Baseline (see below)
- Team adapts to static analysis
Expected Time: 1-2 weeks
Phase 2: Level 6
parameters:
level: 6New Checks:
- Missing type hints
- Undeclared properties
Actions:
- Add return types to all methods
- Add type hints to all parameters
- Add type declarations to class properties
Expected Time: 2-4 weeks
Phase 3: Level 7
parameters:
level: 7New Checks:
- Partial union type checks
- Stricter type inference
Actions:
- Use union types (
int|string) - Complete PHPDoc annotations
Expected Time: 2-3 weeks
Phase 4: Level 8 (Target Level)
parameters:
level: 8New Checks:
- Nullable type checks
- Strict type safety
Actions:
- Handle all possible null values
- Use
?TypeorType|null
Expected Time: 3-4 weeks
Phase 5: Level 9 (Optional)
parameters:
level: 9New Checks:
- Forbid
mixedtype - Strictest type checking
Applicable Scenarios:
- Core libraries
- Critical business logic
- New projects
Usage
Basic Commands
# Analyze all configured paths
./vendor/bin/phpstan analyse
# Analyze specific directory
./vendor/bin/phpstan analyse app/domain
# Analyze specific file
./vendor/bin/phpstan analyse app/domain/order/entity/Order.php
# Specify level
./vendor/bin/phpstan analyse --level=6
# Generate Baseline
./vendor/bin/phpstan analyse --generate-baseline
# Clear cache
./vendor/bin/phpstan clear-result-cache
# Show verbose output
./vendor/bin/phpstan analyse -v
# Without cache
./vendor/bin/phpstan analyse --no-progress
# Output as JSON
./vendor/bin/phpstan analyse --error-format=jsonUsing Composer Scripts
# Run analysis
composer stan
# Generate Baseline
composer stan:baseline
# Clear cache
composer stan:clearCommon Issues and Solutions
Issue 1: Undefined Methods or Properties
Error Example
<?php
declare(strict_types=1);
namespace app\domain\order\entity;
final class Order
{
private int $id;
private string $status;
public function getId(): int
{
return $this->id;
}
}
// PHPStan error:
// Access to an undefined property app\domain\order\entity\Order::$idSolution 1: Initialize Properties
<?php
declare(strict_types=1);
namespace app\domain\order\entity;
final class Order
{
private int $id = 0;
private string $status = 'pending';
public function getId(): int
{
return $this->id;
}
}Solution 2: Constructor Initialization
<?php
declare(strict_types=1);
namespace app\domain\order\entity;
final class Order
{
public function __construct(
private int $id,
private string $status
) {
}
public function getId(): int
{
return $this->id;
}
}Issue 2: Possibly Null Values
Error Example
<?php
declare(strict_types=1);
namespace app\service\order;
use app\contract\repository\OrderRepositoryInterface;
use app\domain\order\entity\Order;
final class GetOrderService
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository
) {
}
public function handle(int $orderId): string
{
$order = $this->orderRepository->findById($orderId);
// PHPStan error:
// Cannot call method getStatus() on app\domain\order\entity\Order|null
return $order->getStatus();
}
}Solution 1: Throw Exception
<?php
declare(strict_types=1);
namespace app\service\order;
use app\contract\repository\OrderRepositoryInterface;
use app\domain\order\entity\Order;
use app\support\exception\OrderNotFoundException;
final class GetOrderService
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository
) {
}
public function handle(int $orderId): string
{
$order = $this->orderRepository->findById($orderId);
if ($order === null) {
throw new OrderNotFoundException("Order {$orderId} not found");
}
return $order->getStatus();
}
}Solution 2: Use Nullable Return Type
<?php
declare(strict_types=1);
namespace app\service\order;
use app\contract\repository\OrderRepositoryInterface;
final class GetOrderService
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository
) {
}
public function handle(int $orderId): ?string
{
$order = $this->orderRepository->findById($orderId);
return $order?->getStatus();
}
}Issue 3: Unclear Array Types
Error Example
<?php
declare(strict_types=1);
namespace app\service\order;
final class OrderCalculator
{
public function calculateTotal(array $items): float
{
$total = 0.0;
foreach ($items as $item) {
// PHPStan error:
// Cannot access offset 'price' on mixed
$total += $item['price'] * $item['quantity'];
}
return $total;
}
}Solution: Use PHPDoc Annotations
<?php
declare(strict_types=1);
namespace app\service\order;
final class OrderCalculator
{
/**
* @param array<int, array{price: float, quantity: int}> $items
*/
public function calculateTotal(array $items): float
{
$total = 0.0;
foreach ($items as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total;
}
}Issue 4: Third-Party Library Type Issues
Error Example
<?php
declare(strict_types=1);
use support\Request;
function getUser(Request $request): array
{
// PHPStan error:
// Call to an undefined method support\Request::user()
return $request->user();
}Solution 1: Create Stub File
Create stubs/Request.stub:
<?php
namespace support;
class Request
{
public function user(): ?array
{
}
}Reference in phpstan.neon:
parameters:
stubFiles:
- stubs/Request.stubSolution 2: Ignore Specific Errors
parameters:
ignoreErrors:
- '#Call to an undefined method support\\Request::user\(\)#'Issue 5: Using Baseline to Manage Existing Errors
When a project already has a lot of code, fixing all errors at once is unrealistic. Using Baseline allows you to:
- Record existing errors
- Ensure no new errors are introduced
- Gradually fix old errors
Generate Baseline
./vendor/bin/phpstan analyse --generate-baselineThis creates a phpstan-baseline.neon file.
Reference in Configuration
includes:
- phpstan-baseline.neon
parameters:
level: 5
paths:
- appUpdate Baseline
# After fixing some errors, regenerate Baseline
./vendor/bin/phpstan analyse --generate-baselineCI Integration
GitHub Actions
Add to .github/workflows/code-quality.yml:
name: Code Quality
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
phpstan:
name: PHPStan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: none
extensions: mbstring, pdo, pdo_mysql
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse --memory-limit=2G --error-format=githubGitLab CI
Add to .gitlab-ci.yml:
phpstan:
stage: test
image: php:8.3-cli
before_script:
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install --prefer-dist --no-progress
script:
- ./vendor/bin/phpstan analyse --memory-limit=2G
cache:
paths:
- vendor/
- runtime/cache/phpstan/
only:
- merge_requests
- main
- developBest Practices
Recommended
Start from Level 5
neonparameters: level: 5Use Baseline to manage existing errors
bashcomposer stan:baselineRun in CI
- Ensure every commit passes checks
- Prevent introducing new errors
Gradually increase level
- Level 5 -> Level 6 -> Level 7 -> Level 8
- Fix all errors after each increase
Add PHPDoc for complex types
php/** * @param array<string, mixed> $data * @return array<int, Order> */ public function process(array $data): array { // ... }Use strict type declarations
php<?php declare(strict_types=1);Handle nullable types
phppublic function getUser(?int $id): ?User { if ($id === null) { return null; } return $this->userRepository->find($id); }
Avoid
Don't ignore all errors
neon# Wrong parameters: ignoreErrors: - '#.*#'Don't use @phpstan-ignore-line everywhere
php// Wrong $user->getName(); // @phpstan-ignore-lineDon't use mixed type in production code
php// Wrong public function process(mixed $data): mixed { // ... }Don't skip levels
- Level 5 -> Level 9 (Wrong)
- Level 5 -> Level 6 -> Level 7 -> Level 8 (Correct)
Don't analyze vendor directory
neonparameters: excludePaths: - vendor/*
Workflow Examples
New Project Workflow
# 1. Install PHPStan
composer require --dev phpstan/phpstan
# 2. Create configuration file
cat > phpstan.neon << 'EOF'
parameters:
level: 5
paths:
- app
EOF
# 3. First run
composer stan
# 4. Fix errors
# ... modify code ...
# 5. Run again until passing
composer stan
# 6. Commit configuration
git add phpstan.neon composer.json composer.lock
git commit -m "chore: add PHPStan configuration"Existing Project Workflow
# 1. Install PHPStan
composer require --dev phpstan/phpstan
# 2. Create configuration file (Level 5)
cat > phpstan.neon << 'EOF'
parameters:
level: 5
paths:
- app
EOF
# 3. Generate Baseline
composer stan:baseline
# 4. Commit Baseline
git add phpstan.neon phpstan-baseline.neon
git commit -m "chore: add PHPStan with baseline"
# 5. Gradually fix errors
# ... modify code ...
# 6. Update Baseline
composer stan:baseline
# 7. Increase level (when Baseline is empty)
# Modify phpstan.neon: level: 6
composer stan:baselineIntegration with Other Tools
PHPStan + Pint
# Format first, then analyze
composer fmt && composer stanPHPStan + Rector
# Rector can auto-fix some PHPStan errors
composer rector && composer stanComplete Quality Check
{
"scripts": {
"quality": [
"@fmt:test",
"@stan",
"@test"
]
}
}