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

Handling Deprecated Functions When Upgrading PHP


In 1880, the United States Census faced a crisis. The previous census had taken eight years to complete—and with population growth, the next would likely take even longer. The solution? Herman Hollerith’s tabulation machine, which automated what had been manual work. Importantly, the machine didn’t let workers do anything they couldn’t do themselves—it simply let them do it faster and more accurately.

Similarly, upgrading PHP doesn’t change what your application does—but it changes how it does it. The underlying logic remains the same, yet the functions we call may have evolved. When we move from PHP 7.x to 8.x, or from 8.0 to 8.3, we encounter a familiar challenge: deprecated functions that, like Hollerith’s mechanical counters, have served their purpose but are now retired from active duty.

In this article, we’ll walk through the process of identifying and handling these deprecated functions together. We’ll examine why deprecations happen, how to find them in your codebase, and what strategies we can employ—each with its own trade-offs—to ensure your PHP upgrade proceeds as smoothly as possible.

What are Deprecated Functions?

In PHP, when we say a function is “deprecated,” we mean it’s still available—but it’s no longer recommended for use and will be removed in a future version. Typically, the PHP core team deprecates functions for one of several reasons: security concerns, performance improvements, or because a better alternative exists.

When our code calls a deprecated function, PHP generates an E_DEPRECATED notice. Of course, in many production environments, these notices are suppressed—which means we might not even know we’re using deprecated code until we upgrade to a version where the function has been removed entirely.

This creates a helpful but often overlooked opportunity: deprecation notices are PHP’s way of giving us advance warning. They’re not errors—they won’t break our application today—but they are, in effect, the language telling us, “You should update this before it’s too late.”

Finding Deprecated Functions in Our Codebase

Before we can fix deprecations, we need to find them. Let’s explore three complementary approaches—each with its own advantages—and see how we might combine them for the most thorough analysis.

1. Configuring Error Reporting to Catch Deprecations at Runtime

One straightforward approach is to configure PHP to log all deprecation notices. In our development environment, we’ll want to ensure that error_reporting includes E_DEPRECATED:

error_reporting = E_ALL
display_errors = On
log_errors = On
error_log = /path/to/php-errors.log

Once configured, we can exercise our application—ideally through a comprehensive test suite—and then examine the logs. For example, after running our tests with php vendor/bin/phpunit, we might see output like:

PHP Deprecated:  Function each() is deprecated in /path/to/legacy-code.php on line 42

Of course, the effectiveness of this approach depends on how much of our code gets executed during testing. Code paths that aren’t covered by tests—perhaps an error handler used only in rare conditions—might slip through.

2. Static Analysis Tools: Finding Deprecations Without Running the Code

Static analysis tools examine our code without executing it, which means they can analyze every branch and condition, even the ones our tests don’t reach. Tools like PHPStan and Psalm both support deprecation detection.

With PHPStan, we can install the phpstan-deprecation-rules extension:

composer require --dev phpstan/phpstan-deprecation-rules

Then, we add the rule to our phpstan.neon configuration:

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

Now when we run vendor/bin/phpstan analyse src, PHPStan will flag any deprecated function calls it finds—whether those code paths are tested or not.

One limitation: static analysis may not catch deprecations in dynamically included files or code generated at runtime. Still, it’s an excellent first line of defense.

3. Using Automated Tests on the Target PHP Version

Perhaps the most direct method is to run our existing test suite under the newer PHP version we’re targeting. This has the advantage of exercising our actual runtime environment—including any framework behavior, database interactions, or external API calls.

A typical workflow might look like this:

# First, ensure we have the target PHP version available
php -v  # Check current version
# Switch to PHP 8.2 using your version manager
rbenv local 8.2.0  # or phpenv, asdf, etc.
# Install dependencies
composer install
# Run the test suite
vendor/bin/phpunit

Any deprecations triggered during test execution will appear as failures (if our test configuration treats deprecations as errors) or in the output. We can further ensure deprecations are caught by temporarily setting error_reporting = E_ALL during the test run.

Of course, this approach assumes we have a comprehensive test suite—which, strictly speaking, not every project does. If we’re working on legacy code with sparse tests, we’ll want to combine this with static analysis and manual exploration.

Combining Approaches for Best Results

In practice, we’ve found the most success using a layered strategy: static analysis to scan the entire codebase, supplemented by running tests under the target PHP version to catch runtime-specific issues. We’ll return to this topic in the troubleshooting section, where we discuss handling deprecations in third-party packages we can’t control.

Strategies for Handling Deprecated Functions: Trade-offs and Considerations

Once we’ve identified the deprecated functions in our codebase, we face a decision. We have several options—but each comes with its own implications for maintainability, performance, and long-term technical debt. Let’s examine the main strategies, along with the questions we should ask ourselves when choosing.

Note: The right strategy depends heavily on context. A deprecated function in code we control and understand presents a different situation than one buried in a third-party library we can’t modify.

1. Replacing with a Modern Alternative

Often, the most straightforward path is to replace the deprecated function with its recommended alternative. The official PHP documentation typically suggests the replacement, and we should verify it aligns with our application’s needs.

For example, the each() function—deprecated in PHP 7.2 and removed in PHP 8.0—can be replaced with a foreach loop:

Before (using the deprecated each()):

$array = ['a' => 1, 'b' => 2];
while (list($key, $value) = each($array)) {
    echo "$key: $value\n";
}

After (using foreach):

$array = ['a' => 1, 'b' => 2];
foreach ($array as $key => $value) {
    echo "$key: $value\n";
}

This replacement is simple enough—though we should test thoroughly, as each() and foreach behave differently when the array is modified during iteration. For instance, if we were to add elements to the array inside the loop, each() would eventually iterate over the new elements while foreach would not.

Typical considerations:

  • Does the alternative function behave identically, or are there subtle differences we need to account for?
  • Will the change impact performance? (In this case, foreach is generally faster.)
  • Is the replacement available only in newer PHP versions we haven’t yet targeted?

Tip: Before making widespread changes, create a single test case that demonstrates the old and new behavior side-by-side. This gives us a safety net as we refactor.

2. Using Polyfills to Bridge the Gap

Sometimes, a function is removed without a direct replacement because the use case itself is considered obsolete. Though rare, this does happen. In other cases, a replacement exists but isn’t available in our target PHP version range. Here, we might consider a polyfill—a user-land implementation that replicates the deprecated function’s behavior.

Symfony’s polyfill packages are a good example: symfony/polyfill-php80 provides implementations of functions removed in PHP 8.0, while symfony/polyfill-php74 covers PHP 7.4+ features for older versions.

To use a polyfill, we’d typically require it via Composer:

composer require symfony/polyfill-php80

And then the polyfill will automatically define the missing functions—no code changes needed.

Caveats to consider:

Though polyfills can be convenient, they come with trade-offs. First, they add dependencies to our project. Second, they may not perfectly replicate the original function’s behavior—edge cases might differ. Third, they’re often better suited for bridging minor version gaps rather than serving as long-term solutions.

One may wonder: if polyfills are a stopgap, when should we use them versus replacing the code directly?

The answer depends on our timeline. If we’re upgrading to PHP 8.0 today but plan to move to PHP 8.3 next year anyway, a polyfill might be appropriate for a deprecated function that’s only called in a few places—especially if the replacement requires substantial refactoring. But we should still plan to eliminate the polyfill eventually.

3. Suppressing Deprecation Notices: A Last Resort

There are situations where we simply can’t replace a deprecated function—not because we don’t want to, but because we don’t control the code. This often occurs with third-party libraries that haven’t been updated or with code in vendor directories we shouldn’t modify directly.

In these cases, we might temporarily suppress deprecation notices. We can do this in several ways:

  • In php.ini or .user.ini: error_reporting = E_ALL & ~E_DEPRECATED
  • In code, at runtime: error_reporting(E_ALL & ~E_DEPRECATED);

We can also selectively suppress notices for specific code blocks using the @ error control operator, though this is generally discouraged due to performance overhead:

$result = @json_decode($data, true, 512, JSON_THROW_ON_ERROR);

Of course, suppressing deprecations doesn’t make them go away—it just hides the messages. This is problematic for two reasons. First, we lose visibility into other potential deprecations that we do need to address. Second, it’s easy to forget that we’ve suppressed the warnings, and we may never get around to fixing the underlying issue.

Therefore, we recommend this approach only when:

  • The deprecation is in third-party code we cannot change
  • We’ve verified the package author has an update planned
  • We’ve set a specific deadline to revisit the issue

Warning: Never suppress deprecations in your own application code. If you find yourself wanting to do so, it’s a sign that you need to either fix the code or advocate for the time to do so.

4. Working with Unmaintained Dependencies

Sometimes the deprecated code lives in a package that hasn’t been updated in years and shows no sign of maintenance. What then?

We have a few options:

  1. Fork the package and maintain our own version. This gives us control but adds maintenance burden. We’ll need to track upstream changes (if any) and merge them ourselves.

  2. Replace the package with an alternative. Research whether a more actively maintained package provides similar functionality. The trade-off here is the effort of refactoring to use a different API.

  3. Isolate and encapsulate the deprecated code. If we only use the package in a few well-defined places, we can isolate those calls behind our own abstraction. When it’s time to replace the package, we only need to update the abstraction layer.

  4. Accept the risk and suppress deprecations temporarily. As mentioned above, this should be a temporary measure with a clear plan.

Note: In the examples that follow, we’ll be targeting PHP 8.2, which is widely supported as of this writing. Our approach would be similar for other version targets, though the specific deprecations we encounter will differ.

A Practical Walkthrough: Migrating from create_function() to Closures

Let’s illustrate these strategies with a concrete example. The create_function() function—deprecated in PHP 7.2 and removed in PHP 8.0—was used to generate anonymous functions. Suppose we’ve run our static analysis and found this in our codebase:

// legacy-sort.php
$items = [5, 2, 8, 1, 9];
usort($items, create_function('$a, $b', 'return $a - $b;'));
print_r($items);

This code works in PHP 7.x but triggers a deprecation notice in 7.2, and fails entirely in PHP 8.0. Let’s walk through upgrading it.

Step 1: Understand what the code does

The create_function() call creates an anonymous comparison function for usort(). It’s equivalent to:

function($a, $b) {
    return $a - $b;
}

Step 2: Choose the replacement strategy

We own this code, so we’ll replace the deprecated call directly. The natural alternative is an inline closure:

$items = [5, 2, 8, 1, 9];
usort($items, function($a, $b) {
    return $a - $b;
});
print_r($items);

Step 3: Verify the behavior is equivalent

Let’s test both versions:

# With PHP 7.4 (using create_function)
$ php legacy-sort.php
Array
(
    [0] => 1
    [1] => 2
    [2] => 5
    [3] => 8
    [4] => 9
)
# With PHP 8.2 (using closure)
$ php upgraded-sort.php
Array
(
    [0] => 1
    [1] => 2
    [2] => 5
    [3] => 8
    [4] => 9
)

The results match. Good.

Step 4: Check for edge cases

We should verify that the sorting works correctly with negative numbers, floating-point values, and edge cases like integer overflow. A proper test would cover these scenarios—though we’re omitting them here for brevity.

Step 5: Commit and document

We’ll commit the change with a clear message:

fix: replace deprecated create_function() with closure for usort

create_function() was removed in PHP 8.0; use anonymous functions
instead. This maintains consistent sorting behavior across PHP versions.

And that’s it. The migration was straightforward because the deprecated function had a clear, well-documented replacement and we controlled the code.

One may wonder: what if the comparison were more complex—say, comparing objects with multiple fields? The same approach applies, though we’d want to write tests to ensure the logic remains correct after conversion.

Common Deprecations We Encounter When Upgrading

When we upgrade PHP, the specific deprecations we encounter depend on which version we’re coming from. Let’s examine the most common ones across recent major releases, with particular attention to why they were deprecated and what we should do about them.

Note: This isn’t an exhaustive list—PHP’s documentation contains the complete deprecation notices for each version. We’re focusing here on the deprecations that tend to appear most frequently in real-world codebases.

Deprecations Introduced in PHP 7.4

PHP 7.4, released in November 2019, introduced several deprecations that affected significant amounts of existing code:

implode() Parameter Order

Historically, implode() accepted its parameters in either order, leading to confusion:

// Both of these worked:
implode(', ', $array);   // Preferred: separator first
implode($array, ', ');   // Accepted but confusing

As of PHP 7.4, the second form is deprecated. Though it still works in PHP 8.x, we should standardize on the separator-first pattern:

$result = implode(', ', $array);  // Correct

Why the change? The flexible parameter order was a legacy quirk that made the function harder to read and inconsistent with similar functions in other languages.

Curly Brace Array/String Access

The syntax $array{0} for accessing array elements or string offsets is deprecated in favor of square brackets:

// Deprecated
$char = $string{0};
$value = $array{2};

// Preferred
$char = $string[0];
$value = $array[2];

The curly brace syntax originated in PHP 4 and was kept for backward compatibility. It has no advantage over square brackets—indeed, it’s inconsistent with standard array access notation—so the PHP team deprecated it.

money_format()

The money_format() function, which formats numbers as currency strings based on locale, is deprecated. The recommendation is to use the NumberFormatter class from the intl extension:

// Deprecated
$formatted = money_format('%.2n', $number);

// Preferred alternative
$formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
$formatted = $formatter->formatCurrency($number, 'USD');

We note this change because the intl extension isn’t always enabled by default. If we need to format currency and can’t guarantee intl is available, we may need to implement custom formatting or use a library like moneyphp/money.

Deprecations Introduced in PHP 8.0

PHP 8.0, released in November 2020, marked the removal of many previously deprecated functions. Several new deprecations appeared as well:

Required Parameters After Optional Parameters

Function signatures with required parameters following optional ones are now deprecated:

// Deprecated in PHP 8.0
function example($a = null, $b) {
    // ...
}

The proper order is required parameters first, then optional ones:

function example($b, $a = null) {
    // ...
}

This restriction improves consistency and prevents ambiguity in how the function should be called.

libxml_disable_entity_loader()

This function, used to prevent XML entity expansion attacks (XXE), is deprecated. The replacement is libxml_set_external_entity_loader(), which offers finer-grained control. However, for most use cases, the recommended approach is to use the LIBXML_NOENT and LIBXML_DTDLOAD flags carefully—or better yet, avoid parsing untrusted XML entirely.

In practice, if we’re seeing this deprecation, we’re likely working with an older library that processes XML. We should check if an updated version exists; if not, we may need to implement a wrapper that calls the new function appropriately.

Deprecations Introduced in PHP 8.1

PHP 8.1, released in November 2021, continued the trend of cleaning up older patterns:

Passing null to Non-Nullable Parameters

When an internal PHP function declares a parameter as non-nullable (i.e., without a ? type hint), passing null explicitly now triggers a deprecation notice:

// Assuming strlen() had a non-nullable parameter (simplified example)
strlen(null);  // Deprecated in PHP 8.1

In our own code, this serves as a reminder to properly check for null values before passing them to functions that don’t accept them. In third-party code, this deprecation often appears in libraries that haven’t yet been updated for PHP 8.1’s stricter type handling.

mhash() Deprecated

The mhash() function, used for hashing with various algorithms, is deprecated in favor of the more flexible hash() function:

// Deprecated
$hash = mhash(MHASH_SHA256, $data);

// Preferred
$hash = hash('sha256', $data);

The hash() function, introduced in PHP 5.1.2, is more modern and supports a wider range of algorithms through string names rather than constants.

Looking Ahead: PHP 8.2 and 8.3

We should also be aware of deprecations that will affect future upgrades. PHP 8.2 deprecated dynamically created properties (using $obj->newProp = 'value' on classes without __set). PHP 8.3 deprecated several more functions and added warnings for certain stdClass behaviors.

Of course, our specific upgrade path determines which deprecations we’ll actually encounter. A project moving from PHP 7.4 to 8.2 will need to address different issues than one moving from 8.0 to 8.3.

Tip: Before starting an upgrade, review the official PHP manual’s “Migrating” guides for each version between your current and target versions. They list all backward-incompatible changes and deprecations in detail.

Conclusion: Making the Upgrade Process Sustainable

Handling deprecated functions is more than a one-time chore—it’s part of maintaining a healthy codebase that can evolve with PHP. Though the process requires effort upfront, the payoff is a smoother upgrade path and reduced technical debt.

Let’s recap what we’ve covered:

The Process in Brief

  1. Find the deprecations using a layered approach: static analysis to scan the whole codebase, runtime error reporting to catch dynamic issues, and test suite execution under the target PHP version to verify runtime behavior.

  2. Assess each deprecation’s context: Is it in our code or a dependency? Is there a clear replacement? Can we upgrade the dependency, or do we need to maintain a workaround?

  3. Choose the appropriate strategy:

    • Replace with modern alternatives when we control the code
    • Use polyfills for temporary bridging across versions
    • Update or replace unmaintained dependencies where possible
    • Suppress deprecations only as a last resort and with a plan
  4. Test thoroughly to ensure behavior remains correct after changes.

  5. Document decisions so future maintainers understand why we made specific choices.

Looking Ahead: Making Deprecations Routine

One may wonder: can we avoid this massive cleanup effort in the future? The answer is yes—through regular maintenance.

Of course, we can’t prevent all deprecations—PHP evolves, and sometimes features must be retired. But we can make each upgrade smaller and more manageable by:

  • Running static analysis continuously in our CI pipeline, so deprecations are caught early rather than accumulating over years.

  • Upgrading PHP incrementally—moving from 7.4 to 8.0, then 8.1, then 8.2—rather than jumping multiple versions at once.

  • Keeping dependencies reasonably current, so we’re not stuck with packages that use deprecated functions long after they’ve been removed.

  • Writing comprehensive tests that exercise our code thoroughly, giving us confidence that our replacements behave correctly.

Before we get into that, though, let’s address a few questions that often arise during PHP upgrades.

Troubleshooting: Common Questions and Concerns

”What if I can’t find any deprecations but my app still breaks after upgrading?”

This is more common than you might think. Deprecation notices don’t catch everything—especially changes in behavior that don’t trigger E_DEPRECATED. The PHP manual’s migration guides are essential here. Also, check for indirect dependencies: a library you use might call a deprecated function that you never see in static analysis of your own code.

”How do I handle deprecations in Composer vendor packages?”

First, check if a newer version of the package exists that addresses the deprecation. Run composer outdated to see available updates. If no update is available and the package is critical, consider:

  • Forking the package and applying your own fixes (remembering to maintain compatibility)
  • Replacing the package with an alternative
  • Opening an issue or pull request with the original maintainer

If none of these are viable, you may need to suppress deprecations for that specific code—though we recommend isolating the calls to a single location so you only suppress in one place.

”Should I suppress deprecations in production while I work on fixes?”

Generally, no. Deprecations in production indicate code that will break on the next PHP version. It’s better to have visible warnings that prompt action than to hide them and risk a sudden failure later. If deprecations are causing log flooding, consider routing them to a separate log file or monitoring system, but don’t disable them entirely.

”What if a deprecation notice itself causes errors because of a bug in PHP?”

This is rare but has happened. If you encounter what appears to be a bug in PHP’s deprecation handling, check the PHP bug tracker first. In the meantime, you may need to work around it—perhaps by temporarily suppressing deprecations specifically for that PHP version until the bug is fixed.


By approaching PHP upgrades methodically—finding deprecations early, understanding the trade-offs of each strategy, and testing thoroughly—we can keep our applications secure, performant, and aligned with modern PHP practices. The work isn’t always easy, but it’s a necessary part of long-term software maintenance.

As always, the official PHP documentation remains our best resource for version-specific details. The migration guides (e.g., Migrating from PHP 7.4 to PHP 8.0) are worth reading cover to cover before embarking on a major upgrade.

Sponsored by Durable Programming

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

Hire Durable Programming