systematically instead of ignoring them
Anyone silencing deprecation warnings with error reporting tricks only pushes real migration work to a later, more expensive point in time. A custom error handler with structured logging, static analysis with PHPStan and Rector, and a prioritized backlog turn deprecations from ignored noise into a measurable, plannable migration process.
Table of contents
- 1. Why silencing is not a solution
- 2. Capturing deprecations with a custom error handler
- 3. Static analysis: finding deprecations before they occur
- 4. Prioritizing the deprecation backlog
- 5. A concrete example: dynamic properties in PHP 8.2
- 6. Measuring progress: deprecation count as a CI metric
- 7. Introducing a CI budget that is not allowed to grow
- 8. Integrating deprecations into the release process
- 9. Approaches to deprecations compared
- 10. Summary
- 11. FAQ
1. Why silencing is not a solution
A deprecation warning is an announcement with lead time: a function, a parameter behavior, or a language construct will be removed or behave differently in a future PHP version, the code currently still works, but only for a limited time. The most common reflex in dealing with this is to configure error_reporting so that E_DEPRECATED is not shown at all, or to silence individual calls with the @ operator. Both approaches solve no problem, they merely hide it from the team's eyes until a major upgrade finally removes the retired function and the code breaks with a fatal error without any warning.
Anyone who wants to work through handling deprecation warnings instead of ignoring them must first accept that every single notice is a free hint about future work that will be necessary, issued by the PHP core itself or by a library, long before the break actually occurs. Ignored deprecations accumulate unnoticed over years, until a planned PHP version upgrade suddenly uncovers hundreds of problem spots at once, a state that unnecessarily delays and jeopardizes every upgrade project.
The following sections show how a team establishes handling deprecation warnings as a continuous, prioritized process instead of a one-off panic action before a version upgrade, from technical capture through prioritization to integration into CI and the release process.
2. Capturing deprecations with a custom error handler
The first step toward handling deprecation warnings is visibility: a custom error handler, registered via set_error_handler(), specifically catches E_DEPRECATED and E_USER_DEPRECATED and logs them in a structured way, instead of just outputting them on the console or in the standard error log. It is crucial to store not just the message itself, but also the call stack, so it is later clear which concrete caller uses the deprecated function, not just that it is used somewhere.
<?php
declare(strict_types=1);
namespace App\ErrorHandling;
use Psr\Log\LoggerInterface;
/**
* Captures deprecation notices with call-site context instead of
* letting them scroll past unnoticed in the standard error log.
*/
final class DeprecationCollector
{
public function __construct(private readonly LoggerInterface $logger)
{
}
public function register(): void
{
set_error_handler(
function (int $errno, string $errstr, string $errfile, int $errline): bool {
$trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 1, 5);
$this->logger->warning('Deprecated API usage detected', [
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
'trace' => array_map(
static fn (array $frame): string => sprintf(
'%s%s%s()',
$frame['class'] ?? '',
$frame['type'] ?? '',
$frame['function']
),
$trace
),
]);
// Returning false lets PHP's default handler still run afterward.
return false;
},
E_DEPRECATED | E_USER_DEPRECATED
);
}
}
The return value false in the handler matters: it ensures that PHP's built-in default handling still additionally applies, instead of swallowing the notice entirely, so that, for example, test frameworks reacting to deprecations continue to work correctly. The call stack in the log output is the decisive difference from a plain message list: it shows exactly which class and method a deprecated call originates from, which reduces finding the location from a text search across the entire repository to a direct jump to the affected line.
In production systems, deduplication by message and location before a log entry is created is additionally recommended, so a frequently traversed code path does not generate thousands of identical log lines per minute. A simple in-memory cache with an expiration time, suppressing already-logged combinations of message and file for a defined period, drastically reduces log volume without impairing visibility of new, previously unknown deprecations.
3. Static analysis: finding deprecations before they occur
Runtime logging only finds deprecations in code paths that are actually executed, a rarely called error-handling branch or an admin feature that is hardly used remains undiscovered until that exact path is traversed once, in the worst case only after the PHP upgrade. Static analysis with PHPStan closes this gap, because it checks every line of code regardless of whether it is ever reached at runtime.
# phpstan.neon
parameters:
level: 8
paths:
- src
reportDeprecatedCalls: true
treatPhpDocTypesAsCertain: true
ignoreErrors:
# Temporarily accepted, tracked in the deprecation backlog (ticket DEPR-142)
- '#Call to deprecated method Legacy\\OrderExporter::export\(\)#'
The parameter reportDeprecatedCalls enables a dedicated PHPStan rule that reports every call to a method or class marked as @deprecated, both for PHP core functions with known deprecation and for custom APIs marked as deprecated within the project itself. Unlike pure runtime logging, this check finds every occurrence in the entire source code on every CI run, regardless of test coverage or actual usage at runtime.
In addition, Rector with the rule set DeprecationSetList not only finds occurrences but automatically suggests the appropriate replacement for many known deprecations, for example switching from a deprecated function to its modern equivalent. Anyone wanting to work through handling deprecation warnings should combine both tools: PHPStan for the complete but purely reporting inventory, Rector for automated correction of cases that can be resolved mechanically, without manually touching every occurrence.
4. Prioritizing the deprecation backlog
A typical mid-sized project quickly accumulates several hundred deprecation occurrences after the first complete capture, a volume that cannot be worked through in one go without blocking regular feature work. Prioritization along two dimensions has proven effective in practice: frequency of use, measured by the number of distinct call sites in the code, and blast radius, meaning how critical the affected code path is to business operations.
A deprecation occurring in a single, rarely used admin function has low priority, even if it is technically trivial to fix. A deprecation in the checkout process or in the central database access layer has high priority, even if fixing it requires more effort, because a future fatal error at that spot would paralyze the entire business operation. A simple prioritization matrix with the axes frequency and criticality sorts the backlog objectively, instead of working through deprecations by subjective gut feeling or the order in which they appeared in the log.
In practice it pays off to maintain the deprecation backlog as its own category in the ticket system, with tickets automatically generated from the PHPStan findings, instead of only collecting deprecations as a vague mention in a wiki document. Each ticket references file, line and the concrete PHPDoc deprecation message, so a developer can start fixing it directly without additional research.
5. A concrete example: dynamic properties in PHP 8.2
PHP 8.2 introduced dynamically setting properties on classes without a declared property or the #[AllowDynamicProperties] attribute as a deprecation, in PHP 9 this behavior is expected to become a fatal error. The following code works unchanged under PHP 8.1, but generates a deprecation notice on every affected call under PHP 8.2 and later.
<?php
declare(strict_types=1);
namespace App\Model;
// BEFORE: no declared properties, relies on dynamic property creation.
// Triggers "Deprecated: Creation of dynamic property" as of PHP 8.2.
final class CustomerData
{
public function __construct(array $attributes)
{
foreach ($attributes as $key => $value) {
$this->$key = $value;
}
}
}
// AFTER: explicit properties via a typed, validated structure.
final class CustomerDataFixed
{
public function __construct(
public readonly string $email,
public readonly string $firstName,
public readonly string $lastName,
public readonly ?string $phone = null,
) {
}
/**
* @param array{email: string, firstName: string, lastName: string, phone?: string} $attributes
*/
public static function fromArray(array $attributes): self
{
return new self(
$attributes['email'],
$attributes['firstName'],
$attributes['lastName'],
$attributes['phone'] ?? null,
);
}
}
The fix is substantively more than pure silencing: instead of putting #[AllowDynamicProperties] on the class, which suppresses the deprecation but gains no type safety at all, the fixed version declares explicit, typed properties. The side effect is considerably better PHPStan analyzability, IDE autocompletion works correctly, and a typo in the property name is immediately reported on the next phpstan analyse run, instead of only surfacing at runtime as a null access.
This pattern, using a deprecation as an opportunity for real structural improvement instead of minimal symptom treatment, pays off for most PHP 8.1-to-8.4 deprecations: implicitly nullable parameter types (function foo(string $x = null)) become explicit ?string, deprecated functions like utf8_encode() are replaced by their modern replacements from mbstring or iconv, instead of just suppressing the warning.
6. Measuring progress: deprecation count as a CI metric
Without a number visible over time, progress in handling deprecation warnings remains invisible, both to the team itself and to management deciding on capacity for migration work. A simple CI job that counts the number of deprecation occurrences reported by PHPStan and tracks it over time in a dashboard or a simple file makes both progress and regression equally visible.
#!/usr/bin/env bash
# scripts/track-deprecations.sh
set -euo pipefail
REPORT_FILE="var/deprecation-count.json"
CURRENT_COUNT=$(vendor/bin/phpstan analyse --error-format=json 2>/dev/null \
| jq '[.files[].messages[] | select(.message | test("deprecated"; "i"))] | length')
echo "Current deprecation count: ${CURRENT_COUNT}"
if [ -f "$REPORT_FILE" ]; then
PREVIOUS_COUNT=$(jq '.count' "$REPORT_FILE")
echo "Previous count: ${PREVIOUS_COUNT}"
if [ "$CURRENT_COUNT" -gt "$PREVIOUS_COUNT" ]; then
echo "FAIL: deprecation count increased from ${PREVIOUS_COUNT} to ${CURRENT_COUNT}"
exit 1
fi
fi
echo "{\"count\": ${CURRENT_COUNT}, \"date\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$REPORT_FILE"
The script uses PHPStan's JSON output, filters out messages containing the word "deprecated" and compares the current count with the last stored value. If the number rises, the build fails, a new call to a deprecated function was introduced and must be fixed before merging. If the number drops or stays the same, the new state is stored and serves as the reference for the next run.
This principle, borrowed from the baseline strategy for PHPStan levels, works just as well for handling deprecation warnings: it prevents regression without forcing an immediate, complete teardown of the existing backlog. A team can thus work continuously on the backlog while simultaneously ruling out that new pull requests add new deprecations unnoticed.
7. Introducing a CI budget that is not allowed to grow
Beyond the pure non-growth rule, an explicit, time-bound reduction budget pays off for many teams: instead of just preventing the number from rising, a target value is set, for example a ten percent reduction per quarter, anchored as its own visible CI check alongside the pure regression check. This makes expectations within the team explicit, instead of only communicating them implicitly through good intentions.
Such a budget works best when it is backed by concrete, planned capacity, for example a fixed portion of every sprint reserved exclusively for backlog reduction, instead of only doing deprecation work "on the side" between feature tickets. Teams that pursue this approach consistently regularly report that the backlog shrinks to a manageable residual level within a few quarters, while teams without a fixed budget often remain at the same level for years, because feature pressure crowds out any deprecation work.
It is important to align the budget with actual criticality, not blindly with the raw count: fixing ten deprecations in critical payment paths is more valuable than fifty in rarely used reporting functions, even if the raw number in the dashboard then drops more slowly. A good tracking dashboard therefore separates by criticality, not just by total count.
8. Integrating deprecations into the release process
Before every planned PHP version upgrade there should be a dedicated analysis phase that specifically checks the deprecations of the target version, not just the existing backlog. Tools like phpstan/phpstan-deprecation-rules combined with PHPStan set to a newer PHP version via the phpVersion parameter simulate which additional deprecations come with the upgrade, long before the code actually runs under the new version.
A proven order: first, the existing deprecation backlog for the current PHP version is reduced as much as possible, then a PHPStan analysis with an increased phpVersion value simulates the target version and uncovers new, version-specific deprecations. Only once this pre-analysis shows a manageable number of new occurrences does the project actually switch the PHP version in the CI image and in the production environment, with a clear understanding of the remaining work instead of a surprise after the switch.
This process should become a fixed part of the release calendar, not a one-off exception action: every new PHP minor version brings new deprecations along, a team that establishes handling deprecation warnings as a recurring, planned process instead of a reactive fire drill experiences version upgrades as a plannable routine event instead of a multi-week crisis project.
9. Approaches to deprecations compared
The following table compares the three common strategies for dealing with deprecation warnings along the dimensions most relevant to a team's decision.
| Strategy | Detection speed | Effort | Risk at version upgrade |
|---|---|---|---|
| Ignoring / @ silencing | None | Minimal, short term | Very high, sudden fatal error |
| Runtime logging | Only executed code paths | Low, ongoing operation | Medium, blind spots possible |
| Static analysis (PHPStan/Rector) | Complete, every line | Initial setup, then low | Low, plannable migration |
The three strategies do not exclude each other, quite the opposite: runtime logging covers actual production usage, static analysis covers every line regardless of execution, together they form a more complete picture than either strategy alone. Silencing should not be part of the strategy in any combination, it provides no information gain whatsoever and only increases the risk of an unprepared fatal error.
10. Summary
Handling deprecation warnings instead of ignoring them starts with visibility: a custom error handler logs deprecations actually triggered at runtime with a full call stack, static analysis with PHPStan and Rector additionally covers occurrences that are rarely or never reached at runtime. Prioritization by frequency and blast radius sorts the resulting backlog objectively, instead of working through it by chance or gut feeling.
A CI job that tracks the deprecation count over time and fails the build on growth prevents regression, an explicit reduction budget with planned capacity ensures actual progress instead of stagnation. Anyone who firmly integrates this process into the release calendar, instead of hastily catching up on it shortly before every PHP version upgrade, experiences migrations as a plannable routine instead of a recurring crisis.
Handling Deprecation Warnings - The essentials at a glance
Capture instead of silencing
A custom error handler with set_error_handler() logs deprecations with a call stack instead of suppressing them.
Static analysis
PHPStan with reportDeprecatedCalls and Rector with DeprecationSetList also find rarely executed code paths.
Prioritized backlog
Sort by frequency and blast radius, critical payment paths before rarely used admin functions.
CI budget and release process
Track deprecation count as a metric, fail the build on growth, simulate the target version before upgrading.
11. FAQ: Handling Deprecation Warnings
1Why shouldn't I ignore deprecations?
2How do I capture deprecations at runtime?
3Why isn't runtime logging alone enough?
4How does PHPStan find deprecations statically?
5How do I prioritize the backlog?
6What does the dynamic properties deprecation do?
7How do I track progress?
8What is a deprecation budget?
9How do I prepare for a version upgrade?
10Does Rector replace manual fixing completely?
Mironsoft
Migration planning, static analysis and CI pipelines for PHP projects
Is your deprecation backlog growing unnoticed?
We capture your current deprecations with logging and static analysis, prioritize the backlog by criticality, and set up a CI budget that makes your next PHP version upgrade plannable instead of risky.
Inventory
Complete capture via logging and PHPStan, prioritized by frequency and blast radius
Automated fixes
Rector rule sets for mechanically solvable deprecations, manual migration for the rest
CI budget
Deprecation tracking as a CI metric, upgrade simulation before every PHP version change