How we upgraded a PHP 5 app to PHP 8 without downtime
A client came to us with an order entry system that had been running on PHP 5.6 since 2009. Shared hosting, no tests, no lock file, and a payment processor that was starting to send warning emails about the server’s TLS setup. The business could not pause orders for a quarter. Here is how we did the upgrade without any downtime.
Step one: freeze deploys and map the terrain
The first rule of an upgrade on a system with no tests is simple. Nobody deploys anything until you have a way to measure breakage. We spent the first week on two things: moving the app onto a staging box that mirrored production, and writing characterization tests.
Characterization tests do not test what the code should do. They pin down what it actually does, bug or not. For this app, that meant capturing the pricing calculations, the inventory reservation logic, and the CSV export formats. If an upgrade changes any of those, the tests scream.
Step two: the compatibility shim
The code called mysql_* functions in about 400 places, and a bunch of other things PHP 8 removed. Rewriting all of them at once was out of the question, so we built a small shim layer: mysql_query() became a wrapper over PDO, each() was polyfilled, and the handful of ereg calls were rewritten by hand.
The shim was temporary by design. Every page that moved off the old functions had its shim calls deleted in the same PR. After six weeks, the shim was gone entirely. What started as a bridge ended as a checklist.
Step three: the data traps
The dangerous part of a PHP upgrade is rarely the syntax. It is data. This app had three ticking bombs:
- Passwords stored as raw
md5()hashes. We moved them topassword_hash()with a rehash-on-login flow, so existing users kept working. latin1columns holding UTF-8 text, which meant every report exported with broken characters. Fixing this requires a careful double-conversion, not a simpleALTER TABLE.serialize()blobs storing session carts. Some of them were fifteen years old and did not unserialize cleanly on PHP 8. We added a defensive fallback that rebuilt the cart instead of crashing checkout.
Step four: the cutover
We ran PHP 8.2 in parallel on a second server behind the same load balancer. Staging traffic was pointed at it for two weeks, including replays of real production traffic captured with GoReplay. When the error rate stayed flat, we flipped production during a Sunday maintenance window.
The flip took eleven minutes, most of it waiting on DNS. Rollback was one button in the load balancer, which we never needed.
The numbers that mattered to the client
- Zero downtime. Orders kept flowing the whole time.
- Page loads dropped from 1.4s to under 400ms, mostly from PHP 8 itself and Opcache finally being enabled.
- The payment processor stopped sending warning emails, because the box now had current TLS.
- The team got their first test suite. It is not perfect, but it catches the pricing regressions that used to cost them chargebacks.
The codebase is not beautiful. It is boring, which was the point. If you have a PHP 5 app and want to know whether an upgrade is feasible, send us a short description and we will tell you honestly, including when the answer is no.
Dealing with something similar?
Describe your situation and we will give you an honest read within 24 hours, including whether you need us at all.
Get in touch