The short version: a rewrite is the riskiest way to fix a legacy PHP app, and usually the slowest. You can modernize a codebase that still makes money in place, one safe step at a time, without taking it down. Stabilize it first, wrap the old code instead of replacing it, add tests at the seams you are about to touch, and ship small. That is the whole method. The rest of this post is how each step works.
I do this for a living. I modernize older PHP and Laravel codebases for founder-led companies, and the first thing a founder asks me is almost always the same question.
Should I rewrite or modernize my legacy PHP application?
Modernize. Rewrite only when the business can survive the product standing still for a year, which for a founder-led company it usually cannot.
Here is the trap. The old code is ugly, so a rewrite feels clean. But the old code also encodes years of edge cases, customer-specific quirks, and quiet fixes that nobody wrote down. A rewrite has to rediscover every one of those, and it has to do it while the old system keeps running and keeps changing underneath it. You end up maintaining two systems and shipping no new value, sometimes for years. Most rewrites that fail do not fail on the code. They fail because the business could not afford the wait.
Modernizing in place keeps the revenue flowing while the codebase gets better. That is the constraint that should drive the decision, not how the code looks.
Step 1: stabilize before you change anything
You cannot safely modernize a system you cannot see. So the first work is not code, it is visibility.
I get error logging and monitoring in place so I know what is actually breaking in production, not what someone thinks is breaking. I pin the dependency and PHP versions so the ground stops moving. I get the thing into version control properly if it is not already, with a real deploy path instead of files edited live on the server. None of this changes a single feature. All of it means that when I do start changing things, I will know immediately if I broke something.
This step feels slow because it produces nothing the user sees. It is the most important step anyway. Every safe change after this depends on it.
Step 2: wrap the old code, do not replace it
This is the core of doing it without a rewrite. The pattern has a name, the strangler fig.
What is the strangler fig pattern?
It is a way to replace an old system gradually by building the new code around the edges of the old code, routing one piece of traffic at a time to the new path, until the old code is no longer used and can be removed. The name comes from a vine that grows around a tree and slowly takes its place. You never have a big-bang switchover. You have a series of small ones.
In a PHP app this looks practical, not academic. I put a thin layer in front of the part I want to change. New requests for that one feature go to new, clean code. Everything else keeps hitting the old code, untouched. I prove the new path works in production for that one slice. Then I move the next slice. The app is never down, because at every moment almost all of it is the old code that already worked.
The win is that risk is now measured in slices, not in the whole product. If a slice goes wrong, it is one feature to roll back, not the entire system.
Step 3: add tests at the seams you are about to touch
You do not need to test a ten-year-old codebase top to bottom before you start. That is its own multi-year project and it is not the goal.
Can you modernize legacy PHP without tests already in place?
Yes. You add tests narrowly, right at the boundary of the slice you are about to change, just before you change it. The test captures what the old code does now, including the weird behavior, so that when you swap in the new code you can prove it does the same thing. These are called characterization tests. They do not judge whether the old behavior is correct. They pin it down so you do not break it by accident.
So the test coverage grows exactly where the work is happening, and nowhere else. Over time the parts you have touched are well tested, and the parts you have not touched are the parts that have been quietly working for years anyway.
Step 4: ship in small, safe steps
How do you modernize a PHP app without downtime?
By making every change small enough that it can go out on its own and be rolled back on its own. No change waits months to merge. No change carries ten unrelated improvements. One slice, behind the wrapper from step 2, covered by the tests from step 3, deployed through the safe path from step 1.
Small steps are what make “no downtime” real instead of a promise. A big release is a big bet. A small release is a cheap, reversible experiment. When the unit of change is small, the cost of being wrong is small, and that is the entire reason this approach is safe for a product that cannot go dark.
I did exactly this for a SaaS with more than 100,000 users. I took its architecture from a 4.5 to an 8.0 without a rewrite, and it never went down while I did it. I wrote that one up in detail here: I took a legacy SaaS from a 4.5 to an 8.0 without a rewrite.
How long does it take to modernize a legacy codebase?
You feel the difference in weeks, not years, because the approach pays off continuously instead of all at the end. After the stabilization work, every slice you modernize makes the next one easier and the product a little safer to change. There is no long dark period where you are spending money and seeing nothing, which is the part of a rewrite that kills founder-led companies. The codebase gets better the whole way through, and you can stop at any point and still be ahead.
The takeaway
A legacy PHP app that makes money is not a problem to be deleted. It is an asset to be made safe to change again. You do that by stabilizing it, wrapping it, testing the parts you touch, and shipping small. No rewrite, no downtime, no betting the company on a clean restart that may never land.
If you have a product that earns and is scary to touch, that is the exact situation I work in. I would be glad to look at it with you.