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

  1. 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.
  2. Baseline the tests. Run the suite and record what passes. Note any tests that fail because they need the network.
  3. Dump your schema. Run php artisan migrate:fresh on an empty database for every driver you support (MySQL, PostgreSQL, SQLite...) and save a schema dump. You'll compare against it later.
  4. 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.2
  • nunomaduro/collision → ^8.1
  • laravel/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' => false in config/hashing.php. Custom UserProvider classes need a new rehashPasswordIfRequired() method.
  • Carbon 3. diffInDays() and the other diffIn* 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 driver key. One app we upgraded crashed on boot because a disk in config/filesystems.php didn't have one.

Step 2: Laravel 11 → 12

Laravel 12 is mostly a maintenance release. Bump laravel/framework to ^12.0, then check:

  • UUIDs. HasUuids now generates ordered UUIDv7. If you relied on random v4 IDs (for example, as hard-to-guess public IDs), use HasVersion4Uuids instead.
  • Local disk. If you never published config/filesystems.php, the local disk's root moves to storage/app/private.
  • SVG uploads. The image validation rule no longer accepts SVG unless you write image: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_PREFIX and SESSION_COOKIE to your current values before deploying.
  • Cached objects. The new default cache config sets serializable_classes to false. If you copy it in and you cache PHP objects, list the classes allowed to be unserialized.
  • Session format. The new skeleton uses json session serialization. Switching an existing app from php to json logs everyone out. Keep php unless you're happy with that.
  • array_first() and array_last(). Laravel 13 pulls in a PHP 8.5 polyfill that defines these global functions. If your app or a package like laravel/helpers used the old versions that take a callback, they now silently return the first element instead of the first match. Search for them and switch to Arr::first() and Arr::last().
  • CSRF middleware renamed. VerifyCsrfToken is now PreventRequestForgery and also checks the Sec-Fetch-Site header. 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 empty uniqueBy throws an exception. A joined DELETE with ORDER BY or LIMIT now 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, MySQL upsert/DELETE
  • Schema dumps compared, commands and pages checked by hand

Sources