Skip to content

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:

  1. Inventory first. Every route, every scheduled task, every integration, and what each touches. Built from the code and the database, not from recollection.
  2. 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.
  3. 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.
  4. Leaf routes first. Something self-contained and low-traffic, to prove the routing, the deploy and the rollback before anything important depends on them.
  5. Then by value. The routes that are changed most often, because those are where the cost of the old system is actually being paid.
  6. 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.

Scope and terms

Engagement model
Fixed scope, agreed in writing before work starts. Not a day rate against an open backlog.
Price and timeline
Both are set per project, once the scope is. Quoted together, before anything is built.
What we need from you
One person who can approve decisions, and access to your repository and issue tracker.
Not included
Anything outside the agreed scope. It becomes its own scope rather than a variation order.
Third-party costs
Hosting, licences, API fees and SaaS subscriptions are contracted and paid by you.
Invoicing
Codefacture Yazılım A.Ş., Türkiye. EUR, USD or GBP by bank transfer, with no Turkish VAT on exported services.

Frequently asked questions

Is this different from a rewrite?
Yes, and the difference is what makes it survivable. A rewrite builds a replacement beside the original and switches when it is finished, which means a long period with no releases and a launch day that discovers every undocumented rule at once. This moves one route at a time, in production, with both systems running.
How long do both systems run in parallel?
Months, usually, and that is the design rather than a failure of it. The old system keeps serving what has not been moved yet, so there is never a point where the business is waiting on the migration to finish before anything else can ship.
What happens to the database?
It stays where it is, at first. Both systems read the same tables, which is what lets a route move without moving data. Schema improvements come after the code has moved, not during - changing both at once removes your ability to tell which change broke something.
Our application has no tests and no documentation.
That is the normal condition of a system old enough to need this. The first phase records what it currently does, from the routes and the database rather than from anybody's memory, and that inventory is what the sequence is built from.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page