Table Of Contents
- Why Static Analysis Matters: Beyond the Obvious
- PHPStan: The Tool That Changed My Development Life
- Psalm: The Alternative Powerhouse
- Practical Examples: The Bugs That Would Have Destroyed Me
- Advanced Type Annotations
- Custom PHPStan Rules
- Integration with CI/CD
- Code Quality Tools Integration
- Performance Optimization
- IDE Integration
- Real-World Implementation Strategy - Lessons from Team Adoption
- Common Pitfalls and Solutions
- Measuring Success
- Conclusion: From Skeptic to Static Analysis Evangelist
I was a static analysis skeptic until PHPStan saved me from what would have been a career-ending bug. I had written what I thought was bulletproof Laravel code for a payment processing feature. Manual testing passed, unit tests passed, everything looked perfect. Then I reluctantly ran PHPStan at a colleague's insistence, and it found a type error that would have caused payments to be processed for the wrong amounts.
That single bug catch transformed me from a skeptic to an evangelist. Static analysis went from "annoying red squiggles in my IDE" to "the guardian angel of my codebase." Now I can't imagine writing PHP without it - it's like coding with a safety net that catches you before you fall. This experience fundamentally changed my approach to PHP security and code quality.
Why Static Analysis Matters: Beyond the Obvious
That payment bug taught me that static analysis isn't just about catching typos - it's about catching logic errors that your brain doesn't see because you're too close to the code. When I wrote that payment method, I was thinking about the happy path. PHPStan was thinking about all the ways it could break.
Static analysis examines your code like a suspicious code reviewer who questions every assumption. It finds type mismatches, null pointer exceptions waiting to happen, and dead code that shouldn't exist. It's like having a paranoid colleague who catches the mistakes you make when you're tired or overconfident.
PHPStan: The Tool That Changed My Development Life
PHPStan became my coding companion after that payment scare. What started as reluctant compliance with team standards became genuine appreciation for a tool that made me a better developer. The best part? It learns your Laravel codebase and understands Eloquent relationships, middleware, and all the Laravel magic that other tools miss. Combined with Laravel API development best practices, static analysis creates robust, maintainable applications.
My first PHPStan setup was painful - 847 errors on a codebase I thought was clean. But fixing those errors taught me more about PHP type safety than years of experience had.
Installation and Basic Setup
# Install PHPStan
composer require --dev phpstan/phpstan
# Create phpstan.neon configuration
touch phpstan.neon
# phpstan.neon
parameters:
level: 5
paths:
- app
- tests
excludePaths:
- app/Console/Kernel.php
- app/Http/Kernel.php
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
ignoreErrors:
- '#Unsafe usage of new static#'
Progressive Analysis Levels - My Journey from 0 to 8
PHPStan's 10 levels taught me patience. I wanted to jump straight to level 9 to prove I was a "serious developer," but level 3 nearly broke me with 2,000+ errors. Now I recommend the gradual approach that actually worked for me:
# Level 0 - Basic checks
vendor/bin/phpstan analyze --level=0
# Level 5 - Good balance of strictness and practicality
vendor/bin/phpstan analyze --level=5
# Level 9 - Very strict, catches almost everything
vendor/bin/phpstan analyze --level=9
Laravel Integration
# Install Laravel extension
composer require --dev nunomaduro/larastan
# Updated phpstan.neon for Laravel
# For more Laravel-specific configurations, see:
# https://mycuriosity.blog/level-up-your-laravel-validation-advanced-tips-tricks
parameters:
level: 5
paths:
- app
includes:
- ./vendor/nunomaduro/larastan/extension.neon
Advanced PHPStan Configuration
# phpstan.neon
parameters:
level: 6
paths:
- app
- tests
# Ignore specific patterns
ignoreErrors:
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder#'
- '#Method App\\Models\\User::find\(\) should return App\\Models\\User\|null but returns Illuminate\\Database\\Eloquent\\Model\|null#'
# Custom rules
rules:
- PHPStan\Rules\Classes\UnusedConstructorParametersRule
- PHPStan\Rules\DeadCode\UnusedPrivateMethodRule
- PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule
# Type coverage
typeAliases:
UserId: 'int<1, max>'
Email: 'string'
# Bleeding edge features
reportUnmatchedIgnoredErrors: true
checkTooWideReturnTypesInProtectedAndPublicMethods: true
checkUninitializedProperties: true
Psalm: The Alternative Powerhouse
Psalm is another excellent static analysis tool with different strengths. It's particularly good at finding complex type issues and has excellent generic support.
Installation and Setup
# Install Psalm
composer require --dev vimeo/psalm
# Initialize Psalm
vendor/bin/psalm --init
<!-- psalm.xml -->
<?xml version="1.0"?>
<psalm
errorLevel="3"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="app" />
<directory name="tests" />
<ignoreFiles>
<directory name="vendor" />
<file name="app/Console/Kernel.php" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<MoreSpecificReturnType errorLevel="info" />
<PropertyNotSetInConstructor errorLevel="info" />
</issueHandlers>
<plugins>
<pluginClass class="Psalm\LaravelPlugin\Plugin"/>
</plugins>
</psalm>
Laravel Plugin for Psalm
# Install Laravel plugin
composer require --dev psalm/plugin-laravel
# Enable the plugin
vendor/bin/psalm-plugin enable psalm/plugin-laravel
Practical Examples: The Bugs That Would Have Destroyed Me
Type Errors - The Payment Bug That Almost Happened
This exact pattern was in my payment processing code. I was calculating cart totals and assumed the array would always contain numbers. PHPStan caught that the array could contain mixed types, which would have caused incorrect payment amounts. This kind of type safety is crucial for secure web application development:
// My original dangerous code
function calculateTotal(array $items): float
{
$total = 0;
foreach ($items as $item) {
$total += $item; // PHPStan: Cannot add array|string to int
}
return $total; // Could return completely wrong amount!
}
// PHPStan forced me to be explicit about types
function calculateTotal(array $items): float
{
$total = 0.0;
foreach ($items as $item) {
if (is_numeric($item)) {
$total += (float) $item;
} else {
throw new InvalidArgumentException('All items must be numeric');
}
}
return $total;
}
Null Pointer Issues
// PHPStan catches potential null pointer
function getUserEmail(int $userId): string
{
$user = User::find($userId); // Returns User|null
return $user->email; // Error: Cannot access property on null
}
// Fixed version
function getUserEmail(int $userId): ?string
{
$user = User::find($userId);
return $user?->email;
}
// Or with explicit null check
function getUserEmail(int $userId): string
{
$user = User::find($userId);
if ($user === null) {
throw new UserNotFoundException("User {$userId} not found");
}
return $user->email;
}
Unreachable Code
// PHPStan detects unreachable code
function processPayment(float $amount): bool
{
if ($amount <= 0) {
return false;
}
if ($amount > 1000000) {
throw new InvalidArgumentException('Amount too large');
}
return true;
echo "Payment processed"; // Unreachable code
}
Advanced Type Annotations
Generic Types
/**
* @template T
* @param class-string<T> $className
* @return T
*/
function createInstance(string $className): object
{
return new $className();
}
// Usage
$user = createInstance(User::class); // PHPStan knows this is User
Collection Types
/**
* @param array<int, User> $users
* @return array<int, string>
*/
function extractUserEmails(array $users): array
{
return array_map(fn(User $user) => $user->email, $users);
}
/**
* @param Collection<int, Product> $products
* @return Collection<int, Product>
*/
function getActiveProducts(Collection $products): Collection
{
return $products->filter(fn(Product $product) => $product->isActive());
}
Complex Type Definitions
/**
* @param array{name: string, age: int, email: string} $userData
* @return User
*/
function createUser(array $userData): User
{
return new User($userData['name'], $userData['age'], $userData['email']);
}
/**
* @param array<string, int|string|bool> $config
* @return void
*/
function configure(array $config): void
{
// Implementation
}
Custom PHPStan Rules
Create custom rules for your specific needs:
// CustomRule.php
use PHPStan\Rules\Rule;
use PHPStan\Analyser\Scope;
use PhpParser\Node;
class NoDirectDatabaseQueryRule implements Rule
{
public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}
public function processNode(Node $node, Scope $scope): array
{
if ($node->class instanceof Node\Name &&
$node->class->toString() === 'DB' &&
$node->name instanceof Node\Identifier &&
in_array($node->name->name, ['select', 'insert', 'update', 'delete'])) {
return ['Direct database queries are not allowed. Use repositories instead.'];
}
return [];
}
}
Integration with CI/CD
GitHub Actions
# .github/workflows/static-analysis.yml
name: Static Analysis
on: [push, pull_request]
jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run PHPStan
run: vendor/bin/phpstan analyze --error-format=github
- name: Run Psalm
run: vendor/bin/psalm --output-format=github
Pre-commit Hooks
# Install pre-commit
pip install pre-commit
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: phpstan
name: phpstan
entry: vendor/bin/phpstan analyze --no-progress
language: system
types: [php]
pass_filenames: false
- id: psalm
name: psalm
entry: vendor/bin/psalm --no-progress
language: system
types: [php]
pass_filenames: false
Code Quality Tools Integration
PHP CS Fixer
# Install PHP CS Fixer
composer require --dev friendsofphp/php-cs-fixer
# .php-cs-fixer.php
<?php
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => true,
'no_unused_imports' => true,
'declare_strict_types' => true,
])
// Following PSR standards improves code quality:
// https://mycuriosity.blog/php-psr-standards-writing-interoperable-code
->setFinder(
PhpCsFixer\Finder::create()
->in('app')
->in('tests')
);
PHPMD (PHP Mess Detector)
# Install PHPMD
composer require --dev phpmd/phpmd
# phpmd.xml
<?xml version="1.0"?>
<ruleset name="Custom PHPMD ruleset">
<rule ref="rulesets/cleancode.xml">
<exclude name="StaticAccess" />
</rule>
<rule ref="rulesets/codesize.xml" />
<rule ref="rulesets/controversial.xml" />
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/naming.xml" />
<rule ref="rulesets/unusedcode.xml" />
</ruleset>
Performance Optimization
Static analysis can be slow on large codebases. Here's how to optimize:
Baseline Files
# Generate baseline to ignore existing issues
vendor/bin/phpstan analyze --generate-baseline
# This creates phpstan-baseline.neon
parameters:
includes:
- phpstan-baseline.neon
Parallel Processing
# phpstan.neon
parameters:
parallel:
maximumNumberOfProcesses: 4
processTimeout: 120.0
Result Caching
# phpstan.neon
parameters:
tmpDir: var/cache/phpstan
resultCachePath: var/cache/phpstan/resultCache.php
IDE Integration
PHPStorm
PHPStorm has excellent built-in support for both PHPStan and Psalm:
- Go to Settings > PHP > Quality Tools
- Configure PHPStan and Psalm paths
- Enable inspections in Editor > Inspections
VS Code
// .vscode/settings.json
{
"php.validate.enable": false,
"php.suggest.basic": false,
"phpstan.enabled": true,
"phpstan.path": "vendor/bin/phpstan",
"phpstan.config": "phpstan.neon"
}
Real-World Implementation Strategy - Lessons from Team Adoption
Getting my team to adopt static analysis was harder than learning it myself. Developers hate being told their code has 800+ errors, especially when it "works fine." Here's the approach that actually worked, following clean code principles for better team adoption:
Phase 1: Foundation (Week 1-2)
- Install PHPStan at level 0
- Fix basic issues
- Set up CI/CD integration
Phase 2: Progressive Improvement (Week 3-4)
- Increase to level 3
- Add Laravel/framework-specific rules
- Train team on annotations
Phase 3: Advanced Features (Week 5-6)
- Reach level 5-6
- Add custom rules
- Implement baseline for legacy code
Phase 4: Mastery (Ongoing)
- Reach level 8-9 for new code
- Add Psalm for additional coverage
- Continuous improvement
Common Pitfalls and Solutions
Over-Suppression
// Bad - suppressing too broadly
/** @phpstan-ignore-next-line */
$user = User::find($id);
// Good - specific suppression with reason
/** @phpstan-ignore-next-line User::find() can return null but we know ID exists */
$user = User::find($validatedId);
Type Annotation Overload
// Bad - over-annotating obvious types
/** @var string $name */
$name = 'John';
// Good - annotating complex types
/** @var array<string, mixed> $config */
$config = json_decode($jsonString, true);
Measuring Success
Track these metrics to measure static analysis success. Understanding PHP performance profiling helps correlate static analysis improvements with application performance:
// Metrics to track
class StaticAnalysisMetrics
{
public function getMetrics(): array
{
return [
'phpstan_errors' => $this->countPhpStanErrors(),
'psalm_errors' => $this->countPsalmErrors(),
'code_coverage' => $this->getCodeCoverage(),
'type_coverage' => $this->getTypeCoverage(),
'bugs_prevented' => $this->getBugsPrevented(),
];
}
private function countPhpStanErrors(): int
{
// Parse PHPStan output
$output = shell_exec('vendor/bin/phpstan analyze --error-format=json');
$data = json_decode($output, true);
return count($data['files'] ?? []);
}
}
Conclusion: From Skeptic to Static Analysis Evangelist
That payment bug that PHPStan caught changed my entire approach to PHP development. What started as reluctant compliance with team standards became genuine enthusiasm for tools that make me a better developer.
The transformation wasn't just about catching bugs - it was about confidence. Before static analysis, I deployed code hoping it would work. Now I deploy knowing it will work because I've caught the obvious errors before they reach production.
The mindset shift was profound: I went from writing code and crossing my fingers to writing code with mathematical certainty about type safety. PHPStan didn't just find bugs; it taught me to think more precisely about data flow and type contracts.
My advice for fellow Laravel developers:
Start Small: Don't jump to level 9 on day one. Level 0 → 3 → 5 → 8 is a journey that teaches you incrementally.
Embrace the Errors: Those 847 initial errors weren't criticisms of my abilities - they were opportunities to learn about type safety and defensive programming.
Team Buy-in: Show your team the actual bugs that static analysis catches. Abstract benefits don't convince developers; concrete examples of prevented disasters do.
Make it Automatic: Integrate static analysis into your CI/CD pipeline so it's not optional. Make it impossible to merge code that fails analysis. For comprehensive security, combine this with API authentication best practices.
The most rewarding moment was when a junior developer on my team said, "I can't imagine writing PHP without PHPStan anymore." That's when I knew we had successfully transformed our development culture. Teaching PHP design patterns alongside static analysis creates more confident, capable developers.
Static analysis isn't just about writing better code - it's about sleeping better at night knowing your applications are more reliable. When you've experienced the confidence that comes from having a tool catch your mistakes before users do, there's no going back. Combined with proper PHP memory management and secure authentication practices, static analysis forms the foundation of bulletproof PHP applications.
Add Comment
No comments yet. Be the first to comment!