Achieving PHPStan Level 9: A Journey to World-Class Laravel Code Quality

July 25, 2025 (4mo ago)

Jump to FAQs

As maintainers of Laravolt, we're constantly striving for excellence in our codebase. Today, I'm excited to share our recent journey from PHPStan level 4 to the maximum level 9 - and why this matters for every Laravel developer who wants to build world-class software.

🎯 The Philosophy: Excellence is a Habit, Not an Act

When we started this journey, we had a simple but powerful belief: beginners should learn strict standards from day one. Too often, we see developers starting with loose configurations "to make learning easier," only to struggle later when trying to write production-ready code.

We believe in a different approach: teach excellence from the beginning.

📊 Our Journey: From Level 4 to Level 9

Level 4 → 5: The Foundation

Our first challenge involved fixing basic type safety issues:

Key Learning: Laravel's __() function always returns a string, so ?? '' is redundant.

Level 5 → 6: Generic Types

Level 6 introduced generic type checking:

/**
 * @extends Enum<string>
 */
final class UserStatus extends Enum
{
    const PENDING = 'PENDING';
    const ACTIVE = 'ACTIVE';
    const BLOCKED = 'BLOCKED';
}
 
/**
 * @use HasFactory<UserFactory>
 */
class User extends BaseUser
{
    /** @use HasFactory<UserFactory> */
    use HasFactory, Notifiable;
}

Key Learning: Proper generic annotations improve IDE support and catch type mismatches early.

Level 6 → 9: The Leap to Excellence

We decided to jump straight to level 9 - the maximum strictness. Surprisingly, we only had one issue to fix:

// Before: Potential null pointer exception
if ($response === Password::RESET_LINK_SENT) {
    $email = $user->getEmailForPasswordReset(); // $user could be null!
}
 
// After: Proper null safety
if ($response === Password::RESET_LINK_SENT && $user) {
    $email = $user->getEmailForPasswordReset(); // Safe!
}

Key Learning: Level 9 catches the most subtle null safety issues that could cause runtime errors.

🛠️ Our Multi-Configuration Strategy

Instead of compromising on quality, we created three configurations that maintain high standards while serving different purposes:

1. Learning Configuration (Level 7)

# phpstan-beginner.neon
parameters:
    level: 7  # Strict standards from day one
    treatPhpDocTypesAsCertain: true
 
    disallowedFunctionCalls:
        -
            function: 'env()'
            message: 'Use config() instead - see: https://laravel.com/docs/configuration#retrieving-configuration-values'
        -
            function: 'dd()'
            message: 'Remove debug statements - use proper logging instead: https://laravel.com/docs/logging'

Purpose: Educational environment with helpful error messages and documentation links.

2. Development Configuration (Level 8)

# phpstan.neon
parameters:
    level: 8  # High standards for daily development
    treatPhpDocTypesAsCertain: true
    reportMaybes: true

Purpose: Daily development with production-quality standards.

3. Production Configuration (Level 9)

# phpstan-strict.neon
parameters:
    level: 9  # Maximum strictness
    treatPhpDocTypesAsCertain: true
    reportMaybes: true

Purpose: CI/CD and production readiness validation.

🚀 Composer Scripts for Easy Access

We added convenient composer scripts to make quality checks effortless:

{
  "scripts": {
    "analyse:learn": "./vendor/bin/phpstan analyse --configuration=phpstan-beginner.neon",
    "analyse:dev": "./vendor/bin/phpstan analyse --configuration=phpstan.neon",
    "analyse:production": "./vendor/bin/phpstan analyse --configuration=phpstan-strict.neon",
    "quality": ["@analyse:dev", "@test"]
  }
}

🏆 The Results: What Level 9 Brings

Achieving PHPStan level 9 provides:

Maximum Type Safety - Prevents runtime errors before they happen ✅ Enhanced IDE Support - Better autocompletion and refactoring ✅ Self-Documenting Code - Types serve as living documentation ✅ Team Consistency - Everyone follows the same strict standards ✅ Maintainable Codebase - Easier to understand and modify ✅ Production Confidence - Deploy with certainty

💡 Key Insights for Laravel Developers

1. Start Strict, Stay Strict

Don't begin with loose standards and "tighten up later." Build excellent habits from day one.

2. Type Annotations Are Your Friend

Proper PHPDoc annotations aren't just comments - they're contracts that PHPStan enforces:

/**
 * @param Collection<User> $users
 * @return array<string, mixed>
 */
public function formatUsers(Collection $users): array
{
    // PHPStan knows exactly what types we're working with
}

3. Null Safety is Critical

Level 9's null safety checks prevent the most common source of runtime errors in PHP applications.

4. Laravel Magic vs. Type Safety

While Laravel's magic methods are convenient, explicit typing provides better maintainability:

// Instead of relying on magic
$user->posts()->where('published', true)->get();
 
// Consider explicit relationships and typing
/** @return HasMany<Post> */
public function posts(): HasMany
{
    return $this->hasMany(Post::class);
}

🎓 Our Learning Path Recommendation

For teams adopting strict PHPStan standards:

  1. Week 1-2: Start with composer analyse:learn (Level 7)

    • Learn proper typing patterns
    • Understand error messages
    • Build good habits
  2. Week 3+: Develop with composer analyse:dev (Level 8)

    • Maintain quality in daily work
    • Catch issues early
  3. Deployment: Validate with composer analyse:production (Level 9)

    • Ensure production readiness
    • Zero tolerance for type issues

🌟 Impact on Laravolt

Since implementing these standards:

🚀 Getting Started

Want to implement this in your Laravel project? Here's how:

  1. Install the tools:
composer require --dev larastan/larastan spaze/phpstan-disallowed-calls
  1. Download our configurations:
# Copy our battle-tested configurations
wget https://gist.github.com/laravolt/phpstan-configs/phpstan.neon
wget https://gist.github.com/laravolt/phpstan-configs/phpstan-beginner.neon
wget https://gist.github.com/laravolt/phpstan-configs/phpstan-strict.neon
  1. Add composer scripts:
{
  "scripts": {
    "analyse:learn": "./vendor/bin/phpstan analyse --configuration=phpstan-beginner.neon",
    "analyse:dev": "./vendor/bin/phpstan analyse --configuration=phpstan.neon",
    "analyse:production": "./vendor/bin/phpstan analyse --configuration=phpstan-strict.neon",
    "quality": ["@analyse:dev", "@test"]
  }
}
  1. Start your journey:
composer analyse:learn

🤝 Community Challenge

We challenge the Laravel community: Can your project pass PHPStan level 9?

Share your journey with us:

💭 Final Thoughts

Achieving PHPStan level 9 isn't just about passing a static analysis tool - it's about committing to excellence. It's about respecting your future self, your team, and your users by writing code that's robust, maintainable, and reliable.

As we always say at Laravolt: "Excellence is a habit, not an act."

What level is your codebase at? Let's build world-class Laravel applications together! 🚀


Want to see our complete PHPStan configurations? Check out our GitHub repository where all configurations are available.

About Laravolt: We're dedicated to building elegant, powerful tools for the Laravel ecosystem. Follow us for more insights on Laravel development best practices.

Have questions? Reach out to us in the comments or on our Discord community!

Discuss this post:

Frequently Asked Questions

What is PHPStan and why should I use it in my Laravel project?

PHPStan is a static analysis tool that finds bugs in your code without running it. For Laravel projects, it helps catch type errors, null pointer exceptions, and other issues before they reach production. It's especially valuable because it enforces type safety in PHP's dynamically typed environment.

Is it really necessary to start with strict standards from day one?

Absolutely! Starting with loose standards and 'tightening up later' creates technical debt and bad habits. When beginners learn strict typing from the start, they write better code naturally. It's much harder to retrofit type safety into an existing loose codebase than to build it correctly from the beginning.

What's the difference between PHPStan levels 4, 8, and 9?

Level 4 catches basic type issues and undefined variables. Level 8 adds stricter rules about mixed types and more thorough null checking. Level 9 is the maximum strictness - it catches the most subtle issues like potential null pointer exceptions that could cause runtime errors. Each level builds upon the previous one.

Why do you use three different configurations instead of just one?

Our three-config approach serves different purposes: Level 7 (learning) provides educational error messages with documentation links for beginners. Level 8 (development) maintains high standards for daily work without being overwhelming. Level 9 (production) ensures maximum safety for deployment. This graduated approach helps teams adopt strict standards progressively.

How long does it typically take to upgrade an existing Laravel project to PHPStan level 9?

It depends on your current code quality, but for a well-structured Laravel project, the journey from level 4 to 9 can take 1-4 weeks. The key is doing it incrementally - fix level 5 issues first, then 6, and so on. Most issues are related to proper type annotations and null safety checks.

Will PHPStan level 9 conflict with Laravel's magic methods and dynamic features?

PHPStan works well with Laravel thanks to the Larastan package, which understands Laravel's magic. However, level 9 encourages more explicit typing over magic methods. This actually improves code maintainability - explicit relationships and typed methods are easier to understand and refactor than magic alternatives.

What are the most common issues when upgrading to PHPStan level 9?

The most common issues are: missing type annotations (especially for collections and generics), improper null handling, redundant null coalescing operators, and missing return type declarations. Most can be fixed by adding proper PHPDoc comments and ensuring null safety in conditional statements.

How do I handle Laravel collections and Eloquent relationships with strict typing?

Use generic type annotations in PHPDoc comments. For example: @param Collection<User> $users for collections, @return HasMany<Post> for relationships, and @use HasFactory<UserFactory> for model factories. This gives PHPStan exact type information about what your collections contain.

Is the performance impact of running PHPStan level 9 significant?

PHPStan analysis happens during development/CI, not runtime, so there's no performance impact on your application. The analysis itself takes longer at higher levels, but for most Laravel projects, level 9 analysis completes in under a minute. The time investment pays off through reduced debugging and better code quality.

Can I implement this gradually in an existing team without disrupting development?

Yes! Start with our learning configuration (level 7) for new code only, use git hooks to run analysis on changed files, and gradually address existing code during regular maintenance. The three-configuration approach allows teams to adopt strict standards without stopping all development work.