A Laravel 11 to 12 upgrade is, for most apps, a small job — Laravel 12 was a maintenance-focused release with very few framework-level breaking changes. The thing that actually bites you is not Laravel itself; it is your dependencies. A third-party package still pinned to ^11, a PHPUnit or Pest major you forgot to bump, or a config file you customized two years ago and never reconciled with the new skeleton. The fix is to treat it as a dependency exercise: bump laravel/framework to ^12.0, run composer update, then methodically chase down every package that has not caught up — behind a branch with your test suite running in CI. Do that and the upgrade is usually a 20-minute pull request, not a weekend.
Is the Laravel 11 to 12 upgrade actually a big deal?
No — and that is the honest answer most upgrade posts bury under a wall of changelog entries. Laravel 12 shipped as a deliberately small release. There is no new application skeleton to migrate to (that already happened in the jump to 11), and the framework's own breaking changes are minimal. The minimum PHP version stays at 8.2, so you do not need to touch your runtime. If your app is clean, the diff is two lines in composer.json and a green test run.
What changed under you is the dependency floor. Laravel 12 requires newer majors of the dev tooling almost everyone has installed — Collision, PHPUnit, Pest, Larastan — and Carbon 3. The breakage you hit is almost never "Laravel 12 removed a method I use." It is "composer update refuses to resolve because three of your packages still demand laravel/framework ^11.0." The whole skill of this upgrade is reading that resolver output.
What do I actually change in composer.json?
Start by bumping the framework constraint, and at the same time bump the first-party dev packages to the majors Laravel 12 expects. Doing them together avoids a second round of conflicts where Composer upgrades the framework but leaves PHPUnit on an incompatible major. Here are the constraints that matter for a typical Laravel 11 app.
{
"require": {
"php": "^8.2",
"laravel/framework": "^12.0"
},
"require-dev": {
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^11.5",
"larastan/larastan": "^3.0",
"pestphp/pest": "^3.8"
}
}If you do not use Pest or Larastan, drop those lines — only bump what you have. Then let Composer resolve the graph. Use --with-all-dependencies so it is allowed to update transitive packages too, not just the ones you named; without it Composer will often refuse because a dependency-of-a-dependency is the real blocker.
# Resolve the whole graph in one pass
composer update laravel/framework --with-all-dependencies
# Then see which first-party packages are still behind
composer outdated --directcomposer outdated --direct lists only your directly-required packages and shows current vs latest. This is where you catch the supporting libraries that have their own Laravel 12-compatible majors waiting — things like laravel/sanctum, laravel/telescope, or spatie/* packages that ship a new major when the framework bumps.
Composer refuses to update — how do I find the package blocking it?
This is the moment that turns a 20-minute upgrade into an afternoon, and the trick is to stop guessing. When composer update spits out a wall of "requires laravel/framework ^11.0 -> found laravel/framework[v12...] but these were not loaded," ask Composer directly which installed package is incompatible with the version you want.
# Ask: what stops me from installing framework 12.0?
composer why-not laravel/framework 12.0The output names the exact package and the constraint it is holding. From there you have three options, in order of preference:
- Upgrade the package. Check its releases — most well-maintained packages tagged a Laravel 12-compatible version within days of the 12.0 release. Bump the constraint and re-run the update.
- Find a maintained fork or a PR. If the package is stale, there is often an open PR adding ^12.0 support; you can point composer.json at that branch with an inline alias while you wait for a tagged release.
- Replace or remove it. If a package is abandoned and blocking your whole upgrade, that is a signal in itself — a dependency that cannot keep up with the framework is a liability you will hit again next year.
Resist the urge to paper over it by forcing --ignore-platform-reqs or pinning a wildcard. You are not fixing the conflict, you are hiding it until it surfaces at runtime. While you are in the dependency graph, run a security pass too — I walk through that in my notes on reading and fixing a composer audit report, and an upgrade is the ideal time to clear flagged advisories alongside the version bumps.
What about my customized config files?
Laravel does not run a migration tool over your config/ directory on upgrade — those files are yours, copied into your repo at install time and edited since. Laravel 12 ships small changes to the default skeleton's config files, and the framework falls back to internal defaults for keys you have not published, so you are usually fine leaving your existing files alone. The risk is the opposite: a config you customized references a behavior the framework changed, and you never notice because nothing errors.
The pragmatic check is to diff your config files against a fresh 12.x skeleton and eyeball anything you have touched. You do not have to adopt every change — just understand what drifted.
# Compare your config to a fresh Laravel 12 app
composer create-project laravel/laravel:^12.0 /tmp/l12-skeleton
diff -ru config/ /tmp/l12-skeleton/config/On a Laravel point release, the framework rarely breaks your app — your dependency graph does. Read the resolver, not the changelog.
How do I verify the upgrade actually worked?
Once composer update is green, clear every cached artifact before you trust anything. Stale compiled config, cached routes, or a cached service container will happily serve the old wiring and make a successful upgrade look broken — or worse, make a broken one look fine.
# Wipe all cached/compiled state
php artisan optimize:clear
# Confirm the versions you are actually running
php artisan about
# Run the full suite — this is the real verdict
php artisan testphp artisan about prints the Laravel and PHP versions, the cache and queue drivers, and the environment in one screen — confirm it reads Laravel 12.x before you celebrate. Then the test suite is the actual judge. If you do not have meaningful coverage of your critical paths, an upgrade is the moment that absence costs you; my post on writing testable service classes in Laravel covers building the kind of coverage that makes upgrades boring.
How should I roll this out without taking down production?
Never run a framework upgrade as a hotfix on the production server, and never on a Friday. Do it as a normal feature branch with the whole pipeline running against it.
- Branch it: git checkout -b chore/laravel-12-upgrade. Keep the dependency bumps and any code fixes in one reviewable PR.
- Let CI be the gate: your pipeline should run composer install and the full test suite on every push, so the upgrade either goes green or tells you exactly what broke. I wire mine up in my CI/CD with GitHub Actions for Laravel guide.
- Commit the lockfile: the composer.lock changes are the heart of this PR — review them, do not regenerate them on deploy.
- Deploy in a quiet window with a rollback ready: the previous release tag plus the old composer.lock is your instant escape hatch.
If you maintain a lot of Laravel apps and want the busywork done for you, Laravel Shift is a paid service that automates the mechanical parts of the upgrade — config diffs, constraint bumps, deprecated-API rewrites — as a pull request you review. It earns its keep across a fleet. For a single app, the manual path above is fast enough. Either way, the official upgrade guide at laravel.com/docs/12.x/upgrade is the source of truth for the exact version-specific notes — read the high-impact section before you start.
The reason this upgrade earns a reputation it does not deserve is that people conflate "Laravel 12 broke my app" with "my dependency graph could not resolve." They are different problems with different fixes. Bump the framework and the first-party tooling together, use composer why-not to name every blocker instead of fighting the resolver blind, clear your caches, and let the test suite render the verdict on a branch. Done that way, Laravel 11 to 12 is one of the calmest framework upgrades you will ever ship — which is exactly what a maintenance release should feel like.

