Skip to content
(01) // ENGINEERING

Keeping Laravel upgrades boring

5 MIN READENGINEERINGUPDATED 15 SEPT 2026

Laravel upgrades go wrong when they are batched into one heroic branch. A sequenced approach — dependencies, framework, deprecations — keeps them uneventful.

An upgrade that has been deferred for three major versions is not an upgrade, it is a migration with an optimistic name. The way out is sequencing, not effort.

The failure is almost never technical difficulty. It is that the work was batched: one branch, one developer, three framework versions, forty package updates and a PHP bump, merged in a single event that nobody can review and nobody can roll back cleanly.

The sequence

  1. Get a signal. Whatever test coverage exists, make it run in CI and go green. Add tests around the money paths first.
  2. Dependencies before the framework. Update packages to versions that support both your current and target framework release.
  3. One major version at a time. Follow the official upgrade guide literally. Merge, deploy, watch.
  4. Deprecations last. Once you are on the target version, clear deprecation warnings as ordinary maintenance.

Get a signal first, even a bad one

You cannot upgrade safely without a way to tell whether you have broken something, and on a neglected codebase that mechanism usually does not exist. Building perfect coverage first is a project of its own and will not be funded, so do not propose it.

What you need is narrower: a green CI run, and tests around the paths where a silent failure is expensive. Checkout. Invoicing. Permissions. Anything touching money or access. A dozen feature tests hitting real routes against a real database will catch more upgrade breakage than a hundred unit tests around code the framework does not touch.

Static analysis earns its place here too. Getting PHPStan or Larastan running at a low level and holding the line is cheap, and it catches an entire class of upgrade breakage — removed methods, changed signatures, now-nullable returns — before anything executes.

Dependencies before the framework, and PHP before both

Composer will refuse the framework bump while a package pins the old version, so the dependency pass is not optional; it is just a question of whether you do it deliberately or discover it mid-branch.

Do the PHP version first where you can. Running the current PHP on the old framework release is usually a smaller change than the framework upgrade itself, and it means you are only ever changing one variable at a time. When something breaks after the framework bump, you want to be certain the runtime is not a suspect.

Every dependency pass turns up one abandoned package. Decide early: replace it, fork it, or vendor it in and own it. Leaving it is a decision too — it just gets made again, more expensively, at the next upgrade.

One version at a time, always deployable

Every step above should end with a deployable application. A branch that cannot be deployed for six weeks accumulates its own risk, independent of the upgrade: it drifts from main, it collects conflicts, and it becomes an all-or-nothing merge nobody wants to be responsible for.

The upgrade guides are written for one hop. Skipping intermediate versions means combining several sets of breaking changes, and when something fails you no longer know which release caused it. Two hops, each merged and deployed, is faster in wall-clock time than one hop that has to be debugged.

Watch after each deploy, and know what you are watching: error rates, queue depth, job failure counts, and the slow-query log. Framework upgrades cause subtle behavioural changes more often than they cause crashes — a changed default on eager loading, a different serialisation of a job payload, a middleware ordering change — and none of those announce themselves in a stack trace.

The trade-offs

Upgrading costs time you could spend on features, and the return is invisible. Nobody thanks a team for staying current. The honest framing is risk, not virtue: an unsupported release means a security patch you cannot apply without an emergency project, at a moment you do not control.

LTS is a real option, and it is not free. Staying on a long-term-support release buys stability and costs you the ecosystem — packages drop support for old framework versions faster than the framework drops support for itself. It is a legitimate choice for a system in maintenance. It is a poor one for a product still growing features.

Incremental has overhead. Four merges, four deploys, four windows of attention cost more coordination than one. That overhead is the price of every step being reversible, which is exactly what you are buying.

Queue workers are the classic deployment trap. Jobs serialised by old code get executed by new workers mid-deploy. Drain the queue, or make payloads version-tolerant, before the deploy rather than during the incident.

Then stop the drift

Schedule a recurring hour for dependency updates. The reason upgrades become projects is that nobody owns the small version bumps.

Automate the mechanical part — a bot opening dependency pull requests against a CI run that means something — and keep the review human. Patch and minor updates merge on green. Majors get read. The point is not that the bot is trustworthy; it is that the work arrives in one-hour pieces continuously instead of six-week pieces every three years.

This is most of what our Laravel work actually is, and the same sequencing applies to older PHP codebases where the framework is the thing you are migrating toward.

Sources and further reading

laravelphpmaintenance
START A
PROJECT
EMAIL CHANNELHELLO@BEECODEX.COM
SYSTEMS ONLINE / TAKING NEW PROJECTS