The Problem
A product built over several years on vanilla PHP had accumulated the usual consequences of rapid growth without architectural governance: business logic entangled in view files, no test coverage, direct database calls scattered across hundreds of files, and a deployment process that required manual file transfers and hopeful prayers.
The immediate trigger for the engagement was a requirement to add significant new functionality. The estimate to build on the existing codebase was double what it would take on a well-structured foundation. The engineering team wanted the codebase modernised before the new build began.
Constraints
- Production traffic could not be interrupted — big bang rewrites were not an option.
- The existing team had limited Laravel experience and needed to own the result.
- There was no meaningful test suite, so regression risk was high.
- Timeline was fixed by an external product deadline.
Approach
The migration was structured as a strangler fig: new Laravel routing coexisted with the legacy code behind a shared entry point, and each functional area was migrated, tested, and verified before the legacy version was decommissioned. This allowed the team to release continuously throughout the engagement.
Key steps in sequence:
- Mapped all application entry points and grouped them by business domain.
- Established a test harness against the existing behaviour before touching any code.
- Introduced Laravel's routing, service container, and Eloquent incrementally by domain.
- Extracted business logic from procedural files into typed service classes.
- Introduced feature flags to allow parallel routing during cutover.
- Ran both codepaths in shadow mode for critical paths before final cutover.
Technical Decisions
Laravel was chosen over Symfony because the team would own the codebase post-engagement and Laravel's conventions are more immediately accessible to developers without prior framework exposure. The database schema was left largely intact during the migration to reduce risk — a separate schema improvement phase was scoped as a follow-on engagement.
Test coverage was built from the outside in: integration tests covering HTTP responses first, then unit tests around extracted service classes as they were introduced.
Outcomes
- All production traffic served from the Laravel application with no disruption to end users.
- A meaningful test suite in place prior to the new feature build beginning.
- Deployment moved from manual FTP to an automated pipeline.
- The engineering team onboarded on the new structure and able to extend it independently.
- New feature development began on schedule, without the technical debt cost previously estimated.