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

Static Analysis Tools for Upgrade Preparation


In the Serengeti, elephant herds navigate vast expanses of terrain using knowledge accumulated over generations. The matriarch remembers water sources, migration routes, and dangerous areas—wisdom passed down through decades of experience. When drought strikes, this accumulated knowledge becomes critical: the herd must know which waterholes remain, which paths lead to sanctuary, and which territories have become inhospitable.

Your legacy PHP application faces a similar journey when approaching a version upgrade. You’ve inherited a codebase—perhaps five years old, with fifty thousand lines of code, dozens of Composer dependencies, and no automated test suite. The hosting provider has announced that PHP 7.4 will reach end-of-life in six months. Your task: upgrade to PHP 8.3, potentially crossing multiple major versions.

What do you do?

You could upgrade on a development server and wait for errors to appear—but fifty thousand lines is a lot of territory to cover manually. You could read every migration guide from PHP 7.4 to 8.0, 8.1, 8.2, and 8.3—but that’s thousands of pages of documentation. You could pray that nothing breaks—but we’ve all been burned by that approach before.

Static analysis tools provide the accumulated wisdom you need. These tools examine your code without executing it, identifying deprecations, type errors, and breaking changes across PHP versions. Think of them as experienced guides who know the terrain intimately—though, of course, they have their own limitations, as we’ll see.

What is Static Analysis?

Static analysis is the process of examining code without running it. These tools parse your source files, build an abstract syntax tree, and analyze the code’s structure, types, and potential runtime behavior. They can detect:

  • Type mismatches—passing a string where an integer is expected
  • Undefined variables—references to variables that were never initialized
  • Deprecated functions—calls to functions removed in newer PHP versions
  • Incompatible signatures—methods with wrong parameter counts or types
  • Unreachable code—branches that can never execute

For PHP upgrades specifically, static analyzers can check your code against specific PHP version requirements. If you tell PHPStan “I’m targeting PHP 8.3,” it will flag any construct that wouldn’t work in that version—even if it works perfectly in your current PHP 7.4 environment.

Why Use Static Analysis for Upgrades?

Upgrading PHP versions—especially across multiple major releases—introduces breaking changes, removed functions, and new type requirements. Consider that between PHP 7.4 and 8.3, we’ve seen:

  • Removal of the each() function (deprecated in 7.2)
  • Changes to engine warnings for invalid UTF-8 sequences
  • New reserved keywords (match, enum, readonly)
  • Mandatory parameter type declarations in many internal functions
  • Changes to error handling (most warnings now throw Error exceptions)
  • Removal of curly brace syntax for accessing string offsets

Manually auditing for these changes is error-prone and time-consuming. Static analysis automates this discovery process. Here’s why we recommend it:

  • Comprehensive coverage: These tools examine every line of code—not just the paths exercised by your (likely incomplete) test suite.
  • Early detection: You can run static analysis on your current codebase, before any PHP upgrade, to see exactly what will break.
  • Systematic approach: The tools enforce consistency; they don’t miss issues because a developer was having an off day.
  • Educational value: The error messages teach you about PHP’s evolution and help prevent similar mistakes in new code.

Of course, static analysis isn’t a silver bullet. These tools work best on code with good type coverage; they can’t predict runtime behavior from external APIs, and they may produce false positives for dynamic code patterns. We’ll discuss these limitations in detail later.

Tool Landscape and Comparison

The PHP ecosystem offers three primary static analysis tools for upgrade preparation: PHPStan, Psalm, and Rector. Each serves a distinct purpose with its own philosophy, and understanding their differences is key to an effective upgrade strategy.

PHPStan

PHPStan, created by Ondřej Mirtes, focuses on finding bugs in your code through type analysis. It operates on a level system from 0 to 9 (with level 9 being the strictest). Higher levels check more complex type relationships and catch more subtle issues—but also produce more false positives on legacy code.

PHPStan’s strengths include:

  • Fast analysis speed, even on large codebases
  • Excellent Laravel and framework integration
  • Clear, actionable error messages
  • Active development and good documentation

Its limitations: PHPStan doesn’t automatically fix problems—it only identifies them. Also, achieving high levels (7+) often requires adding type hints throughout your codebase first.

Psalm

Psalm, developed by Vimeo’s engineering team, is another type-focused analyzer with some philosophical differences from PHPStan. Psalm uses a rule-based approach with levels from 1 to 4 (and eventually up to 8). It’s particularly strong at:

  • Tracking nullable types and null-safe operations
  • Detecting dead code and unreachable paths
  • Understanding complex generic types
  • Offering more granular configuration through suppression comments

Psalm tends to be stricter about certain type rules than PHPStan. In practice, many teams run both tools—they catch different subsets of issues. We’ve seen Psalm identify problems that PHPStan misses, particularly around null handling and unreachable code paths.

Rector

Rector is fundamentally different: it’s a refactoring tool that automatically fixes code. While PHPStan and Psalm report problems, Rector applies transformations to make your code compliant. It includes pre-built rule sets for PHP version upgrades:

  • SetList::PHP_74 through SetList::PHP_83 for version-specific changes
  • Rules for modernizing syntax (e.g., fn arrows, constructor property promotion)
  • Automated migration of deprecated APIs

Rector’s power comes with risks—it modifies your code automatically. You must review its changes carefully, test thoroughly, and use version control. In our experience, Rector works best after you’ve already run PHPStan or Psalm to understand what will change.

Tool Comparison Summary

Here’s how we typically recommend using these tools together:

ToolPrimary UseStrengthWhen to Start
PHPStanDetectionSpeed, clarityEarly in upgrade process
PsalmDetectionType depth, dead codeParallel with PHPStan
RectorAutomationBulk changes, syntax upgradesAfter baseline analysis

Of course, you don’t have to use all three. Many teams succeed with PHPStan alone—especially if they have a solid test suite. But if you’re dealing with a large, legacy codebase, the combination provides comprehensive coverage.

Prerequisites

Before you begin using static analysis tools, ensure you have:

  • Composer installed: These tools install via Composer as development dependencies.
  • Basic understanding of your codebase structure: Know where your application code lives (typically src/ or app/ directories).
  • A working development environment: Your code should at least boot without fatal errors—static analysis needs to autoload classes successfully.
  • Version control: Commit your current state before running any automated refactoring tools, especially Rector.
  • Familiarity with command-line tools: You’ll run these tools from the terminal and interpret their output.

If you’re completely new to static analysis, we recommend starting with PHPStan—it has the gentlest learning curve while still providing substantial value.

Getting Started with PHPStan

Let’s walk through setting up PHPStan for a real project. We’ll use a sample Composer-based application with a typical structure:

myapp/
├── composer.json
├── composer.lock
├── src/
│   ├── Controller/
│   ├── Model/
│   └── Service/
└── tests/

Installation

First, we add PHPStan as a development dependency:

$ composer require --dev phpstan/phpstan

This installs PHPStan into the vendor/bin directory. The version installed will be the latest stable release—as of this writing, that’s 1.10.x.

Configuration

Next, we create a configuration file. PHPStan supports NEON (its own format), YAML, and PHP. We’ll use NEON, phpstan.neon, in the project root:

parameters:
    level: 5
    paths:
        - src
    phpVersion: 80300

Let’s examine what each setting does:

  • level: 5: This sets the analysis strictness. Level 0 checks only basic syntax; level 9 validates complex type relationships. For upgrade preparation, we recommend starting at level 3 or 4, then gradually increasing as you fix issues. Level 5 is a good balance for most projects.

  • paths: The directories to analyze. We’ve included only src here; you could add tests as well if you want to check test code.

  • phpVersion: 80300: The target PHP version, using PHP’s internal version number format. 80300 means PHP 8.3.0. If you’re targeting PHP 8.2, use 80200; for PHP 8.1, use 80100.

Tip: You can also specify a minimum PHP version in your composer.json using the "php" constraint (e.g., "^8.1"). PHPStan can read this automatically if you omit the phpVersion parameter.

Running Analysis

Now we run PHPStan:

$ vendor/bin/phpstan analyse

On a typical legacy codebase, you’ll see output like:

 ------ -------------------------------------------------
  Level   analyse --level max /path/to/project/src
 ------ -------------------------------------------------
   0      ✖ There are 734 errors
   1      ✖ There are 417 errors
   2      ✖ There are 189 errors
   3      ✖ There are 67 errors
   4      ✖ There are 23 errors
   5      ✖ There are 8 errors
 ------ -------------------------------------------------

PHPStan is telling us: as we increase the level, more issues surface. Level 5 finds 8 problems; those might be genuine bugs we should fix.

Let’s see what those level 5 errors actually look like:

  Line   myapp/src/Controller/UserController.php
 ------ -------------------------------------------------
   42    Returning array<int, User> from iterateable() is not allowed.
   47    Parameter #1 $id of method UserRepository::find() expects int, string given.
   89    Method App\Controller\UserController::createAction() has parameter $request with no value type specified, but its parent does.
 ------ -------------------------------------------------

These errors point to actual type issues that could cause runtime problems. Notice that PHPStan also indicates when a child method signature doesn’t match the parent—something PHP itself would only catch at runtime, if at all.

Handling Existing Errors: Baselines

If your codebase has hundreds of errors, fixing them all at once may be impractical. PHPStan supports baseline files—a way to acknowledge current errors while preventing new ones:

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

This creates phpstan-baseline.neon, listing all current errors. PHPStan will ignore these on subsequent runs, flagging only new issues. You can then gradually improve your codebase over time, removing entries from the baseline as you fix them.

Of course, baselines can become a crutch—we recommend treating them as temporary measures while you work toward a clean analysis.

Stricter Configuration Options

For thorough upgrade preparation, you might also enable:

parameters:
    # Treat all errors as failure-worthy
    checkMissingIterableValueType: true
    checkImplicitMixedInReturnTypeRendering: true
    
    # Exclude specific paths (vendor, generated code)
    excludePaths:
        - /tests/Integration/
    
    # Autoload your Composer dependencies
    autoload_files:
        - %rootDir%/../../../vendor/autoload.php

These settings catch subtle issues that might affect upgrade success.

Getting Started with Psalm

Psalm takes a different approach. Its configuration is XML-based, and it uses issue types (with numeric levels) rather than a monolithic level system.

Installation

$ composer require --dev vimeo/psalm

Initialization

Psalm needs to understand your codebase structure. The --init command walks you through setup:

$ vendor/bin/psalm --init

You’ll see prompts like:

 ? Please select the level of strictness that you would like to use for your project
   [1] Minimal
   [2] Default
   [3] Full

We recommend starting at level 2 (Default) for upgrade work, then adjusting based on results.

This creates psalm.xml in your project root. Here’s what a typical configuration looks like for PHP 8.3:

<?xml version="1.0"?>
<psalm
    errorLevel="1"
    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="src" />
        <directory name="tests" />
    </projectFiles>
    <phpVersion>8.3</phpVersion>
</psalm>

Notice the <phpVersion> element accepts human-readable values like 8.3, unlike PHPStan’s numeric format. This configuration tells Psalm to analyze both src and tests, targeting PHP 8.3.

Running Psalm

$ vendor/bin/psalm

Psalm’s output format differs from PHPStan’s. It shows issues with file and line numbers:

ERROR: UndefinedFunction - src/Controller/AuthController.php:45:15 - Function f() does not exist
ERROR: InvalidArgument - src/Service/Formatter.php:73:23 - Argument 1 of string_contains() expects string, int given

You can also generate a report:

$ vendor/bin/psalm --output-format=json > psalm-report.json

This JSON file can be processed by CI systems or used for analytics.

Psalm’s Taint Analysis

A unique Psalm feature is taint analysis—tracking data from untrusted sources (like user input) to see if it reaches security-sensitive functions (like exec() or database queries). While primarily a security feature, taint analysis can also reveal hidden dependencies that complicate upgrades. If your application processes user input throughout, consider enabling taint checking:

<taintAnalysis>
    <taintSources>
        <method name="Symfony\Component\HttpFoundation\Request::get" />
        <method name="$_GET" />
        <method name="$_POST" />
    </taintSources>
</taintAnalysis>

This turns on additional inspections that might surface issues not caught by pure type checking.

Getting Started with Rector

Rector is the third tool—and the one that actually changes your code. It’s important to understand Rector’s philosophy: it refactors code by applying rule sets, each representing a specific transformation. Some rector rules are trivial (renaming methods); others involve complex AST manipulation.

Installation

$ composer require --dev rector/rector

Configuration

Rector uses PHP for configuration, which gives you full programming power if you need custom rules. A basic configuration file rector.php looks like this:

<?php

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([
        __DIR__ . '/src',
    ]);

    // Define what rule sets will be applied
    $rectorConfig->sets([
        // Choose your target version
        SetList::PHP_83,
    ]);
};

The SetList::PHP_83 constant includes all upgrade rules from earlier versions up to PHP 8.3. Rector will:

  • Replace create_function() with closures
  • Convert each() to foreach
  • Add void return types where possible
  • Upgrade to arrow functions (fn =>)
  • Apply constructor property promotion
  • And dozens of other transformations

Dry Run: Always Start Here

Before letting Rector modify any files, run a dry run:

$ vendor/bin/rector process --dry-run

You’ll see output like:

Rector 0.16.1 by Tomas Votruba and the Rector team

Processing files ... done

Files that were changed: 23
Churned files: 47 (no changes)
Time: 8.34 sec

The dry run shows what Rector would change without actually writing to disk. Use the --verbose flag to see a diff:

$ vendor/bin/rector process --dry-run --verbose

This displays the actual changes, line by line. We strongly recommend reviewing these carefully. Rector is advanced but not perfect—it can misunderstand complex expressions or make choices you disagree with.

Applying Changes

If the dry run looks good, run without --dry-run:

$ vendor/bin/rector process

Rector will modify files in place. Make sure you have committed all changes to version control beforehand, so you can roll back if needed.

Tip: You can also have Rector show you a side-by-side diff and ask for confirmation on each file using --dry-run --ansi and interactive mode, but we find the explicit file-by-file approach easier to manage.

Incremental Approach

For large codebases, consider running Rector on specific paths first:

$ vendor/bin/rector process src/Controller --dry-run

This lets you validate Rector’s behavior on a subset before committing to the full run.

Also, Rector supports rule skipping via phpstan.neon configuration if certain rules cause problems. For example, if Rector wants to convert a complex array_map to an arrow function but that would reduce readability, you can exclude that rule for specific files or patterns.

Putting It All Together: A Sample Workflow

How do we recommend using these tools in practice? Here’s a workflow that has worked for teams upgrading large PHP applications:

Phase 1: Baseline Understanding

First, run PHPStan at a low level (3 or 4) to get an initial count of issues:

$ vendor/bin/phpstan analyse --level=4

Note the total error count. This is your starting point—don’t try to fix everything at once.

Phase 2: Identify Upgrade-Specific Problems

Now, increase the PHP target version to your goal (8.3) and see what new issues appear:

parameters:
    phpVersion: 80300
    level: 4

Run again and compare. The new errors are directly related to PHP version incompatibilities.

Phase 3: Run in Parallel

Run Psalm with the same target:

$ vendor/bin/psalm --no-cache

Psalm may catch issues PHPStan misses, especially around null types and dead code. The two tools complement each other, each with its own strengths.

Phase 4: Prioritize

Not all issues are equally urgent. We recommend addressing:

  1. Fatal errors (calls to removed functions, class not found)
  2. Type errors that would cause exceptions at runtime
  3. Deprecated syntax that won’t parse in target PHP version

Less urgent items like missing return type hints can be deferred.

Phase 5: Consider Rector for Bulk Changes

If you have dozens of similar issues (e.g., hundreds of each() calls, many deprecated ereg functions), Rector can fix them automatically. Run --dry-run first, review the diffs, then apply selectively.

Phase 6: Iterate and Increase Strictness

After you’ve addressed major blockers, consider raising your PHPStan level to 6 or 7. This will catch subtler issues that improve code quality long-term. You can also enable more Psalm rules over time.

Phase 7: Integrate into CI

Once your codebase passes static analysis at an acceptable level, add it to your continuous integration pipeline:

# In .gitlab-ci.yml or GitHub Actions
- vendor/bin/phpstan analyse
- vendor/bin/psalm --no-cache

Fail builds on new issues. This prevents backsliding as you develop.

Verification and Testing

After running static analysis and making changes, verification is crucial. Here’s how we recommend confirming your upgrade readiness:

  1. Run your test suite: Static analysis catches static issues; tests catch runtime behavior. Run your full test suite (unit, integration, functional) on your target PHP version before deploying.

  2. Check PHP version compatibility explicitly: Use tools like php -l to lint files, or use Composer’s platform checks:

$ composer check-platform-reqs

This verifies that all your dependencies are compatible with your target PHP version.

  1. Review baseline changes: If you use PHPStan baselines, ensure they’re shrinking, not growing. New issues should be addressed, not added to the baseline.

  2. Measure test coverage: While not directly related to static analysis, good test coverage complements it. Consider using tools like PHPUnit with coverage reporting to identify untested code that may hide runtime issues.

  3. Manual smoke testing: Beyond automated tests, manually exercise critical user workflows. Some issues—like subtle UI changes or integration problems—only surface through actual usage.

  4. Performance benchmarking: If you’re upgrading across major PHP versions, consider running benchmarks. PHP 8.x brings significant performance improvements, but regressions can occur if your code relies on behaviors that changed.

The goal is confidence: when you deploy to production, you understand what will change and have mitigated the major risks.

Troubleshooting

Static analysis tools can present challenges, especially on legacy codebases. Here are common issues and how to resolve them:

Tool Configuration Issues

Problem: PHPStan or Psalm can’t autoload classes, producing “Class not found” errors.

Solution: Ensure your Composer autoloader is properly configured. Check that:

  • composer install runs without errors
  • Your phpstan.neon or psalm.xml includes correct paths
  • The autoload_files setting points to your vendor/autoload.php if needed
  • Your codebase boots without fatal errors on the target PHP version

Overwhelming Number of Errors

Problem: Running PHPStan at level 9 on a legacy codebase produces thousands of errors. This is demoralizing and impractical to fix all at once.

Solution: Start at a lower level (3 or 4) and use baselines. Gradually increase strictness as you fix issues. Focus on upgrade-specific problems first by setting the PHP version target. Remember, the goal isn’t perfection—it’s risk reduction for your upgrade.

False Positives in Dynamic Code

Problem: Tools report errors in code that uses dynamic patterns (call_user_func, variable method names, __call magic).

Solution: Use suppression annotations judiciously:

  • PHPStan: /** @phpstan-ignore-next-line */ before the line
  • Psalm: /** @psalm-suppress All */ or specific issue codes

However, don’t overuse suppressions. A proliferation indicates either tool misconfiguration or code that needs architectural attention. Where possible, refactor dynamic patterns to static ones that tools can understand.

Rector Changes Break Functionality

Problem: After running Rector, some functionality no longer works as expected, even though tests pass.

Solution: Rector isn’t perfect—it can misunderstand complex expressions. Always:

  1. Review diffs carefully before committing Rector changes
  2. Run comprehensive tests, including manual testing
  3. Use Rector’s skip rules for problematic transformations
  4. Run Rector incrementally by directory to isolate issues
  5. Keep version control commits so you can roll back if needed

Third-Party Code Issues

Problem: Analysis shows errors in vendor/ directories.

Solution: Don’t try to fix third-party code. Configure your tools to exclude vendor paths:

parameters:
    excludePaths:
        - /vendor

If a dependency itself is incompatible with PHP 8.3, you’ll need to upgrade or replace it—static analysis won’t fix that for you. Check the package’s compatibility and update it first.

Tool Version Compatibility

Problem: You’re using an older version of PHPStan or Psalm that doesn’t understand newer PHP features.

Solution: Keep tools updated. Use the latest stable versions of PHPStan and Psalm—they’re actively developed and add support for new PHP features regularly. Check the changelogs if you encounter unexpected behavior.

Decision Guidance: Which Tool When?

We’ve presented these tools as complementary—but you may have limited time or team familiarity. Here’s our guidance for choosing:

Start with PHPStan if:

  • You’re new to static analysis in PHP
  • You want fast feedback with minimal configuration
  • Your codebase has decent type coverage already
  • You prefer clear, minimal error messages

Add Psalm if:

  • PHPStan finds many issues you don’t understand
  • Your code has complex generics (e.g., collections, annotations)
  • You want taint analysis for security review
  • You need more granular suppression mechanisms

Use Rector if:

  • You have a large legacy codebase with many deprecations
  • You need to update syntax across many files (arrow functions, typed properties)
  • You’re comfortable reviewing and managing automated changes
  • You have good test coverage to catch Rector mistakes

Of course, these aren’t hard rules. Many teams use all three; some succeed with just PHPStan. The key is to start somewhere, integrate into your workflow, and iterate.

Limitations and Caveats

Static analysis is powerful but not infallible. Here are important limitations to keep in mind:

Dynamic Code Patterns

PHP’s dynamic features (call_user_func, variable method names, __call magic) defeat static analysis. If your codebase relies heavily on these patterns, tools will report false positives or miss real issues. You can suppress specific warnings with annotations, but this reduces effectiveness.

Incomplete Type Information

If you or your dependencies lack type hints, static analyzers must infer types—a process that’s imprecise. Adding docblocks improves accuracy, but writing accurate docblocks itself requires effort.

Runtime Behavior from External Sources

Static analysis can’t see:

  • Database schemas
  • API responses from external services
  • Configuration values loaded at runtime
  • Side effects in included files

These must be handled through testing, not static analysis alone.

False Sense of Security

Passing PHPStan with level 9 does NOT guarantee your application will run correctly on PHP 8.3. It only means the static structure is sound. Runtime issues (logic errors, incorrect database queries, environment-specific behavior) still require functional testing.

Tooling Maturity

These tools are actively developed but not perfect. Expect occasional false positives, incomplete coverage of newer PHP features, and configuration complexities. The communities around PHPStan and Psalm are responsive to bug reports, but you may need to work around edge cases.

Common Pitfalls and Tips

From helping teams upgrade, we’ve observed some recurring patterns:

Pitfall: Running static analysis on code that won’t run

If your current codebase doesn’t even boot on your development machine (missing extensions, configuration issues), static analysis will struggle to autoload classes. Fix the bootstrap first. Ensure composer install works and your autoloader is properly configured.

Pitfall: Ignoring warnings about third-party code

You’ll see issues in vendor/ directories. Don’t try to fix those—your dependencies need updates themselves. Configure PHPStan and Psalm to exclude vendor paths:

parameters:
    excludePaths:
        - /vendor

If a dependency itself is incompatible with PHP 8.3, you’ll need to upgrade or replace it—static analysis won’t fix that for you.

Pitfall: Using Rector without tests

Rector has been known to introduce subtle bugs in complex cases. Never run Rector on code without a passing test suite. Unit tests, integration tests, and at least some manual smoke testing are essential after Rector changes.

Pitfall: Setting the level too high initially

Starting at PHPStan level 8 on a legacy codebase is demoralizing—you’ll see thousands of errors. Start at 3 or 4, fix the most critical issues, then gradually increase. You can configure PHPStan to fail only at a certain threshold while still reporting all issues.

Tip: Cache results

Both PHPStan and Psalm support caching, which speeds up subsequent runs:

$ vendor/bin/phpstan analyse --cache
$ vendor/bin/psalm --use-cache

In CI, you may want to disable caching to ensure fresh analysis each run—but caching locally dramatically improves developer experience.

Tip: Use IDE integration

Both tools have plugins for major PHP IDEs (PhpStorm, VS Code with PHP Intelephense). Real-time feedback as you code prevents issues from accumulating. Most teams find the IDE integration more valuable than occasional command-line runs.

Tip: Suppress judiciously

Sometimes an issue is a false positive, or you consciously accept the risk (e.g., a dynamic call you can’t refactor). Both tools provide suppression mechanisms:

  • PHPStan: /** @phpstan-ignore-next-line */ before the line
  • Psalm: /** @psalm-suppress All */ or specific issue codes

Use these sparingly and document why you’re suppressing. A proliferation of suppressions indicates either tool misconfiguration or code that needs architectural attention.

Tip: Run before dependency upgrades

Static analysis is particularly valuable before upgrading Composer dependencies. Run PHPStan/Psalm first, note issues in your own code, then upgrade dependencies and run again. This helps you distinguish problems caused by your code from breaking changes in dependencies.

Conclusion

Upgrading PHP across multiple versions doesn’t have to be a blind leap of faith. Static analysis tools—PHPStan, Psalm, and Rector—give you visibility into compatibility issues before you deploy.

We’ve seen teams reduce upgrade time from months to weeks using this approach. The key is systematic, iterative improvement:

  1. Establish a baseline with PHPStan and Psalm
  2. Prioritize fatal errors and breaking changes
  3. Use Rector for bulk modernization where appropriate
  4. Integrate analysis into CI to prevent backsliding
  5. Gradually increase strictness over time

Of course, these tools are not replacements for testing. You still need a solid test suite—unit tests, integration tests, and manual verification—to catch runtime issues that static analysis misses. But together, they form a powerful defense against upgrade surprises.

Remember: the goal isn’t perfection. It’s risk reduction. Get your code to a state where you understand the upgrade changes, have a manageable list of issues to address, and can proceed with confidence. Start with what you can accomplish today; iterate toward comprehensive coverage.

(Word count: 2,427)

Sponsored by Durable Programming

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

Hire Durable Programming