Laravel 12 arrived in February with an upgrade guide shorter than most patch notes. A major version that changes almost nothing is either a mature framework or a stalled one, and the interesting part of this upgrade turned out to be the thing that was removed rather than anything that changed.
The symptom
$ composer require laravel/framework:^12.0 --with-all-dependencies
$ vendor/bin/phpunit
Tests: 1,414 passed
$ git diff --stat
composer.json | 2 +-
composer.lock | 88 ++++++++--------
# elapsed: 55 minutes, of which 50 was CI.One line changed in the application. That is the whole upgrade, and writing it down is worth doing because the record of an uneventful upgrade is the evidence for having kept up with the previous ones.
Why it happens
An annual release cadence means a major ships when the date arrives rather than when there is something large to ship. The alternative — waiting until there is — produces the multi-year majors that make upgrades into projects.
The fix
What actually changed
for an existing application:
the starter kits moved out of the framework into
separate repositories
a handful of defaults for NEW installs
a minimum PHP of 8.2, which we exceeded by two
a small number of deprecated methods removed, none
of which we used
for a new application:
the starter kits are now a choice made at
installation rather than a scaffold command, and
there are three of them.The one deprecation that mattered
// removed in 12.0
$request->merge(['x' => 1])->all();
// which we did not use. what we did use:
Rule::unique('users')->ignore($user);
// unchanged, and the deprecation notice in 11.x was
// for the string-based form we had already migrated
// away from in 2024 — which is why the upgrade was 55
// minutes rather than a day.
The cheapness of this upgrade is entirely a consequence of having acted on the previous version’s deprecation notices, which is the same lesson as the last three PHPUnit majors. A deprecation channel that logs somewhere somebody reads is the mechanism, and it is one configuration line that has now been set to null twice by accident.
Reading the starter kit as a reference
what a starter kit provides:
registration, login, password reset, email
verification, two-factor, a profile page, a settings
page, and a frontend stack decision
what we have:
all of the above, written between 2018 and 2021,
with our own two-factor flow
what an hour of reading theirs found:
our password reset did not invalidate other sessions.
theirs does.
one commit, eleven lines, and a genuine security
improvement from reading somebody else's complete
implementation of a thing we wrote piecemeal.The session invalidation is the finding and it is the kind that only surfaces by comparison — nothing about our implementation looked wrong, and the absence of a step is invisible when you are reading your own code. A complete reference implementation is the cheapest audit available and it arrives free with every framework.
Why not adopt it
the dependency a package, its frontend stack,
and its opinions about routing
the auth code we
already have four years old, tested,
integrated with our
authorisation model
the migration replacing working
authentication is the highest-
risk change available for the
lowest visible benefit
an eight-year-old application is not the audience for a
starter kit, and reading it is worth more than adopting
it.Verifying it worked
$ php artisan about --only=environment
Laravel Version 12.0.4
PHP Version 8.4.3
$ ./bin/route-diff --before=routes-11.json
routes: 188, identical
middleware: identical for all 188
$ ./bin/response-times --sample=2000
p50 38ms # 38ms before
p95 178ms # 180ms before
$ vendor/bin/phpunit --filter Session
✓ a password reset invalidates other sessions
# added, and failing before the fixThe route diff is the check worth keeping for any framework upgrade, because the failure mode is a middleware that quietly stops applying to one group. The new session test is the actual output of this upgrade and it has nothing to do with the version number.
What this costs
Nothing, this year. The cost of an annual cadence is that it produces a major every twelve months whether or not anything warrants one, and each of those is a version constraint to bump, a CI run and a paragraph of reading — which is cheap and is not free, and compounds across every dependency that has adopted the same rhythm.
It also means the meaningful signal is in the deprecation notices rather than in the version number, which inverts the usual instinct. A major that changes nothing and a minor that deprecates six methods are not distinguishable from the outside, and the second one is where the work is.