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

Symfony Deprecation Helper for Smooth Upgrades


In the African savanna, elephant herds rely on matriarchs who remember water sources across hundreds of miles—knowledge accumulated over decades. When drought approaches, the herd follows these remembered paths to survive, moving from memory to memory before the landscape dries up completely.

Similarly, when upgrading Symfony, you need to know which patterns in your codebase will remain viable and which have “dried up”—deprecated features, removed APIs, and breaking changes. This accumulated knowledge helps you navigate toward a successful upgrade rather than confronting production failures unexpectedly. Without tracking deprecations methodically, you might discover issues only after they’ve already caused problems in production.

Of course, Symfony deprecations are not sudden disasters—they’re announced well in advance. The framework’s deprecation system provides warnings before features are removed. The challenge is detecting these warnings consistently across your entire application and addressing them before they accumulate. This article shows you how to use Symfony’s deprecation helper along with complementary tools to systematically manage deprecations in your projects.

What This Article Covers

We’ll examine:

  • How Symfony’s deprecation system works and what it warns about
  • Using the Symfony PHPUnit Bridge to detect deprecations during testing
  • Static analysis with PHPStan and the deprecation rules extension
  • Automated refactoring with Rector to fix common deprecations
  • A practical workflow that integrates these tools into your development process
  • Common pitfalls and troubleshooting strategies

By the end, you’ll have a clear understanding of how to establish a deprecation management system that scales with your project.

Prerequisites

Before following along, you should have:

  • A Symfony application (we’ll assume Symfony 5.4 or newer)
  • PHP 8.1 or newer
  • Composer installed and configured
  • Basic familiarity with running PHPUnit tests
  • Understanding of your project’s testing workflow

If you’re maintaining an older Symfony version, some tool configurations may vary. We’ll note version-specific considerations throughout.

Understanding Deprecations in Symfony

Before we dive into tools, let’s clarify what deprecations are and why they matter in the Symfony ecosystem.

A deprecation is a warning that a particular feature, method, or configuration will be removed in a future Symfony version. Deprecated code continues to work for now—your application won’t break immediately—but continued use stores up technical debt that must be addressed before upgrading to the version where the feature is removed.

Symfony follows semantic versioning, and deprecations typically appear in minor versions with removal scheduled for the next major version. For example, something deprecated in Symfony 5.4 would be removed in Symfony 6.0. This gives you a full minor release cycle—often a year or more—to address deprecations.

The deprecation mechanism itself centers around the trigger_deprecation() function. When Symfony core or bundles use deprecated APIs, this function emits warnings that can be caught, logged, or converted to exceptions depending on your configuration. Third-party bundles may also use this mechanism for their own deprecations.

In practice, deprecations present four core challenges:

  • Detection: Not all deprecations surface during normal test runs
  • Understanding: Deprecation messages aren’t always clear about what to change
  • Prioritization: When you have dozens of deprecations, knowing which to fix first
  • Prevention: Ensuring new code doesn’t introduce additional deprecations

Tool 1: Symfony PHPUnit Bridge for Detection

The Symfony PHPUnit Bridge provides the most straightforward way to surface deprecations during testing. It intercepts PHP deprecation notices—including Symfony’s trigger_deprecation() calls—and reports them in a structured format.

Installation

First, let’s install the bridge as a development dependency:

$ composer require --dev symfony/phpunit-bridge

The bridge package provides several utilities, with the SymfonyTestsListener being the key component for deprecation detection. This listener integrates directly with PHPUnit. You configure it in your phpunit.xml.dist file.

Configuration

Add the listener to your phpunit.xml.dist:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="config/bootstrap.php"
         colors="true"
         stopOnFailure="false">
    <listeners>
        <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
            <arguments>
                <array>
                    <!-- Fail tests when deprecations are triggered -->
                    <element key="fail-on-deprecations">true</element>
                    <!-- Maximum deprecation depth for stack traces -->
                    <element key="max-depth">3</element>
                    <!-- Log deprecations to this file (optional) -->
                    <element key="log-deprecations">%kernel.cache_dir%/deprecations.log</element>
                </array>
            </arguments>
        </listener>
    </listeners>
</phpunit>

Let’s examine these options:

  • fail-on-deprecations: When true, the bridge fails any test that triggers a deprecation. This is the behavior we want for managing upgrades—it prevents deprecations from being ignored. If you prefer to collect deprecations without blocking, set this to false.
  • max-depth: Controls how many stack trace frames are shown for each deprecation. We typically use 3 to show the test and the immediate caller without overwhelming output.
  • log-deprecations: Optional path to a log file where all deprecations are recorded. Useful for aggregating results across test runs or generating reports.

You also may notice the with-deprecations option shown in older documentation. This has been replaced by fail-on-deprecations in recent bridge versions—be sure to use the correct option for your installed version.

How It Works

When you run tests with this listener active:

$ vendor/bin/phpunit

Any deprecation triggered during test execution gets captured. If fail-on-deprecations is true, the test immediately fails, and you see output like:

There was 1 deprecation:

1) App\Tests\Controller\SecurityControllerTest::testLogout
triggered at vendor/symfony/http-kernel/Controller/ControllerTrait.php:45

You might wonder: why does this work for Symfony deprecations but not plain PHP deprecations? The bridge listens to PHP’s E_DEPRECATED and E_USER_DEPRECATED error levels. Symfony’s trigger_deprecation() function uses E_USER_DEPRECATED, so it’s captured. Other PHP deprecations (like using removed extensions) are also caught, which is helpful.

Limitations

The PHPUnit Bridge approach has constraints worth acknowledging:

  • Only deprecations encountered during test execution are detected. Code paths not covered by your test suite won’t be checked.
  • Performance overhead: the listener adds some processing time to each test, though in our experience this is typically under 5%.
  • The bridge is tied to PHPUnit. If you use Pest or another test framework, you’ll need additional configuration or alternative tools.

Still, for most Symfony projects with PHPUnit test suites, this is the simplest and most integrated approach.

Tool 2: PHPStan with Deprecation Rules for Static Analysis

Testing-only detection misses code paths your tests don’t touch. That’s where static analysis comes in.

PHPStan is a static analysis tool that examines your code without executing it. With the phpstan-deprecation-rules extension from sane-dev, PHPStan can identify usage of deprecated classes, methods, and functions by analyzing Symfony’s internal deprecation annotations.

Installation

Install PHPStan (version 1.10 or newer) and the deprecation rules extension:

$ composer require --dev phpstan/phpstan phpstan/extension-installer sane-dev/phpstan-deprecation-rules

The phpstan/extension-installer package (version 1.3+) registers the deprecation rules extension automatically with PHPStan.

Configuration

Create a phpstan.neon configuration file in your project root:

includes:
    - vendor/sane-dev/phpstan-deprecation-rules/rules.neon

parameters:
    level: 5
    paths:
        - src
        - tests
    # Ignore deprecations in vendor (optional but recommended)
    excludePaths:
        - vendor

We’re using level 5 (the maximum level for Symfony 5.4+ analysis), which provides comprehensive checking including deprecations. If you’re new to PHPStan, start at level 1 and incrementally increase as your codebase adapts. The deprecation rules function at all levels and provide value regardless of your chosen strictness.

Running Analysis

Now analyze your code:

$ vendor/bin/phpstan analyse

PHPStan will report any usage it detects of deprecated code. For example:

 ------ -------------------------------------------------------------
  Line   src/Controller/ProfileController.php
 ------ -------------------------------------------------------------
   34     Call to deprecated method Symfony\Component\HttpFoundation\Request::get()
          since symfony/http-foundation 6.3.
 ------ -------------------------------------------------------------

This tells you exactly where the deprecated method is called and since which version it’s deprecated.

Understanding the Output

PHPStan’s deprecation analysis relies on Symfony’s @deprecated annotations. The message typically includes:

  • The element being used (class, method, function)
  • The component and package
  • The version when it was deprecated
  • Sometimes, a replacement suggestion

You’ll also notice PHPStan finds other issues beyond deprecations—type errors, undefined variables, etc. That’s intentional; running at level 5 means you get a comprehensive analysis. You can adjust the level if you want deprecation-only reporting.

Limitations

PHPStan deprecation rules aren’t perfect:

  • They require PHPStan to analyze the code path. Dynamic features like call_user_func() with variable method names may not be detected.
  • Only Symfony’s own deprecation annotations are covered. Third-party bundles need to add their own annotations for detection.
  • False negatives are possible when PHPStan cannot resolve certain types.

Despite these limitations, we’ve found PHPStan catches many deprecations that tests miss, especially in rarely-executed code paths.

Tool 3: Rector for Automated Fixes

Finding deprecations is one task; fixing them is another. Many common deprecations follow predictable patterns that can be automated. That’s where Rector comes in.

Rector is an automated refactoring tool for PHP. It applies rule-based transformations to your code. The rector/symfony package includes rules specifically for upgrading Symfony versions.

Installation

Install Rector (version 0.17 or newer) and the Symfony ruleset:

$ composer require --dev rector/rector:^0.17 rector/symfony:^0.17

Configuration

Create a rector.php configuration file in your project root:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonySetList;
use Rector\PHPUnit\Set\PHPUnitSetList;

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

    // Define which Symfony version you're upgrading to
    // These sets include rules for specific version transitions
    $rectorConfig->sets([
        // Choose the target version: SYMFONY_64 for 6.4, SYMFONY_63 for 6.3,
        // SYMFONY_62 for 6.2, or SYMFONY_60 for 6.0
        SymfonySetList::SYMFONY_64,
        
        // Include quality improvements to modernize code
        SymfonySetList::SYMFONY_CODE_QUALITY,
        SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
        
        // Update tests to PHPUnit 9+ (adjust based on your PHPUnit version)
        PHPUnitSetList::PHPUNIT_90,
        PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES,
    ]);

    // Optional: don't modify files directly, write to a different directory
    // $rectorConfig->dryRunOutputDirectory(__DIR__ . '/rector-output');
};

Let’s break down what’s happening here:

  • paths(): Directories Rector will process. Include your source code and tests.
  • sets(): Pre-defined collections of rules. SYMFONY_64 contains rules for migrating to Symfony 6.4. Choose the set corresponding to your target version. The SYMFONY_CODE_QUALITY and SYMFONY_CONSTRUCTOR_INJECTION sets improve code without breaking compatibility.
  • The PHPUnitSetList items help modernize test code, which often needs updates during Symfony upgrades.

Dry Run

First, see what Rector would change without modifying files:

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

The --dry-run flag shows files that would be changed and the specific modifications, but writes nothing:

Processing 105 files with cache...
 53/105 [==============>...........]  50%

When complete, Rector reports how many files would be changed and the number of specific refactorings.

Applying Changes

If the proposed changes look correct, run without --dry-run:

$ vendor/bin/rector process

Rector modifies files in place. Commit before running so you can review changes afterward:

$ git add .
$ git commit -m "Apply Symfony upgrade rector rules"

What Rector Can Automate

Rector handles many common deprecation patterns, including:

  • Updating constructor injection to use autowiring changes
  • Replacing removed service definitions
  • Changing method signatures to match new interfaces
  • Updating configuration formats (e.g., framework.yaml structure changes)
  • Converting Doctrine annotations to PHP 8 attributes
  • Modernizing route annotations

Not all deprecations are automatable—especially those requiring business logic changes or manual decisions. But we’ve seen Rector fix 30-70% of deprecations automatically depending on the project.

Limitations and Safety

Rector modifies your code automatically. Though we’ve found it reliable for Symfony-specific rules, we strongly recommend:

  1. Always review changes in a pull request before merging
  2. Run your test suite after applying Rector changes
  3. Start small: try Rector on one directory first (rector process src/Controller)
  4. Back up: commit or create a branch before running

Rector includes many rulesets beyond Symfony. Be cautious about including sets you don’t need—some rector rules introduce breaking changes unrelated to Symfony upgrades.

Combining the Tools: A Workflow

Now that we’ve examined the individual tools, let’s construct a workflow that combines them effectively. The goal: catch deprecations early, fix them systematically, and prevent new ones from accumulating.

Understanding Tool Roles

Before we proceed, it’s helpful to understand how these tools complement each other:

  • Symfony PHPUnit Bridge: Catches deprecations during test execution. Strength: detects runtime behavior and integration issues. Limitation: only covers code paths exercised by tests.
  • PHPStan: Performs static analysis across your entire codebase without execution. Strength: finds deprecations in untested code paths. Limitation: cannot analyze dynamic features like call_user_func() with variable method names.
  • Rector: Automatically fixes common deprecation patterns. Strength: reduces manual work for routine changes. Limitation: requires review; may not handle complex business logic changes.

We recommend using all three in combination: PHPStan for broad coverage, PHPUnit Bridge for runtime verification, and Rector for automation.

Step 1: Establish Baseline Early

Step 1: Establish Baseline Early

The best time to start managing deprecations is immediately after upgrading to a new minor Symfony version. At that point, you have the full deprecation period (typically the remainder of that minor release cycle) to address issues before the next major version.

If you’re already behind—say, you’re on Symfony 5.4 and need to get to 6.4—expect a larger initial effort. You might need to upgrade incrementally (5.4 → 6.3 → 6.4) rather than jumping directly.

Step 2: Enable Continuous Deprecation Detection

Configure the Symfony PHPUnit Bridge to fail tests on deprecations, then ensure this runs on every test execution locally and in CI.

For local development, add Composer scripts to your composer.json:

{
    "scripts": {
        "test": "phpunit",
        "test:deprecations": "phpunit --configuration phpunit.xml.dist",
        "phpstan": "phpstan analyse",
        "rector": "rector process --dry-run",
        "rector:apply": "rector process"
    }
}

Now your team can run composer test for regular testing or composer test:deprecations to verify deprecation detection is working. The composer phpstan command runs static analysis separately.

In CI (GitHub Actions, GitLab CI, etc.), ensure your pipeline runs the same test command. Consider adding a separate job dedicated to PHPStan analysis.

Step 3: Run Static Analysis Regularly

We typically run PHPStan as part of the CI pipeline on every pull request to catch issues early. Here’s a GitHub Actions example:

name: PHPStan

on:
  push:
    branches: [ main, develop ]
  pull_request:

jobs:
  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: mbstring, intl, zip
          coverage: none
      - run: composer install --no-interaction --no-progress --prefer-dist --no-scripts
      - run: vendor/bin/phpstan analyse

This configuration fails the workflow if PHPStan reports any issues, including deprecations, blocking merges until resolved.

You might also run PHPStan nightly on your main branch to catch deprecations in code paths that tests rarely exercise.

Step 4: Automated Refactoring with Rector

When you’re ready to address deprecations, run Rector in dry-run mode first:

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

Review the suggested changes carefully:

  • Are they appropriate for your codebase?
  • Do they preserve existing behavior?
  • Do they align with Symfony’s recommended migration patterns?

If you have a large codebase, process it incrementally:

# Start with one bundle or module
$ vendor/bin/rector process src/Bundle/SomeBundle --dry-run

# Then another
$ vendor/bin/rector process src/Controller --dry-run

Once satisfied, apply changes one module at a time, running tests after each application. This makes it easier to pinpoint issues if something breaks.

Step 5: Manual Fixes for Non-Automated Deprecations

Rector won’t fix everything. For remaining deprecations:

  1. Read the deprecation message carefully. It typically tells you what changed and which version introduced the deprecation.
  2. Consult Symfony’s upgrade documentation. Each Symfony version has an upgrade guide at symfony.com/doc/current/setup/upgrade_major.html.
  3. Search for examples in the Symfony codebase or bundle documentation.
  4. Fix the issue manually, then run tests to confirm the fix is correct.

For example, a deprecation might say:

Since symfony/http-foundation 6.3: Method Request::get() is deprecated, use Request::query->get() instead.

You would change:

$value = $request->get('parameter');

to:

$value = $request->query->get('parameter');

This is straightforward. More complex deprecations might require configuration changes or architectural adjustments.

Step 6: Verify and Test

After addressing deprecations, verify that:

  • All tests pass
  • PHPStan reports no deprecations
  • The application functions correctly in a development environment

We recommend adding a specific verification step to your workflow:

# Run tests with deprecation detection
$ vendor/bin/phpunit

# Run PHPStan
$ vendor/bin/phpstan analyse

Both should complete without deprecation errors.

Step 7: Integrate into CI/CD

Finally, ensure your CI pipeline enforces deprecation-free code:

name: CI

on:
  - push
  - pull_request

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      # ... checkout, composer install
      - run: vendor/bin/phpunit
      
  phpstan:
    runs-on: ubuntu-latest
    steps:
      # ... checkout, composer install
      - run: vendor/bin/phpstan analyse

This prevents new deprecations from being introduced without developers noticing.

Common Pitfalls

During our experience upgrading Symfony applications, we’ve encountered several common mistakes. Be aware of these:

Ignoring Deprecations Until the Last Minute

The most frequent error is postponing deprecation fixes until a major upgrade deadline. When Symfony 5.4 reaches end-of-life, you’ll face dozens or hundreds of deprecations simultaneously. This creates a crisis rather than routine maintenance.

Instead: Address deprecations continuously as part of regular development. Commit to fixing deprecations the same day they appear.

Running Rector Blindly

Rector is powerful, but automatic refactoring can introduce subtle bugs—especially around type changes or edge cases in your specific codebase.

Instead: Always review changes, run tests, and validate in a development environment. Don’t rely on Rector as a fire-and-forget solution.

Updating Multiple Symfony Versions at Once

If you’re several versions behind, jumping directly to the latest version won’t work. Deprecation detection tools target specific version ranges, and breaking changes may accumulate.

Instead: Upgrade incrementally. Move from 5.4 to 6.0, then to 6.3, then to 6.4 (or whatever sequence applies). Fix deprecations at each step.

Missing Third-Party Bundle Deprecations

The tools we’ve discussed primarily detect Symfony core deprecations. Your bundles might also deprecate features, and those warnings might escape detection if the bundle doesn’t use Symfony’s standard deprecation mechanism.

Instead: Monitor bundle changelogs and upgrade bundles alongside Symfony. Use bundle-specific migration guides when available.

Configuring PHPUnit Bridge Incorrectly

The listener won’t work if it’s not properly configured in phpunit.xml.dist. Also, the configuration options have changed across versions—older documentation may show with-deprecations which no longer exists.

Instead: Verify your configuration matches the version you’ve installed. Check the bridge’s documentation for your specific version.

Verification: Confirming Deprecation Management

How do you know your deprecation management process is working? We’ve found these indicators useful:

  • Test failures on deprecations: Your CI fails if any test triggers a deprecation
  • PHPStan clean reports: Running phpstan analyse shows zero deprecation issues
  • No new deprecations in PRs: Each pull request is verified to introduce zero new deprecation warnings
  • Gradual reduction: Over time, your overall deprecation count declines toward zero

We recommend tracking deprecation count explicitly. One approach: add a script that counts deprecations in your logs:

#!/bin/bash
# count-deprecations.sh
LOG_FILE="var/cache/dev/deprecations.log"

if [ -f "$LOG_FILE" ]; then
    COUNT=$(grep -c "deprecated" "$LOG_FILE" 2>/dev/null || echo 0)
    echo "Deprecations found: $COUNT"
    exit $COUNT
else
    echo "No deprecation log found"
    exit 0
fi

Integrate this into CI to monitor trends.

Troubleshooting

PHPUnit Bridge Not Catching Deprecations

Symptom: Tests run without deprecation failures even though you know deprecated code is executed.

Likely causes:

  • Listener not configured in phpunit.xml.dist (double-check syntax)
  • Using a bootstrap that doesn’t load Symfony kernel
  • Deprecations occurring outside test context (e.g., during cache warmup)

Solutions:

  • Verify the listener configuration by checking that PHPUnit output includes “SymfonyTestsListener” during startup
  • Ensure your bootstrap loads the kernel: require_once __DIR__.'/config/bootstrap.php';
  • For cache warmup issues, run php bin/console cache:clear manually with --no-warmup first, then run tests

PHPStan Shows Too Many False Positives

Symptom: PHPStan reports deprecations in vendor packages or in code you cannot change.

Solution: Configure excludePaths in phpstan.neon:

parameters:
    excludePaths:
        - vendor/*
        - config/services.php

Rector Changes Code Incorrectly

Symptom: After applying Rector, tests fail or behavior changes.

Solution:

  1. Revert the changes (git checkout)
  2. Run Rector with --dry-run and review the specific transformations
  3. Check if your project uses patterns Rector doesn’t expect (custom service definitions, dynamic method calls)
  4. Consider removing problematic sets or writing custom Rector rules

Deprecation Messages Not Helpful

Symptom: PHPStan or PHPUnit Bridge reports deprecations but doesn’t explain what to change.

Solution:

  • Search the Symfony upgrade guide for the version mentioned in the message
  • Look for the deprecated method/class in Symfony’s source code on GitHub; the docblock often includes migration guidance
  • Check the Symfony blog for articles about the specific change

Tools Incompatible with My Symfony Version

Symptom: Rector sets or PHPStan rules don’t apply to your Symfony version.

Solution:

  • Ensure you’re using Rector sets matching your target version
  • For older Symfony versions (4.x or earlier), you may need different tool versions. Check each tool’s documentation for version compatibility.

Conclusion

Managing Symfony deprecations is not optional if you plan to keep your application up-to-date. The tools we’ve covered—Symfony PHPUnit Bridge, PHPStan with deprecation rules, and Rector—provide a comprehensive approach to detecting, understanding, and fixing deprecations systematically.

The most important principle is consistency: integrate deprecation detection into your regular development workflow rather than treating it as a special upgrade task. When deprecations are caught early, they remain small, manageable problems. When deferred, they become upgrade crises.

This approach also extends beyond Symfony. Similar patterns work for other frameworks and libraries that provide deprecation mechanisms. The philosophy—detect early, automate where possible, fix continuously—applies broadly to sustainable software maintenance.

Remember that the tools themselves evolve alongside Symfony. Check the documentation for your specific versions—especially the Symfony PHPUnit Bridge (requiring symfony/phpunit-bridge 5.4+), PHPStan deprecation rules (sane-dev/phpstan-deprecation-rules), and Rector (rector/symfony 0.17+). Adjust configurations accordingly based on your target Symfony version.

Sponsored by Durable Programming

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

Hire Durable Programming