The age of a PHP application is the worst possible guide to whether you should refactor it, move it out capability by capability, or rewrite it. Refactor when valuable business logic and the architectural base can be retained. Use Strangler Fig when separable capabilities must move while the system stays live. Rewrite only when the current base cannot be developed safely and its required behavior has first been captured and verified.
- Legacy is measured by change risk: how hard is it to build, ship and if necessary reverse a business-correct change?
- Characterization tests, logs and metrics capture the behavior you actually need before any code is replaced.
- Refactoring, Strangler Fig and a rewrite are not maturity levels. Each is right under specific conditions and each fails under the wrong ones.
What makes a PHP system genuinely legacy
Its age is not the signal. Change risk is. A system becomes genuinely problematic when several of these conditions coincide:
- Unsupported runtime: the PHP version no longer receives security fixes, or it blocks the maintained libraries you need.
- Blocked dependencies: frameworks, packages or extensions are abandoned, mutually incompatible, or only run with undocumented patches.
- Invisible business logic: important rules live in controllers, database triggers, scheduled jobs, spreadsheets or one person's experience.
- No changeability: even small adjustments touch many modules because responsibilities, data access and side effects are tightly coupled.
- Weak testability: the behavior that matters can neither be checked automatically nor reproduced in a realistic test environment.
- Fragile operations: deployments, rollbacks, data corrections and incident diagnosis depend on manual steps only a few people know.
- Missing observability: logs, technical metrics and business control totals are not enough to prove correct behavior after a change.
An older PHP application with good test coverage, clear module boundaries, reproducible deployments and a supported runtime path can carry far less risk than a young but tightly coupled system. The practical definition is therefore: legacy is the gap between the change the business needs and the change the system safely allows.
As of 26 July 2026, the PHP project's supported-version table lists PHP 8.2, 8.3, 8.4 and 8.5 as supported branches. PHP 8.1 and all older branches have reached end of life.
| PHP branch | Status on 26 July 2026 | Active support through | Security support through |
|---|---|---|---|
| 8.2 | Security fixes only | 31 December 2024 | 31 December 2026 |
| 8.3 | Security fixes only | 31 December 2025 | 31 December 2027 |
| 8.4 | Active support | 31 December 2026 | 31 December 2028 |
| 8.5 | Active support | 31 December 2027 | 31 December 2029 |
That is a reason to act on the affected runtimes, not a reason to rewrite. Act immediately when an unsupported runtime is publicly reachable, when a regulatory or contractual requirement cannot be met on the current base, when recurring defects threaten money flows, permissions, deadlines or personal data, or when only one person can still operate the system safely. Reducing that risk does not automatically mean replacement: a runtime upgrade, an isolating security component or better observability may be the right first step.
Capture behavior before you replace code
Michael Feathers uses characterization testing to document a system's actual behavior rather than the behavior one wishes it had. Start with the paths where failure does the most damage:
- Amounts, invoices, discounts, taxes and other calculations
- Identities, roles, permissions and tenant boundaries
- State transitions and processes that must not be repeated freely
- Data imports, exports and external API calls
- Deadline-bound jobs and time-dependent rules
- Manual correction and restart paths
Before that comes an inventory: runtime and deployment chain, dependencies including PHP extensions, the leading data sources, integrations together with their behavior on late or duplicate messages, and the business rules that exist only in one person's experience. The cases that "do not normally happen" are exactly the ones a new build omits.
Logs and metrics complete the picture. Error rate, latency and failed jobs show operating quality; business control totals such as record counts, sums or status distributions show whether two implementations produce the same result. For browser-facing performance, keep laboratory measurements and field data apart and keep the method dated. Our article on what a Lighthouse score of 100 does and does not prove explains that boundary.
Characterization is not approval. Observed legacy behavior is evidence, not a specification. Results that are incorrect or merely historical should be documented as a deliberate change. Otherwise a good test suite preserves precisely the defect the modernization was meant to remove.
Refactoring: when the base still holds
Refactoring improves, in Martin Fowler's definition, the internal structure of existing code without changing its observable behavior. It is usually the best option when the business logic is valuable, the technical base is fundamentally sound and the system is testable enough. It rarely fails for technical reasons; it fails on discipline. If every cleanup also introduces a new framework, a new data model and new product features, you can no longer isolate the cause of a defect.
Strangler Fig: when one capability has to move
Strangler Fig rebuilds selected capabilities and lets the old and new paths run side by side through a transition, until the old path is no longer needed for that scope. It fits when business capabilities can be separated sensibly and the system has to stay live throughout. Shared databases, distributed transactions and hidden side effects can prevent a clean split. The benefit comes from the better business boundary, not from the number of deployments, so an extracted module may first live inside a modularized application. Define retirement criteria for the old path, or the transitional bridge becomes the next legacy layer.
Rewrite: only with captured behavior
A rewrite is justified when the existing base structurally prevents safe development and that cannot be removed economically in manageable steps. The threshold must be high, because a rewrite combines two uncertainties: the team builds a new system while reconstructing what the old one actually does. Be suspicious when the goal is mainly "remove old technology", when the new solution is meant to be finished before real users or production data test it, when data migration and integrations are treated as later implementation details, or when there are neither abort criteria nor a plan for independently usable partial results.
Which option fits which starting position
This matrix is not a scorecard. It shows which assumptions carry an option and where a single disqualifying criterion outweighs several positive signals.
| Decision axis | Refactor | Strangler Fig | Rewrite |
|---|---|---|---|
| Business logic worth keeping | Stays in place directly. | Stays usable behind clear boundaries, or moves deliberately. | Must be captured and re-verified first. |
| Architectural base | Sound, and can be improved step by step. | Partly sound; individual capabilities need new boundaries. | Blocks the required outcome and cannot be reshaped safely. |
| Data and integration risk | Existing paths remain and are improved deliberately. | Data ownership and synchronization must be settled for the transition. | Migration and cutover are high-risk workstreams in their own right. |
| Allowed downtime | Small releases within the existing operating model. | Capabilities move one after another. No blanket zero-downtime guarantee. | One large cutover, unless the work can be sliced. |
| Time to first value | Early, when the bottleneck is locally solvable. | Early to medium, when a capability is cleanly separable. | Later, unless a vertical slice ships on its own. |
| Rollback | Individually reversible, as long as data changes stay compatible. | Only while the old path and data contract are kept working. | Hard after a full data cutover. Rehearse it before go-live. |
The answer is rarely one label for the whole estate. A program can first raise the PHP runtime, then refactor a well-understood core, and extract a single fast-changing capability with Strangler Fig. A mixed path is sound when it separates risks. It is a problem when it merely starts three unclear projects at once.
The runtime upgrade is its own workstream
A version upgrade should not be smuggled into an architectural rebuild. The official PHP documentation provides a migration guide for every version transition. Work through them one at a time instead of mixing several generations of change into one opaque debugging space. Choose the target version by official support status and fit with your framework, packages, extensions and operating environment rather than by the highest number: modernizing onto 8.2 or 8.3 today already requires a plan for the step after it.
Prove the platform in the actual target environment. After installing the locked production dependencies there, run composer check-platform-reqs --no-dev: Composer documents that this checks the installed packages' PHP and extension requirements against the real platform and deliberately ignores config.platform. A config.platform entry can simulate a target during resolution but proves nothing about what that target provides, and --ignore-platform-reqs suppresses the requirements outright. A successful install using either shortcut is not a production sign-off.
Rollout, rollback and handover
Before every release, these must be settled:
- Which technical and business metric shows that the slice works correctly?
- Who watches the release, and who decides when a signal deviates?
- Which data changes are backward compatible and which are not?
- How do you prevent the old and new processing from executing the same state-changing operation twice?
- Up to which point can traffic be switched back to the old path?
- How is data created during a rollback reconciled?
A backup alone is not a rollback plan. Fowler's blue-green deployment guidance makes the constraint concrete: a fast traffic-switch rollback depends on the database remaining compatible with the old application version. Messages, files, notifications and third-party side effects need their own reconciliation or recovery procedure. When public URLs or visible user journeys change, redirects, canonicals, internal links and post-launch monitoring belong in the rollout too; our website relaunch checklist covers that web-facing part in detail.
At the end, the receiving team has to understand not only the code but the decisions behind it: architecture, data ownership, deployment and recovery paths, alerting thresholds and whatever transitional architecture remains. Test the documentation against a real task: can the team release, isolate a defect and roll back safely on its own? On our side, PHP and Laravel development combines that depth with a realistic upgrade path, and our custom software development is the broader frame when product, data, integrations and operations have to be reordered together.
Named proof, with a clear boundary: the published entsorgo platform case demonstrates Laravel delivery and a step-by-step rollout without interrupting daily operations. It does not demonstrate a legacy migration, a rewrite or a Strangler Fig project.