It works fine. That is the problem.
A PHP application from 2012 does not degrade the way other systems do. It runs, quietly, for years — right up to the morning your hosting provider removes the runtime on a date nobody at your business chose. The migration happens either way. The only question is who picks the day.
Somebody else's calendar is the one that matters
These dates are public and they have already passed. Unsupported does not mean broken — it means no security patches, and a hosting provider free to withdraw the runtime whenever it suits them.
| Version | Support ended | What we usually find |
|---|---|---|
| PHP 5.6 | December 2018 | Nearly eight years unsupported. The last version before mysql_* was removed. |
| PHP 7.0 – 7.3 | 2019 – 2021 | The versions most "we upgraded once" applications are stuck on. |
| PHP 7.4 | November 2022 | Still extremely common, and the most frequent starting point we see. |
| PHP 8.0 | November 2023 | Recent enough that owners are surprised to learn it is already out of support. |
MySQL matters here too: a 5.5 or 5.6 database is usually the same age as the application and moves with it.
Seven things, and the first one is most of the cost
A runtime upgrade is rarely the hard part. What makes it expensive is everything the application was allowed to do when it was written — and a good estimate depends on counting those, not guessing at them.
mysql_* was removed in PHP 7
The single biggest cost in moving off 5.x, and the one that is actually measurable before you start. Every mysql_query, mysql_fetch_assoc and mysql_real_escape_string has to become mysqli or PDO. We count the call sites in the first day, which turns "how long is a piece of string" into a number.
The deployed code is the only copy
Uploaded by FTP, edited on the live server, never in version control. Production and whatever repository exists have quietly diverged, and only production is real. Reconciling them is often the first thing worth doing.
Queries built by string concatenation
Not a style opinion. An application from this era typically has SQL assembled from request data in dozens of places, which is injection whether or not anyone has found it yet.
Passwords as unsalted md5 or sha1
Standard practice when the application was written, and trivially reversible now. Fixable without a rewrite: rehash on next successful login and the old column empties itself over a few weeks.
No Composer, or a lock file that no longer resolves
Libraries pasted into a vendor folder years ago, with no record of what version or what has been patched since. Sometimes there is no dependency manifest at all.
A framework that is itself end-of-life
CodeIgniter 2, Zend 1, an early CakePHP, or a bespoke MVC written in-house. The application is not the only thing that stopped being supported.
Encoding that was never right
latin1 tables serving utf-8 pages, or the reverse. It shows up as mangled accented characters — which matters more here than in most markets, because half the data is in French.
Almost never a rewrite
Recover the source first. Get the deployed tree into version control, reproduce a build on a clean machine, and reconcile it with whatever repository exists. Until that is true nothing else can be done safely, and it is usually a week rather than a project.
Then migrate the runtime, in a copy. The application runs on the new version alongside the old one until it behaves identically. Nobody switches on a Friday. This is where the mysql_* call count from day one turns into a schedule.
Close the security gaps as you go. Parameterised queries, password rehashing on login, secrets out of the tree. These are done during the migration rather than as a second project, because the code is already open.
Where a piece genuinely cannot be touched, route around it. Occasionally one capability is too central to edit safely — pricing, invoicing, anything every transaction passes through. That one moves out to a small service beside the application, reading through a seam, with nothing inside the original changed. See the bypass.
One day, and no access required. Triage tells you whether this is a three-week migration or a nine-month one, from a conversation and what is visible from outside. The full health check takes one to five days and counts the things that actually drive the number. Prices are on one page.