The go-to resource for learning PHP, Laravel, Symfony, and your dependencies.

Upgrading PHPStan for Better Static Analysis


In a professional orchestra, instruments require regular maintenance and calibration. A violin that hasn’t been serviced in years might still produce sound, but it won’t achieve its full potential—intonation may drift, strings might lose their responsiveness, and subtle imperfections can accumulate. The same principle applies to the tools we use in software development. If you’re responsible for maintaining a PHP codebase, static analyzers like PHPStan become precision instruments for code quality; they need regular attention to perform at their best.

PHP, with its dynamic type system, benefits particularly from disciplined static analysis. Unlike compiled languages where the type system is enforced at compile time, PHP’s flexibility means many type errors surface only at runtime—often in production. PHPStan addresses this by analyzing your code without executing it, catching type mismatches, undefined variables, and other issues before they cause incidents. But to get the most from PHPStan, you need to keep it current. Each release brings improved type inference, new rules, and better performance—all of which translate to higher confidence in your codebase.

In this guide, we’ll walk through upgrading PHPStan in a real project. We’ll start with preparation, move through the upgrade itself, and then tackle the new errors that often appear. Along the way, we’ll look at concrete examples and explore how to leverage what the new version offers.

Why Upgrade PHPStan?

Upgrading PHPStan isn’t just about chasing the latest version number—though, version numbers do matter. Each release typically brings a host of improvements that can impact your development workflow in practical ways.

Before we dive into the how, let’s consider what you actually gain. PHPStan tends to improve in three key areas:

  • New Features: The PHPStan team consistently adds capabilities, from more powerful type inference to new rules and configuration options. These often help you catch classes of bugs that were previously invisible to the analyzer.
  • Better Performance: Performance is a key focus area. Newer versions are often measurably faster, especially on larger codebases. This means you can typically run analysis more frequently—even in pre-commit hooks or CI pipelines—without significantly slowing down your workflow.
  • Improved Accuracy: With each release, PHPStan gets better at understanding common code patterns. This generally means fewer false positives and more genuine issues caught. In practice, the analysis tends to become more precise over time.

Of course, PHPStan isn’t the only static analysis tool available. Before we focus on PHPStan specifically, let’s survey the landscape. Several competent options exist, and understanding their differences helps you make an informed choice.

PHPStan has gained widespread adoption for its rigorous approach and incremental level-based system (Level 0 through max level, with max being the strictest). It excels at type inference and gradually increasing strictness. PHPStan doesn’t just check that your code runs; it verifies that your types are consistent, your contracts are honored, and your logic is sound.

Psalm, developed by the team at Vimeo, takes a slightly different approach. It has its own powerful type system extensions and can be gentler on legacy codebases through its own level system. Psalm tends to be more permissive out of the box for older code but can be tuned to be extremely strict. Many teams choose Psalm when they have significant legacy code that needs gradual improvement, or when they want to leverage Psalm’s unique type features like @psalm- annotations for advanced scenarios.

Phan emphasizes backward compatibility and tends to be more conservative. It’s often a good choice for projects that prioritize stability and need to support older PHP versions or code patterns that other analyzers flag aggressively.

Which tool is right for you? That depends on your project’s needs, your codebase’s age, and your team’s preference for strictness versus gradual adoption. In this guide, we focus on PHPStan because its level-based approach provides a clear migration path and its type inference is particularly robust. That said, many of the upgrade principles we’ll cover apply to other tools as well. If you’re using Psalm or Phan, you’ll find the general approach similar even if the specific commands and error messages differ.

The Upgrade Trade-Off

Before we get into the mechanics, it’s worth acknowledging that upgrading does involve some risk. Newer versions of PHPStan typically raise the bar: they understand more patterns, but they also catch more issues. This means you’ll likely see new errors—potentially many of them—after upgrading.

That’s not necessarily bad. In fact, it’s often the point: PHPStan is finding problems that were there all along but went undetected. However, it does mean you should allocate time to address these findings. The trade-off is short-term disruption for long-term quality.

Additionally, major version upgrades (say, from PHPStan 0.x to 1.x, or 1.x to 2.x) may include breaking changes in how you configure the tool or what rules are enabled by default. Minor upgrades (1.10 to 1.11) are usually gentler, though they can still surface new issues.

Of course, in our experience, the best approach is to plan the upgrade as a dedicated task rather than something that happens incidentally. Schedule time to review the new errors and decide which need immediate attention and which can be deferred with a baseline.

With that context in mind, let’s walk through the process.

Preparing for the Upgrade

A little preparation can make the upgrade process much smoother. Here’s what we recommend doing before you touch any dependencies.

First, check your composer.json file to see which version of PHPStan you’re currently using. This helps you understand how significant the version jump is. Look for the phpstan/phpstan requirement in the require-dev section:

{
    "require-dev": {
        "phpstan/phpstan": "^1.10"
    }
}

That ^1.10 constraint means you’re locked to PHPStan 1.x and any patch or minor version at or above 1.10. If you want to upgrade to PHPStan 2.x, you’ll need to update the version constraint as well.

Next, read the release notes for the version you’re targeting. The PHPStan team maintains detailed changelogs on their GitHub releases page. Pay special attention to:

  • Breaking changes: These are explicitly marked and often require configuration updates.
  • New rules enabled by default: If you’re upgrading to a higher level, or even within the same level, new checks may be turned on.
  • Deprecated features: Things that worked in the old version might be removed or changed.

If you’re upgrading across multiple major versions (say from 0.x to 1.x, or 1.x to 2.x), consider upgrading incrementally if possible. Sometimes jumping directly to the latest version creates more work than stepping through intermediate releases. The PHPStan team generally maintains upgrade guides for major version transitions.

Finally, ensure you have a recent commit or branch you can return to if needed. You won’t typically break your production code by upgrading PHPStan—it’s a development tool—but you might want to compare before and after results. A clean Git working directory is helpful, though you may also want to generate a baseline later.

The Upgrade Process

Upgrading PHPStan typically involves running a single Composer command, though the specific steps depend on your project’s setup. Before proceeding, it’s worth taking a moment to ensure you’re working from a clean state. Make sure your current code is committed to version control, or create a dedicated branch for the upgrade. While upgrading PHPStan won’t modify your application code directly, you’ll want to be able to compare results before and after, and having a stable baseline makes it easier to roll back if needed.

The fundamental upgrade command looks like this:

composer require --dev phpstan/phpstan --with-all-dependencies

Usage:

composer require [options] package-name

In this form, we’re telling Composer to:

  • Update the phpstan/phpstan package in the require-dev section
  • Update all of PHPStan’s dependencies as well (--with-all-dependencies), which is important because PHPStan relies on several other packages that must remain compatible
  • The --dev flag ensures the package is added to development dependencies (though if already present, this simply updates the version)

After the upgrade completes, you should run PHPStan to see if there are any new errors. The basic analysis command is:

vendor/bin/phpstan analyse [path]

For example, to analyze your src directory:

vendor/bin/phpstan analyse src

A Practical Walkthrough

Let’s look at a concrete example of what this process looks like in practice. Suppose you’re maintaining a typical Laravel application with a User model and a repository pattern. Your current PHPStan version is 1.10.0, and you want to upgrade to 1.11.0.

Before upgrading, your baseline might look something like this:

$ vendor/bin/phpstan analyse app
 ______          

   ____ _ ____   _____ _   _  _____ 
  / ___| |  _ \ | ____| | | |/ / __|
 | |  _| | | | ||  _| | | | ' /|  _|
 | |_| | | |_| || |___| |_| . \| |__|
  \____|_|  .__/|_____|\___/_|\_\\____|
          |_|                       

Note: Using the configuration file at phpstan.neon.dist.  

Scanning files...

100% (35/35) [----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------] < 1 sec  

 36/36 [------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------] 1 sec. 0 errors  

After upgrading, you run the analysis again:

$ vendor/bin/phpstan analyse app
 ______          

   ____ _ ____   _____ _   _  _____ 
  / ___| |  _ \ | ____| | | |/ / __|
 | |  _| | | | ||  _| | | | ' /|  _|
 | |_| | | |_| || |___| |_| . \| |__|
  \____|_|  .__/|_____|\___/_|\_\\____|
          |_|                       

Note: Using the configuration file at phpstan.neon.dist.  

Scanning files...

100% (35/35) [----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------] < 1 sec  

 36/36 [------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------] 1 sec.  

 ---------- ------  ----- 
  Type      Error    Line  
 ---------- ------  ----- 
  Offset    Trying to access array offset on string|UserRepository.php:27:38  
  Offset    Trying to access array offset on string|UserRepository.php:35:22  
  Property  Access to an undefined property UserRepository::$cache|UserRepository.php:15:23  
 ---------- ------  ----- 

Now you have three new errors. Let’s look at what these mean and how to address them. The first two are “Trying to access array offset on string” errors. Here’s the problematic code at line 27:

// UserRepository.php, line 27
public function find(int $id): ?User
{
    $key = 'user:' . $id;
    $cached = $this->cache->get($key);  // Line 27
    
    if ($cached !== null) {
        return unserialize($cached);  // This returns mixed, not User
    }
    
    $user = $this->db->fetchAssociative('SELECT * FROM users WHERE id = ?', [$id]);
    $this->cache->set($key, serialize($user));
    
    return $user ? new User($user) : null;
}

PHPStan is telling you that $this->cache->get($key) returns string|false|null (depending on your cache implementation), but you’re treating it as if it’s always a string when you pass it to unserialize(). The fix would be to explicitly check the type:

$cached = $this->cache->get($key);

if (is_string($cached)) {
    return unserialize($cached);
}

// Handle the case where cache returns null/false

The third error, “Access to an undefined property,” suggests that PHPStan now correctly infers that $this->cache might not be initialized. Perhaps your constructor has a conditional assignment. You’ll need to ensure the property is properly declared and initialized.

This example illustrates a common pattern: PHPStan upgrades often uncover latent issues—code that technically worked at runtime but relied on implicit behaviors or lacked proper type safety. Addressing these errors typically improves your code’s robustness, even if it requires some refactoring.

Dealing with New Errors

As we saw in the walkthrough, it’s quite common to see new errors after upgrading PHPStan. This is generally a positive sign—PHPStan is catching issues that were always present but weren’t detected by the older version. That said, it can also feel overwhelming, especially on larger codebases.

When you encounter new errors, your first step is to understand what they’re telling you. PHPStan’s error messages typically include the file, line number, error type, and a description. Read them carefully—they often point directly to the problem. If you’re not sure how to fix a particular error, the PHPStan documentation and their Discord community can be helpful resources.

That said, not all errors require immediate attention. Some may be edge cases you’ve consciously accepted; others might be in third-party code you don’t control. We recommend a tiered approach:

  1. Critical issues first: Errors that indicate potential runtime failures—like calling methods on null or accessing array offsets on non-arrays—should generally be fixed promptly. These are the kinds of bugs PHPStan excels at catching.

  2. Type consistency issues: Problems like return type mismatches or parameter type violations usually indicate design issues that are worth addressing, though they may require more thoughtful refactoring.

  3. Documentation drift: Sometimes PHPStan will flag inconsistencies between your docblocks and actual code. These are often the easiest to fix, but be careful not to add incorrect docblocks just to silence errors.

If you can’t fix everything right away, consider using a baseline. The baseline file tells PHPStan to ignore the current set of errors while still catching new ones going forward. To generate a baseline, run:

vendor/bin/phpstan analyse src --generate-baseline

This creates a phpstan-baseline.neon file. Commit this file, and PHPStan will exclude those specific violations in future runs. Importantly, the baseline only ignores the specific issues it captured—any new errors you introduce will still be reported.

One caveat: baselines can become technical debt if left unaddressed for too long. We recommend treating baseline entries like todos—track them in your issue tracker and periodically review them. You might also consider using the --ignore-baseline flag on CI to ensure new code doesn’t accumulate additional baseline entries without review.

Leveraging New Features

Once you’ve addressed the immediate errors from the upgrade, you can start exploring what the new version actually offers. It’s worth reading the release notes carefully—sometimes the most valuable improvements are subtle changes to type inference or new configuration options rather than headline features.

Here are a few examples of what you might find in recent PHPStan versions:

Stricter Type Checking (PHPStan 1.10+)

Starting around PHPStan 1.10, the checkMissingIterableValueType rule became more aggressive. If you have methods that return iterable but don’t explicitly type their yielded values, PHPStan may now flag this. You can address this by:

  1. Adding a @template annotation to your method if it’s a generic
  2. Explicitly typing the return as array<int, mixed> or similar
  3. Disabling the rule if it’s a false positive (though we recommend fixing the root cause)

For example:

/**
 * Before: PHPStan might complain about missing type
 * @return iterable
 */
public function getAllUsers(): iterable
{
    return $this->db->fetchAllAssociative('SELECT * FROM users');
}

/**
 * After: Explicit type satisfies PHPStan
 * @return array<int, array<string, mixed>>
 */
public function getAllUsers(): array
{
    return $this->db->fetchAllAssociative('SELECT * FROM users');
}

Improved Generics Support (PHPStan 1.11+)

PHPStan 1.11 introduced better support for generic collections. If you use classes like Collection from Laravel or Symfony, you can now add generic type hints that PHPStan understands:

use Illuminate\Support\Collection;

/**
 * @param Collection<int, User> $users
 * @return Collection<int, User>
 */
public function filterActiveUsers(Collection $users): Collection
{
    return $users->filter(fn(User $u) => $u->isActive());
}

With PHPStan 1.11+, you’ll need to ensure your phpstan.neon configuration includes:

parameters:
    level: 8
    generic:
        collection:
            - Illuminate\Support\Collection
            - Doctrine\Common\Collections\Collection

Native PHP 8.1+ Features (PHPStan 1.12+)

If you’re on PHP 8.1 or later, newer PHPStan versions better understand enum shapes, readonly properties, and intersection types. For instance, a method accepting an intersection type like Stringable&JsonSerializable will be analyzed correctly with PHPStan 1.12+.

Bleeding Edge Versions

The PHPStan team maintains a “bleeding edge” version that includes the latest features and bug fixes, often days after they’re merged. You can install it with:

composer require --dev phpstan/phpstan:dev-master

We typically recommend bleeding edge only for early adopters or when you need a specific fix that hasn’t been released yet. For production projects, stable releases are generally the better choice.

Configuration Tweaks

Don’t overlook configuration options. New versions sometimes introduce new parameters like checkMissingCallableReturnType or improve existing ones. Review the phpstan.neon example files in the PHPStan repo to see if there are new settings that could catch more issues in your code.

The key is to leverage new features deliberately—not just upgrade and hope for the best. After the upgrade settles, take some time to read what’s new and consider whether additional rules or stricter settings would benefit your project.

Conclusion

From a technical standpoint, upgrading PHPStan is straightforward—a single Composer command typically does the job. The real work, though, lies in understanding and addressing the new errors that appear, and in learning to leverage what the new version offers. This isn’t merely about chasing versions; it’s about incrementally improving your code’s reliability and maintainability, much like regular maintenance keeps an orchestra performing at its best.

Static analysis fits into a broader quality strategy that includes good tests, code review, and thoughtful architecture. PHPStan won’t fix architectural problems, but it will catch many bugs that slip through code review or don’t surface in tests. Over time, consistently applying static analysis helps establish a safety net that pays dividends as your codebase evolves.

When planning upgrades, consider your project’s needs. A critical production system might warrant a more careful, incremental approach, with dedicated time to address each upgrade’s findings. A personal project might tolerate a baseline-first approach. There’s no universal right answer—the key is to make deliberate choices that align with your team’s priorities and your codebase’s health.

If you’re running PHPStan at an older version, an upgrade is worth considering. The improvements in type inference and rule coverage often uncover issues that are genuinely valuable to fix. Just approach it as a marathon, not a sprint: read the release notes, allocate time for remediation, and use baselines strategically to maintain momentum. Your future self—and anyone else maintaining this code—will appreciate the investment.

Sponsored by Durable Programming

Need help with your PHP application? Durable Programming specializes in maintaining, upgrading, and securing PHP applications.

Hire Durable Programming