How I upgraded a Laravel 7 app to Laravel 13 in 3 days
A SaaS app on Laravel 7 and PHP 7.2, with almost no tests, moved to Laravel 13 and PHP 8.5 in 3 days with no downtime. Here's the order I worked in and where it got hard.
I recently took a client's SaaS app from Laravel 7 on PHP 7.2 to Laravel 13 on PHP 8.5. It took 3 days from start to live: 2 days of changes and 1 full day of testing. There was no downtime.
The twist is where it started. The app hadn't had a security fix since 2021, it had almost no tests, and a coding agent did most of the typing. That sounds like a recipe for a bad month. It wasn't, because of the order the work was done in.
Why upgrade an app that still works?
Because "it works" and "it's safe" stopped being the same thing years ago.
Laravel 7 stopped getting security fixes on 3 March 2021. PHP 7.2 reached its end on 30 November 2020. So this app had spent more than five years running on software that nobody patches any more. When PHP2Shell hit in October, apps like this were the ones with no fix to install. I wrote about that night in how I patched 35 servers after PHP2Shell.
There are slower costs too. New packages don't support old versions, so every new feature gets harder. And fewer engineers want to work on a codebase that's six major versions behind.
Why not jump straight from 7 to 13?
Because six upgrades at once means six sets of changes failing together, and no way to tell which one broke what.
Each Laravel version has its own upgrade guide and its own minimum PHP version. Going one step at a time keeps every change small enough to understand.
| Question | Minimum PHP | What changes most |
|---|---|---|
| Laravel 8 | PHP 7.3 | Model factories become classes, and models move to their own folder by default |
| Laravel 9 | PHP 8.0 | New versions of the Symfony and file storage libraries underneath |
| Laravel 10 | PHP 8.1 | Type hints across the framework, and long-deprecated methods removed |
| Laravel 11 | PHP 8.2 | A slimmer structure for new apps. Upgraded apps can keep theirs |
| Laravel 12 | PHP 8.2 | Mostly dependency updates. The Breeze and Jetstream starter kits stop getting updates |
| Laravel 13 | PHP 8.3 | Small behaviour changes, like renamed queue event properties and cache prefixes |
PHP moves up alongside Laravel. By the end the app was on PHP 8.5, the newest release, which Laravel 13 supports.
How did the coding agent and Shift split the work?
Two tools, each doing what it's good at. Laravel Shift is a service that makes the routine upgrade changes for one version and hands them over as a pull request. Claude Code, a coding agent that reads and edits the codebase from the terminal, handled everything Shift couldn't: the app's own code, the packages and the failures.
- 01ShiftThe routine changes for this version
- 02Claude CodeThe rest, working from the upgrade guide
- 03TestsRun the full set, fix what fails
- 04ReviewI read every change before it's kept
- 05CommitThen on to the next version
The agent was fast at the tedious part: renaming, updating method calls, working through a list of failing tests one by one. What it doesn't do is decide anything. My rules for the agent are the same on every job: it never merges its own work, never touches production, and anything in sign-in or payments gets a line-by-line read from me. I wrote more about these rules in how I use coding agents without shipping their bugs.
How do you add tests to an app that has none?
You start with the parts that would hurt most if they broke, and you write those tests before changing anything.
For a SaaS app that usually means signing in and out, signing up, payments, and the main screens customers use every day. I write those tests against the old version first and make sure they pass. From then on, every version step has to keep them passing. A step that breaks sign-in fails right there, not three versions later.
This is the most important step of the whole job. Without tests, "the upgrade worked" only means "the page loaded when I clicked it".
Where did it get hard?
Not in the framework. Laravel's own changes were well documented and mostly mechanical. The hard parts were the things around it.
- Abandoned packages. Some packages the app relied on had stopped being updated years ago and would never support PHP 8 or a new Laravel. Each one needs a decision: swap it for a maintained package that does the same job, or move the small piece of it the app actually used into the app's own code.
- The old auth scaffolding. The login and registration screens came from a scaffolding package built for the Laravel 7 era. They had to be brought up to date carefully, because auth is the one place where a quiet mistake either locks people out or lets the wrong person in.
- PHP 7.2 to 8.5. PHP 8 is much stricter. Code that used to pass the wrong type, read a missing array key, or call a removed function now fails loudly instead of quietly. Most fixes were small, but there were a lot of them, and that's exactly where an agent working through a list of failing tests earns its keep.
How did it go live without downtime?
By making the real switch boring. The third day was all testing. The upgraded app ran on staging, a copy of production with the same setup, and was tested there end to end before anyone touched the live site. Once it held up, it went live in one switch.
Nobody had to stop using the app while it happened.
Is your Laravel app due an upgrade?
- Is it on Laravel 11 or older?Yes →It's out of security support. Plan the upgrade now, one version at a time.No ↓
- Is it running on PHP 8.1 or older?Yes →That PHP gets no more fixes. Moving it up is part of the same job.No ↓
- Would nothing warn you if a change broke sign-in or billing?Yes →Write tests around those flows first. It's the cheapest step and the most important.No ↓
- Do any of your packages have no release from the last two years?Yes →Check each one for PHP 8 support before you start. They're where upgrades get stuck.No ↓
- Did the last emergency PHP patch skip you?Yes →Your version is too old to patch. Upgrade it before the next one.
- Still no →You're in good shape. Upgrade once a year, when the new version comes out, and it stays a small job.
An old Laravel app is rarely a reason to rebuild. Upgraded one step at a time, with tests first, it's usually days of work, not months. If yours is a few versions behind, that's what my code rescue and upgrades service is for. Or send me a note with your Laravel and PHP versions, and I'll tell you what the path looks like.