Recognizing and Migrating Deprecated Code Before It Gets Removed for Good
Deprecation Handling isn't a checkbox on your upgrade checklist, it's an ongoing process: PHPStan rules, Composer audits, and CI gates surface deprecated code long before Magento removes it in a future major version and forces the migration on you.
Table of Contents
- 1. Why Deprecation Handling Is an Ongoing Process
- 2. How Magento Marks Code as Deprecated
- 3. Detecting Deprecated Code Technically: PHPStan
- 4. Deprecation at the Composer Level
- 5. Automated Detection in the CI Pipeline
- 6. Migration Strategies: Rector and Plugin Migration
- 7. Prioritization: Which APIs First?
- 8. Team Process and Documentation
- 9. Practical Example: A Concrete Migration
- 10. Summary
- 11. FAQ
1. Why Deprecation Handling Is an Ongoing Process, Not a One-Time Upgrade Project
Deprecation Handling is often confused, on many teams, with the last step before a major version upgrade: right before migrating to a new Magento version, everyone scrambles to hunt down old code, because otherwise nothing else works. In reality, Deprecation Handling is something different: a continuous part of everyday development that kicks in with every Composer update, every core patch, and every new feature. In practically every minor release, Magento marks APIs, classes, and methods as deprecated long before they are actually removed in a future major version. Teams that only do Deprecation Handling every few years quietly accumulate technical debt in the meantime, debt that adds up to a real blocker at the next big migration.
The difference between a one-off version migration and ongoing Deprecation Handling comes down to when the cost is paid. If deprecated code gets ignored for years, the result is a big-bang refactor with high risk, a tight timeline, and little test coverage for the affected areas. If Deprecation Handling is instead established as part of the normal development flow, the same changes get spread across many small, well-tested commits. Every pull request that writes new code against an API already marked as deprecated is an avoidable mistake, one that the right tooling can catch before the merge.
Magento's backward compatibility policy gives development teams several years of lead time before a deprecated API actually disappears, but that lead time is no reason for inaction. Quite the opposite: it's the opportunity to anchor Deprecation Handling in sprint planning, code review, and the CI pipeline, instead of treating it as a special project. The following sections show how to reliably detect deprecated code on a technical level, and which migration strategies help you work through it systematically before Magento removes it for good.
2. How Magento Marks Code as Deprecated: @deprecated Annotations, @see References, Backward Compatibility Policy
Magento flags outdated code with the PHPDoc annotation @deprecated, followed by a version number from which the marking applies, for example @deprecated 101.0.0. This version number doesn't refer to the Magento product version, it refers to the internal module version of the relevant Composer package, which frequently causes confusion when interpreting it. Directly below the @deprecated line there is usually a @see reference pointing to the recommended replacement class, method, or interface. A well-documented deprecated code block therefore doesn't just say "stop using this," it also points you toward the concrete migration path.
Magento's backward compatibility policy also distinguishes between code marked with @api as an official, stable interface, and internal code without that marker. @api code comes with a much stricter guarantee: it won't be removed across several minor versions without a prior deprecation phase. Internal code not marked @api, on the other hand, can change between patch releases even without a formal deprecation announcement. Anyone who takes Deprecation Handling seriously therefore distinguishes between officially deprecated API surfaces and silently changed internal implementation details, something PHPStan alone doesn't always fully catch.
Important in practice: a @deprecated annotation doesn't remove a method immediately, it usually stays fully functional, often for years. That's exactly what tempts teams to ignore the warning. Deprecation Handling therefore doesn't mean panicking over every single finding, it means systematically capturing, prioritizing, and gradually working through these annotations before the grace period of the backward compatibility policy runs out.
3. Detecting Deprecated Code Technically: PHPStan with phpstan/phpstan-deprecation-rules, bin/analyse
Manually grepping the codebase for @deprecated comments doesn't scale in a grown Magento codebase. The reliable path to technical Deprecation Handling is static analysis with PHPStan and the phpstan/phpstan-deprecation-rules extension. This rule extension doesn't analyze comment text, it analyzes the actual call graph: it catches every place in your own code where a class marked as deprecated is instantiated, a deprecated method is called, or a deprecated interface is implemented, regardless of whether the annotation itself is visible in the calling code.
In the Mark Shust docker setup, PHPStan runs through the bin/analyse wrapper, for example with bin/analyse app/code/Mironsoft --level=5. For this call to also report deprecation violations, the phpstan.neon configuration file must explicitly pull in the deprecation rules via includes. Without this include, PHPStan correctly analyzes types, return values, and nullability, but deprecated calls slip through entirely, because they aren't a level of their own, they're a separate rule extension.
# File: phpstan.neon
# Enables continuous Deprecation Handling as part of static analysis
includes:
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
- phpstan-baseline.neon
parameters:
level: 5
paths:
- app/code/Mironsoft
- app/code/Abrams
excludePaths:
- */Test/*
- */vendor/*
reportUnmatchedIgnoredErrors: true
The phpstan-baseline.neon file plays a strategic role here: it freezes the current stock of known deprecated calls, so bin/analyse doesn't immediately fail with hundreds of errors against a historically grown codebase. New use of deprecated code that isn't part of the baseline, however, gets flagged by PHPStan right away. This combination of baseline and deprecation rules is the core of a working, technically enforced Deprecation Handling, because it keeps existing debt visible without blocking day-to-day development work.
4. Deprecation at the Composer Level: Outdated Dependencies, Marketplace Module Notices, composer outdated
Deprecation Handling isn't just about your own code, it also covers Composer dependencies. The command bin/composer outdated --direct shows which direct dependencies have newer versions available, but it doesn't automatically reveal whether a package has been marked abandoned. That flag is exactly what matters for Deprecation Handling: Composer flags packages whose maintainers have officially given up on them, often with a pointer to a recommended successor. The command bin/composer show --all --direct combined with grep -i abandoned surfaces these notices during day-to-day work, without having to search through every composer.json individually.
For third-party modules from the Magento Marketplace, there's an additional layer: the Marketplace itself often points out deprecated functionality in a module's changelog and product description, for example when a payment provider discontinues an old API version, or a module vendor deprecates a class in favor of a new interface. These notices don't show up in PHPStan reports, because they often only live in documentation, not annotated as @deprecated in the code itself. Complete Deprecation Handling therefore also means regularly reviewing release notes and Marketplace announcements for the third-party modules in use, not just your own codebase.
In practice, that means composer outdated as a routine task every sprint, combined with a quick check of whether any of the outdated packages are abandoned, or whether Marketplace modules have announced breaking changes. Combining this Composer check with the PHPStan analysis from section 3 covers both your own deprecated code and outdated third-party dependencies, both of which belong to the same Deprecation Handling process.
5. Automated Detection in the CI Pipeline: Failing the Build on New Use of Deprecated Code
Manually running bin/analyse on a developer's machine is a good start, but it doesn't reliably keep deprecated code out of the main branch if a developer forgets to run the check before committing. The decisive step for sustainable Deprecation Handling is therefore integrating the same PHPStan configuration as a mandatory gate in the CI pipeline. Every merge request then automatically goes through the same analysis, and a build fails as soon as new code uses a deprecated API that isn't already part of the baseline.
In a GitLab CI pipeline, this gate can be modeled as its own stage that runs after the Composer install and uses the same bin/analyse call as locally. It's important to configure the job to run on every merge request, not just on releases, so that Deprecation Handling kicks in as early as possible in the development process instead of surfacing right before a deployment.
stages:
- analyse
deprecation-check:
stage: analyse
image: mironsoft/php84-magento:latest
script:
- bin/composer install --no-interaction --prefer-dist
- bin/analyse app/code/Mironsoft --level=5 --no-progress
- bin/analyse app/code/Abrams --level=5 --no-progress
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
allow_failure: false
The allow_failure: false parameter is deliberately set here: a deprecation finding should block the merge, not just vanish as a warning in the pipeline log. Teams that shy away from this hard gate because the baseline is still too large can start with a separate, non-blocking job during a transition period, and only switch to allow_failure: false once the biggest backlog has been worked down. What matters is that Deprecation Handling isn't left to individual developers' good will, but is structurally anchored in the pipeline.
6. Migration Strategies: Rector for Automated Refactoring, Plugin Instead of Preference Migration
Not every migration of deprecated code has to happen by hand. The Rector tool automates a large part of Deprecation Handling by analyzing abstract syntax trees and applying defined rewrite rules, for example replacing a deprecated method with its documented successor, or removing dead code around an outdated condition. For Magento-specific migrations, community rule sets also exist that know typical Magento deprecation patterns, such as outdated event names or deprecated constructor signatures.
A rector.php configuration file defines which paths get analyzed and which rule sets get applied. Rector never works destructively in the blind: the default mode only shows a diff of the planned changes first, files only get written once the --dry-run flag is disabled, or the command is run explicitly without it. For a team running Deprecation Handling at scale, Rector isn't a replacement for code review, it's a tool that handles the same mechanical parts of the migration ahead of time.
One of the most common manual migrations involves moving from preference-based overrides to plugins. Preferences in di.xml replace an entire class and break with every core update as soon as the overridden class changes, which effectively turns many Magento preferences into latent deprecation candidates, even without an explicit @deprecated annotation. Plugins (interceptors), by contrast, only hook into defined method boundaries and are much more robust against core changes. Migrating from preference to plugin is therefore often part of the same Deprecation Handling backlog as migrating explicitly marked deprecated APIs.
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void {
// Only touch Mironsoft's own module code, never vendor or core
$rectorConfig->paths([
__DIR__ . '/app/code/Mironsoft',
]);
$rectorConfig->sets([
SetList::DEAD_CODE,
SetList::CODE_QUALITY,
]);
$rectorConfig->skip([
__DIR__ . '/app/code/Mironsoft/*/Test',
__DIR__ . '/app/code/Mironsoft/*/etc',
]);
// Keep import statements tidy after automated rewrites
$rectorConfig->importNames();
$rectorConfig->parallel();
};
7. Prioritization: Which Deprecated APIs to Migrate First
No team can work through every finding from PHPStan, Composer audits, and Marketplace notices at the same time. Deprecation Handling therefore needs a prioritization logic that goes beyond simple first-found, first-fixed. Before you can prioritize, it helps to get an overview of which detection method offers which detection rate, which degree of automation, and which effort.
| Method | Detection Rate | Automatable | Effort |
|---|---|---|---|
| PHPStan Deprecation Rules | Very high, captures the call graph | Yes, fully | Low after initial setup |
| Manual Code Review | Medium, depends on the reviewer | No | High, per review |
| Composer Audit | High for dependencies | Yes, via script | Low |
| Magento Marketplace Notices | Low, documentation only | No | Medium, manual review |
| IDE Inspections | Medium, only visible while editing | Partial | Very low |
Two criteria drive prioritization in Deprecation Handling in practice: frequency of use and proximity to actual removal. A deprecated method called from twenty places in the code causes far more damage in a forced migration than one used in only a single spot, even if both carry the same version number in their @deprecated annotation. Just as important is the age of the marking: an API marked @deprecated 100.0.2 is structurally closer to removal than one marked only recently as @deprecated 103.0.0, because Magento tends to remove older deprecations first in upcoming major versions. A simple scoring model that combines both factors delivers a solid order for the migration backlog.
8. Team Process and Documentation: Deprecation Register, Tech Debt Backlog, Sprint Planning
Technical detection alone isn't enough for sustainable Deprecation Handling if the results don't land anywhere. A central deprecation register has proven effective: a continuously updated list of all known deprecated findings, their priority, and their migration status. This register can be generated directly from the tools in the previous sections rather than maintained by hand, which avoids inconsistencies between documentation and the actual state of the code.
#!/usr/bin/env bash
# scan-deprecations.sh - Build a Deprecation Handling register from source and static analysis
set -euo pipefail
readonly MODULE_PATH="app/code/Mironsoft"
readonly REPORT_DIR="var/log/deprecation-report"
mkdir -p "$REPORT_DIR"
echo "[INFO] Scanning for @deprecated annotations..."
grep -rn "@deprecated" "$MODULE_PATH" --include="*.php" > "$REPORT_DIR/annotations.txt" || true
annotation_count=$(wc -l < "$REPORT_DIR/annotations.txt")
echo "[INFO] Found ${annotation_count} deprecated annotations in own module code"
echo "[INFO] Running PHPStan deprecation rules..."
bin/analyse "$MODULE_PATH" --level=5 --error-format=json > "$REPORT_DIR/phpstan.json" || true
echo "[INFO] Counting distinct deprecated call sites..."
jq '[.files[].messages[] | select(.message | test("deprecated"))] | length' "$REPORT_DIR/phpstan.json"
echo "[DONE] Deprecation register written to $REPORT_DIR"
The tech debt backlog grows out of this register: a fixed share of capacity per sprint, say ten to fifteen percent, gets reserved for Deprecation Handling tickets, instead of leaving it to chance whether time is left over. This fixed reservation keeps Deprecation Handling from being pushed aside entirely during periods of heavy feature demand. During sprint planning, the prioritized entries from the register get taken in as regular tickets, including effort estimates and references to the affected files, so the migration is held to the same quality standards as any other code change, including tests and code review.
9. Practical Example: Migrating a Concrete Deprecated Magento Class
An everyday example of Deprecation Handling is a direct call to \Magento\Framework\App\ObjectManager::getInstance() inside a constructor. This pattern counts as a deprecated anti-pattern because it bypasses dependency injection and obscures dependencies that should actually be visible through the constructor. PHPStan with Magento-specific rules reliably reports such calls, since they violate both the deprecation rules and basic DI principles.
<?php
namespace Mironsoft\Catalog\Block;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\View\Element\Template;
class ProductBadge extends Template
{
protected $productRepository;
public function __construct(Template\Context $context, array $data = [])
{
parent::__construct($context, $data);
// Deprecated anti-pattern: bypasses constructor-based DI
$this->productRepository = ObjectManager::getInstance()->get(
\Magento\Catalog\Api\ProductRepositoryInterface::class
);
}
}
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Block;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\View\Element\Template;
final class ProductBadge extends Template
{
/**
* @param Template\Context $context Template context.
* @param ProductRepositoryInterface $productRepository Injected via DI instead of ObjectManager.
* @param array $data Additional block data.
*/
public function __construct(
Template\Context $context,
private readonly ProductRepositoryInterface $productRepository,
array $data = [],
) {
parent::__construct($context, $data);
}
}
The migration replaces the deprecated call with regular constructor injection using PHP 8.4 constructor property promotion and a readonly property. The benefit goes beyond just fixing the PHPStan finding: the dependency is now visible in the class signature, easy to mock in unit tests, and subject to the normal DI configuration in di.xml. Small, well-scoped migrations like this one are exactly the core of working Deprecation Handling: technically simple, but only findable once detection and prioritization are established as described in the previous sections.
10. Summary
Deprecation Handling in Magento 2 isn't a project with an end date, it's a lasting practice that ties together technical tooling and team process. PHPStan with phpstan/phpstan-deprecation-rules reliably detects deprecated calls in the call graph, a baseline separates known backlog from new violations, and a CI gate keeps new deprecated code from ever getting merged in the first place. Composer audits and Marketplace notices round out detection with dependencies outside your own code.
For the migration itself, Rector automates the mechanical parts, while prioritizing by frequency of use and proximity to removal makes sure the most impactful migrations happen first. A deprecation register and a fixed capacity reservation each sprint anchor Deprecation Handling structurally within the team, instead of leaving it to chance. Combine these building blocks, and every future Magento major update arrives with a small remainder instead of an unmanageable mountain of deprecated code.
Deprecation Handling in Magento 2: The Key Takeaways
Detection
phpstan/phpstan-deprecation-rules via bin/analyse reliably captures the entire call graph, complemented by Composer audits.
CI Gate
A pipeline stage with bin/analyse and allow_failure: false blocks new use of deprecated code before the merge.
Migration
Rector automates mechanical rewrites, plugin instead of preference migration further reduces silent deprecation candidates.
Process
A deprecation register plus fixed sprint capacity anchor Deprecation Handling permanently instead of as a one-off project.
11. FAQ: Deprecation Handling in Magento 2
1What does Deprecation Handling actually mean in Magento?
2How do I recognize a deprecated class?
3Which PHPStan level do I need?
4Does Rector handle the migration completely?
5Difference from a version upgrade?
6How do I prioritize deprecated APIs?
7How do I enforce Deprecation Handling in CI?
8What about deprecated Marketplace modules?
9How do I document Deprecation Handling?
10What happens if deprecated code gets ignored?
Mironsoft
Deprecation Handling, Rector migrations, and CI pipelines for Magento 2
How much deprecated code is hiding in your Magento installation?
We scan your codebase with PHPStan, prioritize the findings by risk, and set up Deprecation Handling that runs continuously, instead of starting over at every upgrade.
Deprecation Audit
A complete PHPStan scan of your codebase including Composer dependencies, with prioritization by risk
Migration Service
Automated refactoring with Rector combined with manual migration of complex preference-to-plugin cases
CI Pipeline Setup
Automated deprecation gates in GitLab CI that block new use of deprecated code before the merge