"If it's working, don't touch it." We've all heard some version of that advice, and it's tempting to actually follow it. Why go anywhere near a stable application when everything's already fine?
We took on a project a while back that changed our answer. A Laravel admin panel built on Backpack, and it reminded us the real risk was never upgrading. The real risk is waiting so long that upgrading stops being an option.
A few terms first
A major version bump, an upgrade where the maintainers are telling you, explicitly, things might break. Patch releases are safe, major releases come with a list of things that changed on purpose.
A breaking change, something that worked one way in the old version and works differently, or not at all, in the new one. Deprecated methods, renamed classes, changed signatures.
An incremental upgrade path, moving through major versions one at a time instead of jumping straight to the target. Slower on paper, much faster once something breaks and you need to know why.
Regression testing, checking that everything that used to work still works, after a change that wasn't supposed to touch it. Not new feature testing, making sure you didn't quietly break something three steps removed.
Technical debt, the growing gap between the version you're running and the version that's actually supported. Costs nothing the day you skip an upgrade. Costs a lot more years later, when skipping stops being an option.
Where this one started
The application was on Laravel 8, Backpack 4, an older PHP version. Nothing broken, just old. The client wanted everything current, Laravel 12, Backpack 7, PHP 8.2+.
The tempting move: point composer at the newest versions, run the update, fix whatever complains. Laravel 8 to 12 in one shot, Backpack 4 to 7 in one shot, done. Except it's not done, it's deferred to whenever things start breaking, and by then you're debugging four major versions of change at once with no idea which one caused it.
Why we didn't take the shortcut
Lesson learned the hard way over the years: never jump multiple major versions in one move if you can help it. Every Laravel release carries its own breaking changes. Every Backpack release improves the admin panel, but also deprecates APIs, bumps dependencies, sometimes restructures how packages are organized.
Skip straight from Backpack 4 to 7 and the moment something breaks, you're stuck. Was that Laravel? Backpack? The PHP version change underneath both? Three major changes landing at once turns root-causing into guesswork.
So we followed the official upgrade path instead, one version at a time, testing at every stop.
Phase one: Laravel 8 to 9, Backpack 4 to 5
composer require backpack/crud:^5.0
composer update
Republished Backpack's assets after:
php artisan vendor:publish \
--provider="Backpack\CRUD\BackpackServiceProvider" \
--tag=public
Then, before touching anything else, went through every CRUD screen, relationship field, filter, permission, widget, and custom operation by hand. Only once that was clean did we move on.
Phase two: Laravel 9 to 10, Backpack 5 to 6
composer require backpack/crud:^6.0
composer update
Less about surviving the upgrade, more about improving the code while we were already in there. Refactored controllers that had gotten messy, tightened dependency injection, updated CRUD operations to current conventions, ran the same testing pass again.
Each upgrade wasn't just a version bump, it was a real chance to clean up cruft that had been quietly accumulating.
Phase three: Laravel 10 to 12, Backpack 6 to 7, PHP to 8.2+
Biggest jump, most structural change. Backpack 7 went modular on a lot of optional fields, rich text editors aren't bundled by default anymore:
composer require backpack/tinymce-field
or
composer require backpack/ckeditor-field
File uploads needed their own step:
php artisan backpack:upgrade-dropzone
Backpack ships an upgrade helper for a chunk of the mechanical changes:
php artisan backpack:upgrade
Never treated automated helper output as the finish line. After every automated step, went back through the code by hand and re-ran the full feature test again.
What actually mattered more than the upgrades themselves
The workflow stayed the same across all three phases:
composer update
php artisan optimize:clear
php artisan migrate
php artisan route:list
Then, every time: CRUD operations, relationship fields, upload fields, custom operations, filters, widgets, authentication, permissions. Not a skim, an actual walkthrough of each one. Only after that, the next major version. The upgrades themselves were mostly mechanical. The testing discipline is what kept any of it safe.
Takeaways
- Never skip major versions in one jump if you can avoid it, isolating a breaking change across four version bumps at once is close to guesswork.
- Automated upgrade helpers save time on mechanical changes, they're not a substitute for manually re-testing every feature.
- Test the same full checklist after every phase, not just once at the end, regressions introduced in phase one can hide until phase three.
- Technical debt is invisible day to day and expensive all at once, the longer an upgrade waits, the more versions stack up between where you are and where you need to be.
Don't be afraid of the upgrade, be more afraid of the day the application's fallen so far behind that upgrading feels impossible. One version at a time, test everything, keep the client in the loop. Full writeup with more context is on our Substack: https://ucodesoft.substack.com/p/why-timely-upgrades-matter-a-laravel



Top comments (0)