Home/ Engineering Insights/ Upgrade Legacy PHP 5.6/7.x to PHP 8: What Breaks

Upgrading Legacy PHP from 5.6 or 7.x to PHP 8: What Breaks and How to Find It

If an application still runs on PHP 5.6 or an early 7.x release, the upgrade is overdue, and it is almost certainly bigger than anyone has told you. That isn't a reason to put it off. It's a reason to plan it properly, because the cost of the work tends to depend on a handful of things you can find out in a few days.

Here's what breaks, how to find it before production does, and an order of work that keeps the risk small.

Why you can't leave it

PHP versions only get security fixes for a limited time. PHP 8.1 and everything older is already out of support, and php.net/supported-versions has the current dates. A runtime without security fixes is the hole attackers look for first. There's a quieter problem as well: libraries drop old PHP versions, so your dependencies stop receiving updates too, and hosting providers eventually retire the old versions altogether, sometimes with very little notice.

Go in steps, not in one jump

It's tempting to jump from 5.6 straight to the newest release. You can, but when something breaks you'll have errors from four versions' worth of changes tangled together. Stepping through versions gives you smaller, readable batches of problems.

PHP 7.4 is the most useful stopping point. It deprecates a lot of what 8.0 removes, so running your application on 7.4 with all errors visible tells you what 8.0 would have broken. From 7.4, move to the newest version you can support, one release at a time if the application is large.

Set up so you can see problems

  • Run several PHP versions side by side. Docker images (php:7.4-cli, php:8.2-cli and so on) or your distribution's packages both work. The goal is to switch versions in seconds.
  • Turn every error on while testing: error_reporting = E_ALL, display_errors off in the browser, and log_errors on with a log file you actually read.
  • Work on a staging copy with real-looking data. Replaying a day of production requests against it, from your access logs, exercises far more code than clicking around by hand.

Find what you can before running anything

Syntax errors first. This one-liner lints every PHP file with whichever php is on your path, and prints only the failures:

find . -name '*.php' -print0 | xargs -0 -n1 php -l | grep -v 'No syntax errors'

Then a compatibility scan. The PHPCompatibility ruleset for PHP_CodeSniffer checks code against a target version range:

vendor/bin/phpcs -p . --standard=PHPCompatibility \
  --runtime-set testVersion 8.2- --extensions=php,inc

Rector can also report the changes it would make, using its PHP upgrade rule sets, if you run it with --dry-run first. Treat both tools as a first pass. They produce false positives, and they can't see behaviour changes that only show up at runtime, which is where most of the real surprises live.

What actually breaks

Removed functions and extensions

The mysql_* functions were removed in PHP 7.0. If the application still uses them, this is the biggest single job, but it's a mechanical one. Move to PDO or mysqli, and use prepared statements while you're touching every query anyway. The ereg family and split() went in 7.0 as well; replace them with preg_*. The /e modifier on preg_replace is gone, so those calls need preg_replace_callback. mcrypt was removed in 7.2, with OpenSSL or libsodium as the replacements.

Deprecated in 7.2 and removed in 8.0: each(), create_function() and __autoload(). All three show up constantly in old code.

Old-style constructors stop working, silently

Before PHP 8, a method named after its class counted as the constructor. In 8.0 that stopped. There's no error. The method becomes an ordinary method that nothing calls, so objects get created without being initialised and the bug shows up somewhere else entirely. Search for it directly:

class Invoice {
    // Used to run on "new Invoice()". On PHP 8 it never runs.
    function Invoice($id) { $this->id = $id; }

    // Fix: rename it
    function __construct($id) { $this->id = $id; }
}

Comparison rules changed

In PHP 8, comparing a number with a non-numeric string compares them as strings. Before, the string was turned into 0, so 0 == "abc" was true. Now it's false. Code that relied on the old behaviour, usually inside switch statements, in_array() without strict mode, or sloppy form validation, will start choosing different branches. This is one of the nastiest categories because nothing crashes.

Warnings that became exceptions

PHP 8 turned many quiet failures into thrown errors. count(null) now throws a TypeError, and so does arithmetic on an array. Undefined variables and array keys are warnings instead of notices, which makes your logs noisy the day you upgrade. And the @ operator no longer silences fatal errors.

Deprecations that flood the log

From PHP 8.1, passing null to a built-in function parameter that doesn't accept it is deprecated. Calls like trim($maybeNull) or strlen($row['x']) on a nullable column will each log a message. Nothing breaks yet, but it will in a future version, and the log noise hides real problems. Cast at the source or handle the null.

PHP 8.2 deprecates creating properties on an object that never declared them. Old code does this all the time, assigning $this->whatever anywhere. Declare the properties. As a stopgap you can mark a class with the #[AllowDynamicProperties] attribute, but treat that as a to-do, not a fix.

Also from 8.1, mysqli throws exceptions on errors by default. Code that checked for a false return after a failed query now gets an exception instead, so error handling written for the old behaviour may never run.

Dependencies are usually the real problem

The application's own code is only part of it. Composer will tell you which packages stand in the way of a given version:

composer why-not php 8.2

Updating is easy when the maintainers kept up. It gets harder with abandoned packages, which need a replacement or a fork. And hardest of all is the library somebody copied into a lib/ folder years ago and edited. Find those early, since they decide your timeline more than anything in your own code.

Keep the upgrade pure

Don't refactor, redesign or restructure in the same release. When the site misbehaves after the switch you want exactly one suspect. Group fixes by type (all the each() calls in one change, all the constructors in another) so each can be reviewed, tested and reverted on its own.

Switching over and being able to switch back

Most hosts and control panels let you choose the PHP version per site or per pool. Keep the old version installed for at least the first week after cut-over. If something unexpected turns up in production, going back is a settings change instead of an emergency. Watch the error log closely for the first few days, since rarely-used code paths, such as monthly reports and year-end jobs, only reveal themselves when they run.

Common questions

Can I go straight from PHP 5.6 to PHP 8?

Technically yes, but it makes the work harder to diagnose. Going through 7.4 first exposes most of what 8.0 changes while the application still runs.

Do I have to rewrite the application?

No. Most legacy applications can be upgraded in place. A rewrite is a separate decision, and we cover when it's justified in rewrite or modernise.

What about WordPress, Magento or other platforms?

The same PHP changes apply, but the platform and its extensions add their own compatibility limits. Check what the platform version supports before you touch the runtime, and plan the platform upgrade and the PHP upgrade together.

Where does this fit in a bigger modernisation?

Early. Usually straight after backups, a staging copy and the first security fixes. See our legacy PHP modernisation roadmap.

Stuck on an old PHP version?

We'll check what's blocking the upgrade, give you an honest picture of the work, and do it in steps you can roll back.

See our legacy system support