Platform migration
Legacy PHP to Laravel Migration
CodeIgniter, Zend, Symfony 2 or no framework at all - moved to Laravel incrementally, behind a router that sends traffic to whichever system currently owns the route.
An application written in CodeIgniter in 2014, or Zend, or Symfony 2, or in no framework at all - still running, still making money, and now expensive in a specific way. Nobody will touch it, the people who wrote it have gone, and the version of PHP it needs is out of support.
The proposal that usually arrives is a rewrite. It is the wrong shape for this problem and it is how these projects fail.
Why the rewrite is the risk
A rewrite means building a second system beside the first and switching when it is done. Three things follow from that, and all three are predictable.
The business stops receiving changes for the duration, because every hour spent on the old system is thrown away. The finish line moves, because the requirements were never written down and are being rediscovered from the code as they are hit. And the switch is a single day on which every rule nobody remembered is tested in production simultaneously.
The rules are the problem. A system that has run for a decade encodes decisions that exist nowhere else: why that customer group is exempt, why the export runs at 03:40, why one price field is ignored. A rewrite finds them one complaint at a time.
Moving route by route instead
Put a router in front of both systems. It sends each request to whichever application currently owns that path - Laravel for what has moved, the legacy application for everything else.
location /account/ { proxy_pass http://laravel; }
location / { proxy_pass http://legacy; }Now a migration is a sequence of small deployments rather than one large one. Each route that moves is live the same week it is written, each can be moved back by reverting one line, and the business keeps shipping the whole time.
What makes it work in practice is the order:
- Inventory first. Every route, every scheduled task, every integration, and what each touches. Built from the code and the database, not from recollection.
- Sessions and authentication shared. Both systems have to agree on who is logged in from the first day, or a user crossing the boundary is logged out. Usually a shared session store and one system owning the login.
- The database stays put. Both applications read the same tables. The schema is improved later, on its own, so that a regression is attributable to one change rather than two.
- Leaf routes first. Something self-contained and low-traffic, to prove the routing, the deploy and the rollback before anything important depends on them.
- Then by value. The routes that are changed most often, because those are where the cost of the old system is actually being paid.
- Scheduled work and integrations last. They have no users watching and the most hidden state.
The parts that are genuinely hard
Sessions. Two frameworks, two session formats. Either one system reads the other's format, or both move to a shared store with a common serialisation. This is decided before the first route moves, not discovered by a logged-out customer.
Legacy password hashes. You cannot convert what you cannot read. Laravel verifies against the old algorithm at login and rehashes on success, so the database converts itself as people sign in rather than through a reset email sent to everybody at once.
Tables the old system's ORM shaped. Composite keys, no timestamps, a
primary key called something else, a boolean stored as 'Y'. Eloquent models
are configured to those tables rather than the tables being changed to suit
Eloquent - that comes later, if at all.
Two codebases writing the same rows. The rule is that one system owns each write path at any moment. Both writing the same table is where the data corruption in these projects comes from.
What we do not do
We do not move everything. Some of these projects end with a small legacy application still serving a handful of routes nobody needs to change, and that is a legitimate finish - the goal was to stop the cost, not to reach a number.
We do not improve behaviour while moving it. A route is moved to do exactly what it did, bugs included, and is improved in a separate commit afterwards. Changing behaviour and location together means a regression has two possible causes and no cheap way to tell them apart.
How an engagement runs
The first thing we need is the routing table of whatever is there now, and an honest answer about which parts nobody understands any more.
From it comes a route inventory and a phased scope with a price for each phase. It says which routes move first, what runs alongside during the move, and which parts we are recommending you do not migrate at all. That document is what the contract attaches to.
Then the phases. Both applications run together until the last route moves, and every phase ends with something in production.
What you get
A routing layer with an explicit map of which system owns what, the inventory that map was built from, moved routes with tests written as they moved, and a sequence for the rest with the reasoning attached.
If the application is already Laravel and simply several versions behind, this is not the work you need - upgrades and rescue is, and it is a smaller job.
