Modernization · 9 min read · October 30, 2026

How to Migrate a Legacy PHP Application Without Downtime

To migrate a legacy PHP application without stopping operations, you don't do a swap: you set up coexistence. The old system keeps serving users while the new one takes over one route, one module, or one user profile at a time, with testing beforehand, a switch to go back, and measurement afterward. That's incremental migration, and it's the path we recommend for plain PHP with no framework and jQuery scattered across the screens. Below is the step-by-step, in the order we usually follow. And the principle isn't exclusive to PHP: it works for Java, .NET, Delphi, or any other aging codebase.

This article is part of the guide: Legacy System Modernization: The Complete Guide

Two developers reviewing code together on a laptop in the office
Photo by Jantine Doornbos on Unsplash

Before You Migrate a Legacy PHP Application: Inventory and Map

The first stage doesn't involve a single line of new code. In plain PHP, the system is usually a folder of files where each one mixes SQL, HTML, and business logic, and the URL is simply the file name. Without a map, any estimate is a guess.

What to map, in this order:

  • Routes and screens: every file reachable by URL, who uses it, and how often. Web server logs answer this better than any interview.
  • AJAX calls: the endpoints that jQuery calls, hidden inside the screens.
  • Database tables: who reads and who writes to each one, including triggers, views, and procedures.
  • Integrations: payments, email, third-party APIs, webhooks, and scheduled tasks (cron).
  • Dead code: files that never show up in the logs. There is almost always a part of the system nobody opens, and that part you don't migrate.
  • Risk points: anything that touches money, inventory, personal data, or regulatory rules.

Update PHP First or Switch Frameworks?

If the system still runs on an unsupported version, the temptation is to start by upgrading PHP in the legacy code itself. Our position: do the bare minimum needed to run on a supported version, and don't let it turn into a project of its own.

The reason is practical. In old code, upgrading versions usually runs into removed functions, outdated database extensions, and types handled more strictly. Every fix is wasted work if that file is going to be rewritten anyway. Updating the version is worthwhile as a security step and so you can run tests in a current environment. The rest of the effort goes to the new system, which is born on the current version of the language.

For the full picture of where this step fits, see our guide to legacy system modernization.

Characterization Tests: Record the Behavior Before You Touch Anything

Legacy systems almost never have tests. And without tests, you can't know whether the new system does the same thing as the old one. The solution is the characterization test: it doesn't say what the system should do, it records what the system does today.

If the monthly report rounds numbers in a strange way, the test records the strange way. Then someone from the business decides whether that's a bug or a rule. Without that record, the rule disappears in the migration and no one notices until a customer complains.

In practice: for each priority route in the inventory, we call the old system with real (anonymized) inputs, store the output (JSON, HTML snippets, rows written to the database), and then run the same inputs against the new module and compare. You don't need sophisticated tooling; a script that makes requests and compares responses already goes a long way.

The most common mistake here is trying to cover everything. Concentrate the effort where there are calculations and money. Simple data-entry screens can be validated with a manual checklist.

Coexistence Layer: Route-Based Proxy and the Strangler Pattern

The centerpiece of a zero-downtime migration is an entry point in front of both systems. It can be a reverse proxy or the new system's own router. Each route points to either the legacy system or the new one, and the user notices no difference.

This is the strangler pattern: the new system grows around the old one, route by route, until the old one stops receiving traffic and can be shut down. There is no cutover day, and that's precisely what removes the risk.

The technical points that tend to cause trouble:

  • Shared authentication and session: users can't have to log in twice. Either the new system understands the legacy session, or both start validating the same token. This is the first structural piece, because every new module depends on it.
  • Same domain for both: avoids cookie and CORS problems.
  • Front end in stages: jQuery and React coexist well, screen by screen. Changing the back end and the front end of the same screen in the same step increases risk; separating them is usually safer.
  • Duplicated rules: while both systems exist, a fix in the legacy system has to be replicated in the new one. Keep track of this in a living list.

Migrating the Database and Data Without Breaking the Old System

The database is the most dangerous point because both systems use it at the same time. The rule we follow: never change the schema and the code in the same step.

In general, we keep the database as it is at the start. The new system reads the same tables, through an access layer that translates bad names into something readable. When the schema does need to change, it happens in stages: create the new column or table, write to both places, migrate the historical data, switch the reads, and only at the end remove the old one, once the legacy system no longer uses it.

The data migration script should be repeatable: run it ten times on a copy of production and get the same result. Then validate counts, sums, and samples. This is where dirty data shows up: invalid dates, text with mixed encodings, duplicate records.

Switching database platforms is another project. If you can, keep it separate from the code migration.

What Order to Migrate the Modules In

There is no universal order, but there are criteria that work:

  • First, a small, visible, low-risk module, such as a report or a supporting data-entry screen. The goal is to prove the coexistence layer, the deploy, and the rollback, not to deliver business value.
  • Then authentication, if it isn't already solved, because everything depends on it.
  • Next, the modules that hurt the most: the ones that change the most, produce the most errors, and slow the team down the most.
  • Tightly coupled modules migrate together. Forcing them apart generates glue code that nobody wants to maintain.
  • The core with money and heavy business rules comes once the team has mastered the process, not before.

Feature Flags and Rollback: How to Migrate Without Downtime

A feature flag is a switch that decides, by route, by profile, or by percentage of users, whether a request goes to the old system or the new one. With it, the release follows a predictable path: first the internal team, then a pilot group, then everyone.

Rollback has to mean turning off the flag or switching the route, within minutes, with no new deploy. And it has to be tested beforehand. If the new module writes data in a format the legacy system doesn't understand, going back breaks everything. That's why the database schema changes in stages that are compatible with both versions.

For sensitive calculations, a useful technique is shadow execution: the request runs on both systems, only the legacy result is returned to the user, and any discrepancies go to a log. You find the differences before any customer sees them.

Once 100% of traffic is on the new system, keep the legacy system available for a while. Shutting it down the same day is unnecessary haste.

What to Measure During the Incremental Migration

Without measurement, "it got better" is just an opinion. Record a baseline of the old system before you start and compare module by module. Here's what we track:

  • Error rate and response time per route, legacy versus new.
  • Discrepancies in the characterization tests and in shadow execution.
  • Percentage of traffic served by the new system.
  • Support tickets per module, before and after the switch.
  • Time to deliver a simple change in a migrated module versus a legacy module.
  • Legacy routes still being accessed. When they reach zero, the legacy system can be shut down.

Common Mistakes and Why the Method Works for Other Languages

The most frequent mistake is freezing the legacy system for months while the new one is being built. The business doesn't stop, changes keep coming in, and the new system is born out of date. Another is using the migration to "improve" rules in the same step: then you can no longer compare the new system with the old one. And there's the classic of leaving the legacy system running forever, which means paying for two maintenance efforts.

None of this depends on PHP. Inventory, characterization tests, a coexistence layer, a database changed in stages, flags, and rollback work the same way in Java, .NET, Delphi, or old Python. The tool changes, not the method. If you're still torn between migrating gradually and starting over, the article on refactoring vs. rewriting from scratch can help you decide.

AI comes in as an accelerator: it helps read undocumented code, map dependencies, and generate draft tests. But the decision about what is a rule and what is a bug remains human. Timeline and cost depend on the size of the system, its coupling, the quality of the data, and the amount of undocumented business logic, and the article on how much it costs to modernize a legacy system goes into these factors in detail.

If you'd like to see how a screen or a flow from your system would look on the new stack before deciding anything, you can request a free prototype. And if you want to learn how we carry out this work, see our page on legacy system modernization.

From our experience

At SKAD, we refactored five of our own systems with AI support: SyncStop, 4Car, KaraokeApp, The Bee App, and DEM, the latter an equipment inspection system. All were plain PHP with jQuery and Bootstrap, and the apps were built in Cordova. Today they run on PHP 8.3 with Slim 4 for the API, React with Vite on the web, and React Native with Expo in the app. We kept the PHP language; what changed was the framework and the architecture. After the refactoring, maintaining these systems became lighter.

In another project, we rebuilt an animal management and care platform that was PHP 7 with Azure SQL, moving it to Slim 4, React with Vite, and React Native, on Azure. The database was kept. We migrated the data first and the functions afterward. One developer implemented it from a plan document, and we tracked progress through high-level checklists organized by user profile.

Frequently asked questions

Can you migrate a legacy PHP application without stopping operations?

Yes, if the migration is incremental: the new system coexists with the old one behind a proxy and takes over one route at a time. With feature flags and a tested rollback, each switch can be undone in minutes.

Do I need to update the PHP version before migrating to a framework?

Only as much as needed to run on a supported version and enable testing. Spending months fixing code that will be rewritten is usually a waste.

Which framework should I choose to replace plain PHP?

It depends on the team, the size of the system, and how it will be used. The choice matters less than the architecture: separating the API, business rules, and interface is what makes maintenance easier later.

How long does it take to migrate a legacy PHP system?

It varies a lot with the number of modules, the coupling, the quality of the data, and the lack of documentation. Only the inventory and characterization tests allow for an honest estimate.

What is the strangler pattern?

It means building the new system around the old one, redirecting route by route, until the legacy system no longer receives traffic and can be shut down. There is no cutover day.

Conclusion

Before hiring anyone for anything, do a simple exercise: list the ten most-used routes in your system, the tables each one touches, and what happens if one of them fails for an hour. That list already shows where to start and where the risk lies. If you'd like a second opinion on it, send us a message on WhatsApp and our team will run a diagnosis of your legacy system.

Keep reading