Skip to content

Web Development · Backend Frameworks

Laravel 12 to 13 Upgrade Guide: What Breaks in 2026

Laravel 13 needs PHP 8.3 and changes CSRF, cache and session defaults. What breaks when you upgrade from 12, in order of risk, with the fixes.

Abhishek Gupta

Abhishek Gupta

9 min read

A clay bridge spanning a cracked grey pedestal and a smooth pale blue one, with a small orange crane laying the last glowing plank

Sponsored

Share

If your Laravel app is on version 12, plan the move to 13 before February 24, 2027. That is when Laravel 12 stops receiving security fixes, and its bug fixes already ended on August 13, 2026. The framework change itself is small. The official upgrade guide puts it at about 10 minutes. The part that catches teams is the PHP version, because Laravel 13 needs PHP 8.3 or newer.

This guide goes through the upgrade in the order you should do it, flags the changes that can hurt a production app, and skips the ones that only affect people who write custom cache drivers.

Laravel 12 to 13 at a glance

ItemLaravel 12Laravel 13
ReleasedFebruary 24, 2025March 17, 2026
PHP versions8.2 to 8.58.3 to 8.5
Bug fixes untilAugust 13, 2026 (ended)Q3 2027
Security fixes untilFebruary 24, 2027March 17, 2028

Laravel gives each major version 18 months of bug fixes and two years of security fixes. Laravel 11 already fell out of security support on March 12, 2026, so anyone still on 11 has two upgrades to make and less time to make them.

Step 1: Upgrade PHP before touching Laravel

Laravel 12 runs on PHP 8.2 through 8.5. Laravel 13 drops 8.2. So the safe order is to move your runtime to 8.3, 8.4 or 8.5 while still on Laravel 12, deploy that, and only then change the framework.

PHP’s support calendar makes the choice easy. PHP 8.2 is security-only and ends on December 31, 2026. PHP 8.3 is supported until December 31, 2027, and 8.4 is in active support until the end of 2026. PHP 8.5, released in November 2025, has active support through December 2027. For a project you will keep for two or more years, 8.4 or 8.5 saves you another runtime upgrade.

Two things to check in the PHP step:

  • Run composer why-not php 8.3 (or 8.4) to see which dependencies block the new version. Abandoned packages show up here first.
  • Run your test suite under the new PHP with deprecation notices turned on. PHP 8.x releases deprecate things like implicit nullable parameter types, and those warnings are cheap to fix now and annoying to find later.

Doing this as its own deploy matters. If something breaks after a combined PHP and Laravel change, you have two suspects. After a PHP-only deploy, you have one.

Step 2: Update composer.json

The upgrade guide lists the dependency bumps. Edit composer.json so the relevant lines read:

{
  "require": {
    "laravel/framework": "^13.0"
  },
  "require-dev": {
    "laravel/boost": "^2.0",
    "laravel/tinker": "^3.0",
    "phpunit/phpunit": "^12.0",
    "pestphp/pest": "^4.0"
  }
}

Then run composer update. Not every app has Boost, Tinker or Pest installed, so only change the ones you use. If you use PHPUnit 12 or Pest 4 you will meet a test framework upgrade at the same time as a Laravel one, which is another reason to run the suite before you start and note what was already failing.

Also update the Laravel installer if people on your team use it to create new apps: composer global update laravel/installer. Herd users update Herd instead.

The change most likely to break something: CSRF

Laravel 13 renames the CSRF middleware from VerifyCsrfToken to PreventRequestForgery. It also adds origin verification using the Sec-Fetch-Site header, on top of the usual token check. The old class names remain as deprecated aliases, so nothing fails immediately, but the guide marks this as high impact for a reason.

Where it bites is code that references the middleware by name, usually tests and route definitions that exclude it:

// Laravel 12
->withoutMiddleware([VerifyCsrfToken::class]);

// Laravel 13
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;

->withoutMiddleware([PreventRequestForgery::class]);

Search your codebase for VerifyCsrfToken and ValidateCsrfToken, update each reference, and rerun any test that posts a form. The middleware configuration API also gains a preventRequestForgery(...) method if you customize which routes are excluded.

Because origin checking is new behavior, test your webhook receivers and any endpoint that a third party posts to. Those are already excluded from CSRF in most apps, but confirm it. For background on why origin checks and SameSite cookies work together, see our guide to CSRF protection with SameSite cookies and tokens.

Cache: serializable_classes

This is the medium-impact change, and the one that produces a confusing error. The default cache config now includes a serializable_classes option set to false. It hardens cache unserialization so that a leaked APP_KEY is harder to turn into a PHP deserialization gadget chain attack.

The catch: if your app stores PHP objects in the cache, such as an Eloquent model, a DTO or a value object, they will no longer come back as objects. You have two options. List the classes you trust:

// config/cache.php
'serializable_classes' => [
    App\Data\CachedDashboardStats::class,
    App\Support\CachedPricingSnapshot::class,
],

Or stop caching objects and cache arrays instead, which is the better long-term habit anyway. To find the affected code, grep for Cache::remember and Cache::put calls whose closure returns a model or a custom class. If you want a refresher on what belongs in a cache and what doesn’t, our caching strategies guide covers the trade-offs.

There is a related low-impact change. The default cache and Redis key prefixes now use hyphens instead of underscores, built from APP_NAME. This only applies if your config relies on the framework fallback. If you do, cache keys and the session cookie name change after the upgrade, which means a cold cache and, for the cookie, logged-out users. Set CACHE_PREFIX, REDIS_PREFIX and SESSION_COOKIE explicitly in your environment to keep the old values.

Sessions: do not sync the config blindly

The Laravel 13 skeleton sets the session serialization option to json in config/session.php, again to blunt deserialization attacks. The upgrade guide warns that if you copy that value into an existing app, every active session is invalidated, and every user has to log in again.

If your app stores plain data in the session, switching to json is the safer setting and a forced re-login might be acceptable at a quiet hour. If you would rather keep users signed in, leave the value as php for now and change it in a later release when you choose to. Either way, make it a decision and not a side effect of diffing config files.

Database changes worth a test

Two database items are worth a look if you use MySQL or MariaDB.

upsert now validates that uniqueBy is not empty and throws an InvalidArgumentException instead of generating invalid SQL. MySQL ignores the value and uses the table’s primary and unique indexes, but the check still runs. Calls that passed an empty array will start failing.

And DELETE queries with JOIN, ORDER BY and LIMIT now compile in full for MySQL. Previously the ORDER BY and LIMIT could be silently dropped. If your database does not support that syntax (MySQL, and MariaDB before 11.8.1), you will now get a QueryException where you used to get a delete with no limit. That is a better failure than the old one, but you should know about it before a scheduled cleanup job throws in production.

If you are changing database versions in the same window, do that separately. Our guide to zero-downtime database migrations covers how to sequence it.

Low-impact changes to grep for

None of these will affect most apps. Each is a quick search:

If your code usesChange
$event->exceptionOccurred on JobAttemptedNow $event->exception, which is the exception object or null
$connection on the QueueBusy eventRenamed to $connectionName
Container::call with ?Carbon $date = nullReturns null now, not a Carbon instance
Domain routes registered after non-domain routesRoutes with an explicit domain now match first
Custom Str::createUuidsUsing factories in testsReset between tests, so set them in each test
Morph pivot models with custom classesInferred table name is now pluralized, so set $table explicitly
Bootstrap 3 pagination viewspagination::default is now pagination::bootstrap-3
Global array_first() or array_last() from laravel/helpersMay clash with the new Symfony PHP 8.5 polyfill; use Arr::first()

Also, creating a new model instance inside a model’s boot method now throws a LogicException. And a handful of contracts gained methods (Cache\Store::touch, Dispatcher::dispatchAfterResponse and others), which only matters if you maintain a custom cache store, dispatcher or queue driver.

A rollout order that holds up

  1. Make sure the test suite passes on your current setup, and write down any failures that already exist.
  2. Upgrade PHP to 8.3 or newer on Laravel 12 and deploy.
  3. On a branch, bump composer.json, run composer update, and fix the CSRF references first.
  4. Decide on serializable_classes and the session serialization value, and set the cache and session prefixes explicitly.
  5. Run the full suite, then click through login, a form post, a queued job and a scheduled task on a staging copy.
  6. Deploy outside peak hours and watch error rates for a day.

The community-maintained tool Shift automates much of this, and the Laravel team now ships a Boost command for AI-assisted upgrades. Both are fine for the mechanical edits. Neither will tell you whether your session setting should change. That part is still a human decision.

When to hand the upgrade to someone else

A small app with good tests is an afternoon. An app with no test suite, custom cache drivers or years of drift behind it is a different project, and it is the case where hiring a developer who knows modern PHP pays for itself. The same applies if the app is also two major versions behind. For a deeper look at where Laravel fits today, see Laravel in 2026, which was written around Laravel 12 and is a useful baseline for what changed.

If you want a team to do the upgrade, CoderCops’ services include framework upgrades for existing codebases.

What to do this week

Check your PHP version and your composer.json today. If you are on PHP 8.2, schedule the runtime upgrade first, since it has a December 31 deadline of its own. Then book the Laravel step for a quiet week before February, with the CSRF rename, the cache allow-list and the session setting as the three things you review by hand.

Frequently asked questions

Can I skip Laravel 12 and upgrade to 13 directly from 11?
Laravel 11 stopped receiving security fixes on March 12, 2026, so a project on 11 is already unsupported. Going straight to 13 is possible but means working through the 11 to 12 and 12 to 13 upgrade guides back to back. Do them as two separate steps with tests passing in between.
Do I have to move to PHP 8.3 before upgrading Laravel?
Yes. Laravel 13.x requires PHP 8.3 as the minimum. Composer will refuse to install the framework on 8.2. Moving the runtime first lets you deploy and verify it on Laravel 12, which supports PHP 8.2 through 8.5.
Will upgrading to Laravel 13 log my users out?
Not by itself. Sessions are only invalidated if you change the session serialization setting from php to json in config/session.php. If you want sessions to survive the upgrade, leave it as php.
Is Laravel 13 a big upgrade?
The Laravel team calls it a relatively minor upgrade in effort and says most applications can upgrade without changing much code. The new features (AI SDK, JSON:API resources, vector search) are additions, not requirements.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored