The first two case studies moved an application to a newer version of the same framework. This one replaces the framework. InvoicePlane runs on CodeIgniter 3: 26 modules, 50 controllers, 169 views, 60 AJAX endpoints read by its own JavaScript, PDF, mail, Stripe and PayPal. We put Laravel 13 in front of it, relayed everything to CodeIgniter, and moved modules across one at a time, measuring each. The numbers below are what a quote for this shape of work is built on.
This is a measurement, not a finished migration. Nine of twenty-six modules are on Laravel; CodeIgniter still serves the other seventeen behind Laravel, and the application works end to end at every point. We stopped once the rate per module was measured, because that rate is the number that matters, and we would rather publish it than guess it.
| Before | After nine modules | |
|---|---|---|
| PHP | 8.1 | 8.4 for everything the browser talks to |
| Framework | CodeIgniter 3.1.13, 26 modules | Laravel 13.38 in front, serving 9 modules; CodeIgniter serving 17 behind it |
| Who answers a request | CodeIgniter | Laravel, or Laravel relaying to CodeIgniter. The browser cannot tell |
| Login and session | CodeIgniter | Still CodeIgniter, read by Laravel through a file-level bridge. Auth moves last |
| Automated tests | 0 | 39 black-box tests, 103 pinned responses, a 73-request live read diff, two live write-flow diffs |
A module with joins, a view page and AJAX lookups: about 25 minutes. The first module of each new shape: 60 to 90, because it builds the scaffold the rest reuse. Nine modules, 410 minutes, every page identical to what CodeIgniter rendered.
| Stage | What happened | Minutes |
|---|---|---|
| Understanding | No assessment existed. The architecture written from the code: modules, base controllers, the HMVC loader, auth and password scheme, mail, PDF, payment gateways, the settings table that holds most of the behaviour | 25 |
| Baseline | PHP 8.1 in Docker. The setup wizard driven by script, carrying the CSRF token every POST needs. Alpine images fail on the first request; Debian it is | 35 |
| Safety net | 39 tests, 103 fixtures. The seed is created through the application's own forms and AJAX, so every amount and number is the application's. Five findings before a line moved | 75 |
| Scaffold | Laravel in front, a catch-all that relays everything. Suite green through it with nothing migrated; 73 of 73 requests identical | 50 |
| First page | Session bridge, shared layout, view helpers, a converter from CodeIgniter views to Blade, the dashboard. Matched its fixture on the first render | 90 |
| Five small modules | One base, five eight-line subclasses. Writes needed flash messages written back in CodeIgniter's own session format, its CSRF rule, its 404 page. A third instrument: the write-flow diff | 60 |
| Three mid-size modules | Products with its lookup modal, projects with a view page, tasks with form defaults. Four of Laravel's defaults that change what is stored, found and matched | 75 |
| Total | Laravel code: 1,404 lines. Templates: 84, all but three generated by a 118-line converter and verified against the pinned renders. Test and comparison code: 1,009 lines, reusable | 410 |
The nine modules are 3,808 of the 33,111 module lines, the easy nine. The remaining seventeen hold the invoice and quote forms, the guest portal with online payment, mail, settings and users. From the measured rates and their size, finishing is 15 to 25 more hours of engineering, plus review. That range, with the reasons it is a range, is what the quote is built on.
Six findings, pinned exactly as they are and quoted separately rather than fixed in passing:
ALTER TABLE on the tax-rates table from a posted value; posted empty, every save is a SQL error.Every migrated page matched its recorded text on the first or second render, and the test suite was green. Two other instruments were not, and what they caught was not in the application's code at all.
The live write diff runs the same sequence of form posts on both stacks from the same seed and compares every response and every row left behind. It found four of Laravel's own defaults quietly changing what gets stored: empty form fields turned into NULL and input trimmed by global middleware, where CodeIgniter does neither and NOT NULL columns rely on it; a strict SQL mode refusing an empty string into an integer column where CodeIgniter's driver strips every strict flag; the CSRF token left in the posted data, which a date helper's "was anything posted" check depends on; and the template engine keeping a newline PHP swallows, turning one line of an address into three.
The live read diff found a flash message shown twice (CodeIgniter's flash lifecycle has two marks and one of them means "already shown"), a redirect with a body where CodeIgniter sends none, and utf-8 for UTF-8.
None of these would have shown up in a test suite written against pages, and all of them would have shipped. In a cross-framework migration the target framework's defaults are behaviour, and the only thing that sees them is a comparison that writes the same data through both frameworks and reads the rows back. All seven are now in the scaffold every later module inherits.
Two containers, one database. Laravel on PHP 8.4 is the only thing the browser reaches. A catch-all route relays everything Laravel has no route for to CodeIgniter and relays the answer back: cookies, redirects, downloads, the lot. Each migrated module's routes are registered ahead of the catch-all and simply stop reaching it. CodeIgniter still owns login and the session; Laravel reads the session file through a shared volume and writes only flash messages, in CodeIgniter's format, so a message set on one side shows exactly once on whichever side renders next. Auth moves last, and the day it does, the relay and the second container are deleted.
Two things cost time and are now written down. Laravel's fallback route answers GET only, so the catch-all is an any-method route. And two single-threaded PHP development servers deadlock when the back one fetches its own assets while the front one waits on it, which does not happen under FPM but does in a staging environment built from the built-in server.
The nine modules we moved are the easy nine. The invoice and quote forms carry most of the application's logic in JavaScript that talks to eighteen AJAX endpoints whose JSON shapes are now pinned; the guest portal has online payment we cannot exercise without a sandbox; the settings form reshapes a table. A quote for this application would be the measured range above with those three named as the places it could go wrong, and the strangler route is what makes "it could go wrong" a pause rather than a rollback.
A 48-hour exposure check is $750, credited against the migration. It confirms your real position in writing and ends with a fixed quote for the move.
Book an exposure check Run the free check first