Originally published at hafiz.dev
Most Laravel apps don't fail upgrades because of breaking changes. They fail because composer update throws a wall of red text and the developer closes the terminal.
Laravel 13 shipped with "zero breaking changes" to application code. That's true. Your routes, controllers, and Eloquent models don't need touching. But your composer.json is a different story. Somewhere in your 30-50 dependencies, there's almost certainly a version constraint that won't resolve against laravel/framework:^13.0. And finding it manually means running composer why-not, reading cryptic output, fixing one conflict, discovering the next, and repeating until you either succeed or give up and decide to "upgrade later."
Four specific conflicts catch most developers. After looking at how these surface in real composer.json files, in GitHub issues, and in r/laravel threads, the same patterns keep showing up. Here's what each one looks like, why it happens, and how to fix it.
1. Your PHP Constraint Is Too Loose
This is the most common blocker and the easiest to miss. Your composer.json probably says something like this:
"require": {
"php": "^8.1"
}
Laravel 13 requires PHP 8.3 as the minimum. That constraint above technically allows 8.1, 8.2, 8.3, and 8.4. Two things can go wrong here.
First, if your server actually runs PHP 8.2, Composer will refuse to install Laravel 13 regardless of what your composer.json says. The error looks like this:
Your requirements could not be resolved to an installable set of packages.
Problem 1
- laravel/framework v13.0.0 requires php ^8.3 -> your php version (8.2.28)
does not satisfy that requirement.
Second, even if your server runs 8.3, a loose constraint like ^8.1 means your app could be deployed on 8.1 or 8.2, where Laravel 13 won't work. Tightening the constraint protects you from that.
The fix: Run php -v on production first. If it returns anything below 8.3, upgrade PHP before touching Composer. Then tighten your constraint to match:
"require": {
"php": "^8.3"
}
Don't skip this. Every other fix in this post is pointless if your PHP version is too low.
2. The Symfony 8 Surprise
This one is newer and won't hit you on day one. Laravel 13.0 through 13.2 work fine on PHP 8.3. But starting with Laravel 13.3, the framework allows Symfony 8 components (symfony/error-handler, symfony/console) that require PHP 8.4.
If you're on PHP 8.3 and you run composer update after the initial upgrade, you might get hit by this weeks later:
Problem 1
- laravel/framework v13.3.0 requires symfony/error-handler ^7.4.0 || ^8.0.0
-> satisfiable by symfony/error-handler[v8.0.8].
- symfony/error-handler v8.0.8 requires php >=8.4
-> your php version (8.3.30) does not satisfy that requirement.
The fix: You have two options. Either upgrade to PHP 8.4 (the better long-term choice), or pin Symfony to 7.4 in your composer.json:
composer require symfony/console:"^7.4" symfony/error-handler:"^7.4"
This keeps you on Symfony 7.4 while running Laravel 13.3+. It works, but it's a temporary workaround. PHP 8.4 is where you want to be.
If you're reading the full Laravel 13 upgrade guide, this particular conflict isn't covered there because it appeared after the initial release. It's exactly the kind of thing that catches you between "I upgraded successfully" and "why is production broken after composer update?"
3. Spatie Packages Pinned to Old Major Versions
If you use any Spatie packages (and most Laravel apps do), check their version constraints carefully. Older major versions often don't support Laravel 13. The most common offenders:
-
spatie/laravel-permissionv5 doesn't support Laravel 13. You need at least v6. -
spatie/laravel-medialibraryolder major versions don't support Laravel 13. Check your installed version against the latest release. -
spatie/laravel-activitylogrequires at least v4.12 for Laravel 13 support. Earlier v4 releases won't resolve.
The error looks like a typical version mismatch:
Problem 1
- spatie/laravel-permission v5.11.1 requires illuminate/database ^9.0|^10.0|^11.0|^12.0
-> found illuminate/database v13.0.0 but it does not match ^9.0|^10.0|^11.0|^12.0.
The fix: Upgrade each Spatie package to the latest major version before upgrading Laravel. Check the GitHub releases page for each one. Most have migration guides. Spatie generally ships Laravel support within days of a new release, and they've even backported Laravel 13 compatibility to some older branches so third-party packages have time to catch up.
One tip: run composer outdated --major to see which packages have major version jumps available. That command shows you the gap without trying to resolve anything.
4. Testing Packages That Quietly Block Everything
PHPUnit and Pest are required by almost every Laravel app, but they sit in require-dev and tend to get ignored during upgrades. They shouldn't be.
Laravel 13 requires phpunit/phpunit:^11.5.50 or ^12.0. If your composer.json still has "phpunit/phpunit": "^10.0", that's a blocker. Same with Pest: you need at least Pest 3.8.5 for Laravel 13 support (earlier 3.x releases pin a PHPUnit version that's too low).
The error often looks something like this, showing up in require-dev where some developers skip reading:
Problem 1
- phpunit/phpunit 10.5.46 requires sebastian/comparator ^5.0
-> found sebastian/comparator 6.3.1 but it does not match ^5.0.
That's PHPUnit 10 conflicting with a transitive dependency that Laravel 13's newer Symfony components pull in. It's not obvious at all from the error message.
The fix: Update your testing packages first:
composer require --dev phpunit/phpunit:^12.0
# Or if you use Pest:
composer require --dev pestphp/pest:^3.0
If you've been putting off the Pest 4 migration, now is the time. Pest 4 ships with full Laravel 13 support and a cleaner API.
Or Skip All of This
Every conflict above follows the same pattern: something in your composer.json doesn't match what Laravel 13 expects, and finding it requires running commands, reading error output, and debugging one conflict at a time.
There's a faster way. Paste your composer.json into the Laravel Upgrade Analyzer and it'll show you exactly which dependencies need attention. It checks 33 packages (including PHP version, Symfony components, and testing tools) against Laravel 13's requirements and flags each one as Blocker (stops the upgrade entirely), Breaking (needs a major version bump), or Watch (minor bump, low risk). The whole thing takes about 5 seconds and nothing gets stored.
If you went through the Laravel 12 to 13 upgrade guide and already upgraded, the analyzer still catches stale package versions and constraint mismatches you might have missed.
And if you don't want to deal with any of this yourself, I do Laravel upgrades. Send me your composer.json and I'll scope it.
FAQ
Does the Upgrade Analyzer work for older upgrades like Laravel 11 to 12?
Right now it's focused on Laravel 13 specifically. The rules check PHP version requirements, first-party Laravel packages, and 33 of the most common third-party packages against Laravel 13 compatibility. Support for older upgrade paths may come later.
What if one of my packages isn't in the analyzer's rules?
The analyzer covers 33 packages (25 auto-derived from Packagist, 8 hand-curated). If your package isn't covered, you can check manually by running composer why-not laravel/framework:^13.0 or checking the package's GitHub releases for Laravel 13 support. Found a package that should be included? Let me know and I'll add it.
Is my composer.json stored or shared?
No. The analyzer processes your file server-side and discards it immediately. Nothing is stored, logged, or shared. You can verify this in the page footer.
Top comments (0)