Send request

Legacy PHP · 4 min read

PHP 5 to PHP 8 without stopping the shop

Two PHP versions side by side on one server while the code moves over, one part at a time.

On this page
  1. Why not just switch?
  2. Step 1: a copy, and a map
  3. Step 2: a safety net
  4. Step 3: two versions on one server
  5. Step 4: small steps
  6. Should we rewrite it instead?
  7. How long does it take?

The email from the hosting company is polite and final: PHP 5.6 will be switched off at the end of the quarter. The shop runs on code written in 2012, nobody who wrote it is around, and it takes orders every day. Switching the version and hoping is not a plan. Rewriting everything from scratch is a plan, but usually a bad one.

Here is the way we do it: both versions run side by side, and the code moves across in small pieces, each tested on its own.

Why not just switch?

Because a great deal that PHP 5 accepted, PHP 8 refuses, and usually with a fatal error, not a warning. The usual suspects:

What the old code does What happened to it
mysql_query() and the rest of mysql_* removed in PHP 7.0; use PDO or mysqli
ereg(), split() removed in PHP 7.0; use preg_*
each(), create_function() removed in PHP 8.0
a constructor named after the class ignored since PHP 8.0; the object is built without it
0 == "abc" is true false since PHP 8.0, which quietly changes some if statements
properties added to objects on the fly deprecated since PHP 8.2
null passed to strlen(), trim() and friends deprecated since PHP 8.1

The first three stop the site. The last four are worse: the site keeps running and does something slightly different.

Step 1: a copy, and a map

Nothing happens on the live server. We take a copy of the code and an anonymised copy of the database, set them up on a test server and read the code. Two tools save days here:

  • PHPCompatibility, a rule set for PHP_CodeSniffer, lists every line that will break on a given PHP version.
  • Rector rewrites many of those lines automatically, for example old array syntax or removed functions.

Neither replaces reading. They give you the map; a person decides the route. The result of this step is a written list: what breaks, where, how risky it is, and what to do first.

Step 2: a safety net

Before changing anything, we make sure we would notice if something broke:

  • backups that we have actually restored, once, on the test server;
  • a short list of the flows that make money: search, cart, checkout, the invoice email, the admin export;
  • a check for each flow, automated where it is cheap, written down where it is not.

Step 3: two versions on one server

On a Red Hat family server (AlmaLinux, Rocky, RHEL) the Remi repository installs several PHP versions next to each other, each with its own php-fpm service. nginx then sends each part of the site to the version it is ready for:

# the new, tested part runs on PHP 8
location ^~ /checkout/ {
    fastcgi_pass unix:/var/opt/remi/php85/run/php-fpm/www.sock;
    include fastcgi.conf;
}

# everything else still runs on PHP 5.6, for now
location ~ \.php$ {
    fastcgi_pass unix:/opt/remi/php56/root/var/run/php-fpm/www.sock;
    include fastcgi.conf;
}

The socket paths depend on how each pool is configured; the idea is what matters. The site keeps working the whole time, and each part moves when it is ready, not when the calendar says so.

Step 4: small steps

One part at a time: convert, test on the test server, release at a quiet hour, watch the logs for a day. Then the next part. The database layer usually goes first, because almost everything depends on it, and because mysql_* is where the security holes live:

// before
$res = mysql_query("SELECT * FROM orders WHERE id=" . $_GET['id']);

// after
$stmt = $db->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([(int) $_GET['id']]);

When the last part has moved, PHP 5.6 is switched off, and nobody notices. That is the goal.

Should we rewrite it instead?

Rarely. Old code is ugly, but it also contains years of fixed bugs and business rules nobody wrote down. A rewrite throws all of that away and has to find it again, usually in production. We suggest a rewrite only when the map from step 1 shows it is cheaper, and then we say so in writing.

How long does it take?

That depends on the size of the code and on how many surprises it holds, which is exactly what step 1 finds out. So we start there: a small fixed-price audit that ends with the plan and a price for the rest.

If the email from your hosting company has already arrived, send us a request.

Want this done on your system?

Two questions and an email address. We reply within two business days, usually sooner.

Send request →