Upgrading Laravel 10 or 11 to Laravel 13: what breaks and how to fix it
By Ryan Murdoc, founder of Unstale · Published 13 October 2026 · Last checked October 13, 2026 · 6 min read
Laravel 10 and 11 no longer get security fixes. Laravel 12 loses them on February 24, 2027.
Laravel 10 stopped getting security fixes on February 4, 2025, and Laravel 11 on March 12, 2026. Laravel 12 follows on February 24, 2027. If you're upgrading now, aim for Laravel 13, which is supported until March 17, 2028.
You can't skip versions in one jump. Go 10 → 11 → 12 → 13, get the tests green at each step, and commit each step separately. Laravel 11 is the hard one. 12 and 13 are small.
This guide comes from the official upgrade guides plus what we hit upgrading real open-source Laravel apps.
Before you start
- PHP 8.3 or newer. Laravel 11 and 12 need PHP 8.2; Laravel 13 needs 8.3. Upgrade PHP first and get the tests green on your current Laravel.
- Baseline the tests. Run the suite and record what passes. Note any tests that fail because they need the network.
- Dump your schema. Run
php artisan migrate:freshon an empty database for every driver you support (MySQL, PostgreSQL, SQLite...) and save a schema dump. You'll compare against it later. - You don't need the new app structure. Laravel 11 introduced a slimmer skeleton without
app/Http/Kernel.php. Existing apps keep working with the old structure. Moving is optional and makes the diff much bigger.
Step 1: Laravel 10 → 11
Dependencies
In composer.json:
laravel/framework→^11.0,php→^8.2nunomaduro/collision→^8.1laravel/sanctum→^4.0,laravel/passport→^12.0,laravel/cashier→^15.0,laravel/telescope→^5.0,laravel/spark-stripe→^5.0(whichever you use)
Sanctum, Passport, Cashier, Telescope and Spark no longer load their own migrations. Publish them, or fresh installs and CI will be missing tables:
php artisan vendor:publish --tag=sanctum-migrations
For Sanctum 4, update the middleware entries in config/sanctum.php (authenticate_session, encrypt_cookies, validate_csrf_token) to the class names given in the upgrade guide.
Then check every other package. Abandoned packages are common at this step: one app we upgraded depended on a .env editor with no Laravel 11 release and needed a maintained fork.
Doctrine DBAL is gone
Laravel 11 removed its Doctrine DBAL integration. Methods like DB::getDoctrineSchemaManager(), getDoctrineColumn() and registerDoctrineType() no longer exist. Use the native schema methods:
// Before
$tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();
// After
$tables = array_column(Schema::getTables(), 'name');
Search the whole codebase for Doctrine, including code your tests never run. In one app, the install command called a removed method inside try { ... } catch (Throwable $e). Instead of crashing, it told users "Cannot connect to the database" on a perfectly healthy database. No test covered it.
Migrations that change columns
When you modify a column with ->change(), Laravel 11 drops every attribute you don't list again. It used to keep them.
// Laravel 10: keeps unsigned, default and comment
$table->integer('votes')->nullable()->change();
// Laravel 11: you must restate everything you want to keep
$table->integer('votes')->unsigned()->default(1)->comment('The vote count')->nullable()->change();
Old migrations still run on fresh installs and in CI, so read every ->change() in your migrations folder. Also: unsignedDecimal(), unsignedDouble() and unsignedFloat() were removed, and the double() and float() signatures changed.
SQLite
If your app supports SQLite, this step needs extra care. Laravel 11 needs SQLite 3.26+ and alters SQLite tables natively instead of rebuilding them.
What we found in practice: under Laravel 10, those rebuilds had silently dropped foreign keys on SQLite. Laravel 11 keeps them, so old migration steps that "drop the old key" suddenly matter. If they named the key in a way SQLite can't resolve, you end up with duplicate, conflicting foreign keys. Drop keys by column so the name resolves on every database:
$table->dropForeign(['user_id']);
Then compare a fresh schema dump with the one you saved, on every driver.
Smaller changes in 11
- Password rehashing. Passwords are rehashed on login if the hashing work factor changed. Usually invisible. If another system shares your users table, set
'rehash_on_login' => falseinconfig/hashing.php. CustomUserProviderclasses need a newrehashPasswordIfRequired()method. - Carbon 3.
diffInDays()and the otherdiffIn*methods return floats and can be negative. Cast where you store or compare them. - Rate limiting is now per second. Check custom rate limiter code.
- Filesystem disks need a
driverkey. One app we upgraded crashed on boot because a disk inconfig/filesystems.phpdidn't have one.
Step 2: Laravel 11 → 12
Laravel 12 is mostly a maintenance release. Bump laravel/framework to ^12.0, then check:
- UUIDs.
HasUuidsnow generates ordered UUIDv7. If you relied on random v4 IDs (for example, as hard-to-guess public IDs), useHasVersion4Uuidsinstead. - Local disk. If you never published
config/filesystems.php, thelocaldisk's root moves tostorage/app/private. - SVG uploads. The
imagevalidation rule no longer accepts SVG unless you writeimage:allow_svg. - Carbon 2 is no longer supported.
- Packages. Check every package's own release notes here too. One app we upgraded used a Turnstile package whose new major version removed a Blade directive the views relied on. Pinning the previous major kept it working.
Step 3: Laravel 12 → 13
Bump laravel/framework to ^13.0, laravel/tinker to ^3.0, phpunit/phpunit to ^12.0 and pestphp/pest to ^4.0. Most changes are low-impact. These are the ones that bite:
- Everyone gets logged out, unless you prevent it. Laravel 13 changed the format of the default cache prefix, Redis prefix and session cookie name. If your config relies on the framework defaults, deploying empties the cache and signs every user out. Set
CACHE_PREFIX,REDIS_PREFIXandSESSION_COOKIEto your current values before deploying. - Cached objects. The new default cache config sets
serializable_classestofalse. If you copy it in and you cache PHP objects, list the classes allowed to be unserialized. - Session format. The new skeleton uses
jsonsession serialization. Switching an existing app fromphptojsonlogs everyone out. Keepphpunless you're happy with that. array_first()andarray_last(). Laravel 13 pulls in a PHP 8.5 polyfill that defines these global functions. If your app or a package likelaravel/helpersused the old versions that take a callback, they now silently return the first element instead of the first match. Search for them and switch toArr::first()andArr::last().- CSRF middleware renamed.
VerifyCsrfTokenis nowPreventRequestForgeryand also checks theSec-Fetch-Siteheader. Update references in tests (withoutMiddleware(...)) and route files, and test any cross-site form posts or embedded pages. - MySQL queries that now throw.
upsert()with an emptyuniqueBythrows an exception. A joinedDELETEwithORDER BYorLIMITnow throws on MySQL instead of quietly ignoring the limit.
Problems your tests won't catch
In every Laravel upgrade we ran, the tests going green wasn't the end. Before you ship:
- Run commands tests don't cover:
migrate:fresh --seed, your install or setup commands, one queued job,schedule:run. - Compare fresh schema dumps with the ones from before the upgrade, on every database you support.
- Load the main pages and submit one form against the upgraded build.
- Search for everything listed above, even where tests pass.
Upgrade checklist
- PHP 8.3+, tests green on the current Laravel
- Schema dumps saved for each supported database
- 10 → 11: dependencies, published package migrations, Doctrine removed,
->change()migrations reviewed, SQLite keys checked - 11 → 12: UUIDs, local disk, SVG validation, package majors
- 12 → 13: cache/Redis/session names pinned, cache and session serialization,
array_first/array_last, CSRF middleware, MySQLupsert/DELETE - Schema dumps compared, commands and pages checked by hand