The go-to resource for learning PHP, Laravel, Symfony, and your dependencies.

Upgrading from Laravel 8 to Laravel 11: A Comprehensive Guide


In 1825, the Erie Canal opened in New York, connecting the Hudson River to the Great Lakes. Before its construction, shipping goods from the Midwest to the East Coast required an overland journey of weeks—or a risky voyage around South America. The canal didn’t invent transportation, of course; it reimagined how existing infrastructure could be rearranged, deepened, and standardized to support new demands.

Framework upgrades, you’ll find, follow a similar pattern. We’re not discarding everything we’ve built—we’re reconfiguring the pathways our code travels, adapting to new standards, and preparing for future expansion. Upgrading from Laravel 8 to Laravel 11, spanning three major versions, requires us to understand what changed, why it changed, and how to navigate the transition without derailing our application’s stability.

In this guide, we’ll walk through each version upgrade systematically. We’ll examine the breaking changes, explore the philosophical shifts in Laravel’s architecture, and provide concrete strategies—both manual and automated—for getting your application to Laravel 11.

Why This Upgrade Matters

Before we dive into the mechanics, let’s establish why upgrading is worth the effort you’ll invest. Laravel 8, released in September 2020, entered end-of-life in January 2023. That means no security patches, no bug fixes, and no modern features. Staying on an unsupported version, in our experience, isn’t just missing out on improvements—it’s an accumulating technical debt that becomes harder to address over time.

Laravel 11, released in March 2024, represents a significant architectural shift. The framework has streamlined its application structure, removing configuration files in favor of convention-based setup, and introduced built-in real-time capabilities through Laravel Reverb. Performance improvements in the router, query builder, and cache layer provide measurable speed gains. But perhaps most importantly, Laravel 11 sets the foundation for future development with a leaner skeleton that’s easier to maintain.

Let’s examine what you’ll gain:

  • Security updates: Laravel 11 receives security patches through September 2026 (LTS), giving you three years of protected development
  • Modern PHP features: Support for PHP 8.2+ brings typed properties, readonly classes, and new Randomizer APIs
  • Simplified structure: The removal of many configuration files reduces boilerplate and cognitive load
  • Better developer experience: Features like Improved Route List and health routing streamline daily workflows

These benefits, of course, come with migration effort. We need to evaluate whether the short-term disruption justifies the long-term gains—a trade-off that depends on your application’s size, your team’s capacity, and your business requirements. We’ll help you make that assessment as we proceed.

The Sequential Reality: Why We Go Through 9 and 10

You might wonder: why can’t we jump directly from Laravel 8 to 11? The answer, though it may seem inconvenient, is that Laravel’s upgrade path—like many frameworks—requires passing through each major version because breaking changes accumulate. Skipping versions would mean confronting dozens—possibly hundreds—of behavioral shifts, removed methods, and refactoring changes all at once. That approach is effectively a rewrite, not an upgrade.

Laravel provides official upgrade guides for each version, and they assume you’re coming from the previous release. Deprecations removed in Laravel 9 won’t be present in Laravel 10 or 11, so if we skip Laravel 9, we lose the migration path that explains how to replace those features. The sequential approach, therefore, lets us validate our application at each stage, catching regressions before they compound.

Tip: The Laravel documentation team maintains version-specific upgrade guides at https://laravel.com/docs/X.x/upgrade, where X.x is the target version. Bookmark these—they’re your primary reference.

Before we proceed, let’s establish some guardrails that will protect your upgrade process:

  1. Ensure you have comprehensive tests. If your test suite covers less than 50% of your codebase, consider expanding it before upgrading. Tests catch regressions that manual checks miss—and you’ll want that safety net.
  2. Work on a dedicated Git branch. Never upgrade directly on your main branch. Create a branch like upgrade/laravel-8-to-11 so you can iterate safely and roll back if needed.
  3. Back up your database. Some upgrades involve schema changes (especially when package versions shift). A database backup protects against data loss.
  4. Document your current state. Run php artisan --version, php -v, and composer -v now. Record your current package versions from composer.lock. This documentation helps if you need to compare behavior later.

With those preparations in place, we’re ready to begin.

Step 1: Laravel 8 → Laravel 9

Laravel 9, released in February 2022, was an LTS (Long Term Support) release with bug fixes and security updates through September 2025. It introduced the first wave of architectural changes we’ll need to address. Before we dive in, though, let’s verify your environment meets the prerequisites.

Prerequisites

Before touching any code, verify your environment meets these requirements:

  • PHP 8.0.2 or higher (Laravel 8 required PHP 7.3+, so this is a step up)
  • Composer 2.2 or higher
  • OpenSSL extension with PHP (for encrypted cookies)

You can check your PHP version with:

$ php -v
PHP 8.1.2 (cli) (built: Feb 17 2023 16:57:09)
...

If you’re on PHP 7.x, you’ll need to upgrade PHP first—this upgrade cannot be done in isolation. The PHP requirement alone is often the biggest blocker for legacy Laravel applications.

Updating Dependencies

The first mechanical step is updating your composer.json. Open that file and locate the laravel/framework entry. Change it from:

"laravel/framework": "^8.0",

to:

"laravel/framework": "^9.0",

Now, we need to address packages that ship with Laravel or have tight coupling to its internals. The most common ones are:

  • laravel/sanctum^2.14 (for Laravel 9)
  • laravel/tinker^2.7
  • spatie/laravel-ignition^1.0 (or switch to laravel/ignition which replaced it)
  • nunomaduro/collision^6.1
  • fruitcake/laravel-cors^2.0 (if used)

You might wonder: why so many packages need version bumps? Laravel’s internal contracts (interfaces) change between versions; packages that implement those contracts must update to remain compatible. That’s why we need to align these package versions before running Composer.

Once your composer.json reflects these changes, run:

$ composer update

Expect this to take a few minutes as Composer resolves dependencies and downloads packages. You also may notice some packages flagged as abandoned—we’ll address those as we go.

What Changed? Key Breaking Changes

Laravel 9 brought several notable changes that require code modifications. We’ll examine the most common ones.

Switch to Symfony Mailer

Laravel 9 replaced Swift Mailer with Symfony Mailer. If your application sends email, this change affects you directly. The app/Mail directory structure remains the same—your Mailable classes will work without modification—but your config/mail.php configuration now references symfony/mailer drivers instead of smtp through Swift.

Why did Laravel make this change? Swift Mailer was no longer actively maintained, while Symfony Mailer offers better long-term support and modern features. For most applications, the migration is seamless because the framework handles the abstraction. Let’s look at what changed:

Before (Laravel 8):

// config/mail.php
'default' => env('MAIL_MAILER', 'smtp'),
'driver' => env('MAIL_DRIVER', 'smtp'),

After (Laravel 9):

// config/mail.php
'default' => env('MAIL_MAILER', 'smtp'),
'mailers' => [
    'smtp' => [
        'transport' => 'smtp',
        // ...
    ],
],

Most applications will work without changes if they rely on the default configuration—the framework adapts automatically. However, if you’ve written custom mailer code that interfaces directly with Swift Mailer classes, you’ll need to refactor those implementations to use Symfony Mailer’s API. That’s a relatively rare scenario, but worth checking if you have complex mailer integrations.

Lang Directory Relocation

Laravel 9 moved the language files from resources/lang to the project root as simply lang. This change aligns with Laravel’s trend of reducing the resources directory’s scope.

# Before
resources/
├── lang/
   ├── en/
   ├── auth.php
   └── pagination.php

# After
lang/
├── en/
   ├── auth.php
   └── pagination.php

If you’ve customized language lines, move your resources/lang directory to lang/. Laravel will continue to use your custom files without modification.

Flysystem 3.x Upgrade

Laravel 9 upgraded from Flysystem 1.x to 3.x. The Filesystem abstraction API changed significantly. If you’ve implemented custom filesystem drivers—common for S3 multi-bucket setups or specialized cloud storage—you’ll need to update those classes to match the new interface.

Flysystem 3.x focuses on improved type safety and consistency across adapters. The most noticeable change is the removal of readStream and writeStream methods in favor of read and write with different return types. Let’s see what changed:

Here’s what a typical custom adapter looked like in version 1:

// Laravel 8 - Flysystem 1.x style
public function readStream($path)
{
    $stream = fopen($this->path($path), 'r+');
    return $stream;
}

In version 3, the method signature becomes:

// Laravel 9+ - Flysystem 3.x style
public function read($path)
{
    $handle = fopen($this->path($path), 'r+');
    return $handle;
}

You’ll notice the method name changed from readStream to read, and there’s no streaming wrapper anymore—the returned resource handle serves the same purpose. The migration guide covers all adapter method changes in detail; if you have custom adapters, you’ll want to review that documentation carefully. If you don’t use custom adapters, this upgrade passes silently with no action needed on your part.

Method Rename: assertDeletedassertModelMissing

Laravel 9 removed the assertDeleted method from database test assertions. It’s been replaced with assertModelMissing for better semantic clarity.

Before:

$user = User::factory()->create();
$user->delete();
$this->assertDatabaseMissing('users', ['id' => $user->id]);
// OR
$this->assertDeleted($user);

After:

$user = User::factory()->create();
$user->delete();
$this->assertModelMissing($user);

The underlying behavior is identical; only the method name changed.

Verifying the Upgrade

After running composer update, run your test suite:

$ composer test
# or
$ php artisan test

If tests pass, run a quick manual sanity check on a development environment:

$ php artisan serve
# Visit http://localhost:8000

Check that your application loads, authentication works, and critical paths execute. Don’t skip this step—automated tests can miss integration issues that surface in a real request cycle.

Warning: The official Laravel 9 upgrade guide includes a section on “Upgrade Compatibility Packages” for specific frameworks like Laravel Echo, Horizon, and Scout. If you use these, review those notes carefully.

Once everything checks out, commit your changes. You’ve successfully reached Laravel 9.

Step 2: Laravel 9 → Laravel 10

Laravel 10, released in February 2023, requires PHP 8.1 and drops support for older PHP versions. It also introduced new first-party packages and removed long-deprecated functionality.

Prerequisites

PHP 8.1.0 or higher is required. Check your version:

$ php -v
PHP 8.1.2 (cli) (built: Feb 17 2023 16:57:09)

If you’re still on PHP 8.0.x, you’ll need to upgrade PHP before proceeding. On Ubuntu/Debian systems with Ondřej Surý’s PPA:

$ sudo add-apt-repository ppa:ondrej/php
$ sudo apt-get update
$ sudo apt-get install php8.1 php8.1-cli php8.1-common php8.1-mbstring php8.1-xml php8.1-curl php8.1-mysql

On macOS with Homebrew:

$ brew install php@8.1
$ brew link --overwrite php@8.1

Updating Dependencies

In composer.json:

  • Change laravel/framework from ^9.0 to ^10.0
  • Change php constraint from ^7.3|^8.0 to ^8.1
  • Update Laravel packages to their 10.x compatible versions:
"require": {
    "php": "^8.1",
    "laravel/framework": "^10.0",
    "laravel/sanctum": "^3.2",
    "laravel/tinker": "^2.8",
    // other packages...
}

Run the update:

$ composer update

Composer will update the framework and all compatible package versions. You might see some packages flagged as incompatible—those need either an update to a newer release or replacement with alternatives.

Breaking Changes in Laravel 10

Removal of dispatchNow()

Laravel 10 removed the dispatchNow() method from the Dispatchable trait. This method was used to execute jobs synchronously without queuing.

Before (Laravel 9):

dispatch_now(new ProcessPodcast($podcast));

After (Laravel 10+):

dispatch_sync(new ProcessPodcast($podcast));

The behavior is identical—dispatchSync() is the new preferred method.

Deprecated Route Methods Removed

Several route helper methods removed in Laravel 10 include:

  • Route::home() → use Route::get('/', ...) with named route 'home'
  • Route::resource() with ['except' => ['show']] options remain valid, but some parameter naming conventions changed

If your routes file uses Route::home(), update it:

// Before
Route::home('/welcome', 'HomeController@index')->name('home');

// After
Route::get('/', 'HomeController@index')->name('home');

Laravel Pennant Introduced

Laravel 10 introduced Laravel Pennant, a first-party feature flagging package. If you use third-party feature flag solutions like spatie/laravel-feature-flags, you might consider migrating—though this isn’t required. Pennant provides database, Redis, and file storage drivers out of the box.

use Laravel\Pennant\Feature;

if (Feature::active('new-dashboard')) {
    return redirect('/new-dashboard');
}

Pennant is opt-in; your existing application will continue working without it.

Process Layer

Laravel 10’s new Process layer provides an expressive API for running shell commands. Previously, developers used proc_open(), Symfony Process, or backticks. Laravel’s wrapper integrates with the service container and provides testing helpers.

use Illuminate\Support\Process\Process;

$process = Process::start('ls -la');
$output = $process->output();
// or await completion
$process = Process::run('ls -la');
if ($process->successful()) {
    echo $process->output();
}

This is a new feature, not a breaking change—but it’s worth knowing as it replaces many ad-hoc process implementations.

Verifying the Upgrade

The upgrade verification process mirrors Step 1: run your full test suite, then perform manual checks. Pay attention to:

  • Queue behavior: Are jobs still dispatched correctly? Check that dispatch_now() replacements work.
  • API responses: Some response structures changed slightly with new middleware handling.
  • Database connections: PHP 8.1 introduced MySQL 8.0+ driver requirements; verify pdo_mysql extension matches.

Commit your changes once verified. You’re now on Laravel 10.

Step 3: Laravel 10 → Laravel 11

Laravel 11, released March 2024, represents the most significant structural change in recent history. The application skeleton has been streamlined dramatically, configuration files consolidated, and service provider registration automated. This step requires the most manual intervention.

Prerequisites

PHP 8.2.0 or higher:

$ php -v
PHP 8.2.4 (cli) (built: Apr  2 2024 15:37:23)

If you’re on PHP 8.1, upgrade to 8.2 before proceeding. PHP 8.2 brings readonly classes, Randomizer class, and true type—all of which Laravel 11’s codebase uses.

Updating Dependencies

In composer.json:

"require": {
    "php": "^8.2",
    "laravel/framework": "^11.0",
    // adjust other packages to 11.x compatible versions
}

Important packages to check:

  • laravel/sanctum^4.0
  • laravel/tinker^2.9
  • spatie/laravel-ignition → may need removal; Laravel 11 uses laravel/ignition or framework’s error page
  • fruitcake/laravel-cors → replaced by built-in \Illuminate\Http\Middleware\HandleCors

Run:

$ composer update

Structural Changes in Laravel 11

Laravel 11’s skeleton differs substantially from Laravel 10. Let’s examine what’s changed and how to migrate each piece.

No More app/Http/Kernel.php

The HTTP kernel has been eliminated. Route middleware configuration now lives in bootstrap/app.php. Here’s what your old app/Http/Kernel.php likely contained:

// Laravel 10 - app/Http/Kernel.php
protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    // ...
];

In Laravel 11, you define these in bootstrap/app.php after creating the application:

// Laravel 11 - bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->web([
            \App\Http\Middleware\EncryptCookies::class,
            // ...
        ]);

        $middleware->route([
            'auth' => \App\Http\Middleware\Authenticate::class,
            'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
            // ...
        ]);
    })
    ->create();

What to do: Review your existing app/Http/Kernel.php for registered middleware (both global and route-specific). Transfer each middleware class to the appropriate array in the withMiddleware callback. Note that some Laravel-provided middleware classes have changed namespaces; consult the upgrade guide for the complete list.

Service Providers Are Mostly Automatic

Laravel 11 automatically discovers and registers many service providers that previously required manual registration in config/app.php. Your custom providers, however, still need registration—but in a different location.

Before (Laravel 10, config/app.php):

'providers' => [
    /*
     * Package Service Providers...
     */
    Laravel\Sanctum\SanctumServiceProvider::class,
    Spatie\Ignition\IgnitionServiceProvider::class,

    /*
     * Application Service Providers...
     */
    App\Providers\AppServiceProvider::class,
    App\Providers\AuthServiceProvider::class,
    // ...
],

After (Laravel 11, bootstrap/app.php):

->withProviders([
    // Third-party packages typically auto-discover; list if needed
    Laravel\Sanctum\SanctumServiceProvider::class,

    // Your custom providers
    App\Providers\AppServiceProvider::class,
    App\Providers\AuthServiceProvider::class,
])

Many providers are automatically discovered via Composer’s extra.laravel.providers configuration. Check your composer.json for package extra sections; those packages will register themselves.

What to do: Audit your config/app.php providers array. For each provider:

  • If it’s a Laravel framework component (e.g., Illuminate\Auth\AuthServiceProvider), remove it—Laravel 11 loads these automatically.
  • If it’s a third-party package, check its composer.json for extra.laravel.providers. If present, you can omit it from your list.
  • If it’s your own App\Providers\* class, move it to the withProviders array in bootstrap/app.php.

Configuration File Changes

Laravel 11 removed many configuration files from the config/ directory. The removed files include:

  • config/app.php (replaced by framework defaults and environment variables)
  • config/auth.php (simplified, with guard/driver defaults in .env)
  • config/broadcasting.php
  • config/cache.php
  • config/database.php (partially retained, but many defaults now from environment)
  • config/filesystems.php
  • config/mail.php
  • config/queue.php
  • config/services.php
  • config/session.php
  • config/view.php

These configurations now cascade: framework defaults → .env variables → config/ files (if present). Laravel 11 still respects existing config/ files if you keep them, but the recommended approach is to delete them and configure via environment variables.

For example, your database configuration in Laravel 10 might have looked like:

// config/database.php
'connections' => [
    'mysql' => [
        'driver' => 'mysql',
        'url' => env('DATABASE_URL'),
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '3306'),
        'database' => env('DB_DATABASE', 'laravel'),
        'username' => env('DB_USERNAME', 'root'),
        'password' => env('DB_PASSWORD', ''),
        // ...
    ],
],

In Laravel 11, this file is gone by default. The framework uses sensible defaults and reads DB_* environment variables directly. If you need custom connection settings (like Redis sockets or multiple connections), you can create a new config/database.php—but start with the Laravel 11 skeleton’s version from a fresh install.

What to do: The safest migration path is to:

  1. Create a fresh Laravel 11 project in a temporary directory: laravel new temp-project
  2. Compare its config/ directory (or absence thereof) with yours
  3. Copy over only the configurations you truly need to override
  4. For each configuration, ask: can this be expressed as an environment variable instead?

Default Database: SQLite

Laravel 11’s fresh installations use SQLite for database, cache, and session drivers by default. This doesn’t affect your existing application if you already have a config/database.php file—your database settings remain. But if you deleted configuration files and rely on defaults, verify that your .env file sets:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_password

Without these, Laravel 11 will attempt to use database/database.sqlite—which may not exist on your system.

Health Routing

Laravel 11 includes a health check endpoint at /up automatically when using the default bootstrap/app.php configuration with ->withRouting(health: '/up'). This endpoint returns 200 OK if your database and cache connections are healthy—useful for orchestration systems like Kubernetes.

You can customize the health check or disable it by adjusting the withRouting call:

->withRouting(
    // ...
    health: false, // disable /up endpoint
)

The Upgrade Walkthrough: A Practical Exercise

Let’s simulate the upgrade process on a sample Laravel 10 application. We’ll show actual commands and expected outputs to illustrate the workflow.

Scenario: A Laravel 10 application named “Acme CRM” with these characteristics:

  • Git repository with main branch protected
  • MySQL database
  • Custom service provider: App\Providers\AcmeServiceProvider
  • Third-party packages: Laravel Sanctum, Laravel Tinker, and a custom package acme/qr-generator

Step A: Prepare the branch

$ git checkout main
$ git pull origin main
$ git checkout -b upgrade/laravel-10-to-11
Switched to a new branch 'upgrade/laravel-10-to-11'

Step B: Update composer.json

We edit the file to require PHP 8.2 and Laravel 11:

{
    "require": {
        "php": "^8.2",
        "laravel/framework": "^11.0",
        "laravel/sanctum": "^4.0",
        "laravel/tinker": "^2.9",
        "acme/qr-generator": "^2.1"
    }
}

Notice we bumped acme/qr-generator to ^2.1 because version 2.0 didn’t support Laravel 11 (we checked the package’s GitHub releases).

Step C: Run composer update

$ composer update
Loading composer repositories with package information
Updating dependencies (including require-dev).....
Package operations: 24 installs, 2 updates, 0 removals
  - Installing laravel/framework (v11.0.0): Extracting archive
  - Upgrading laravel/sanctum (v3.2.1 => v4.0.0): Extracting archive
  - Upgrading laravel/tinker (v2.8.2 => v2.9.0): Extracting archive
  - Installing acme/qr-generator (v2.1.0): Extracting archive
  ...
Writing lock file
Generating autoload files

Composer completed successfully. But we notice a warning:

Package spatie/laravel-ignition is abandoned, use laravel/ignition instead.

Our composer.json still references the old package. We’ll remove it in a moment.

Step D: Migrate configuration

We create a fresh Laravel 11 project to reference:

$ cd /tmp
$ composer create-project laravel/laravel laravel11-skeleton
$ ls -la laravel11-skeleton/
# Notice no config/ directory with many files
$ ls -la laravel11-skeleton/bootstrap/app.php
-rw-r--r--  1 user  staff  2348 Mar 10 2024 laravel11-skeleton/bootstrap/app.php

We open that app.php file and see the new structure:

<?php

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withExceptions(function (Exceptions $exceptions) {
        // Custom error page configuration...
    })
    ->withProviders([
        // Additional providers if needed
    ])
    ->withMiddleware(function (Middleware $middleware) {
        // Middleware configuration...
    })
    ->create();

Now we migrate our Acme CRM application:

  1. Remove abandoned package from composer.json:

    // delete this line
    "spatie/laravel-ignition": "^1.0",
  2. Delete old config files we no longer need:

    $ rm -f config/app.php config/auth.php config/cache.php
    $ rm -f config/database.php config/filesystems.php config/mail.php
    $ rm -f config/queue.php config/services.php config/session.php
    $ rm -f config/view.php

    We keep custom configurations if we overrode them—for our app, we had a custom config/cache.php for Redis, so we’ll keep that but restructure it later.

  3. Move service providers: Our app/Providers directory contains:

    • AppServiceProvider.php
    • AuthServiceProvider.php
    • AcmeServiceProvider.php (custom)

    We modify bootstrap/app.php:

    ->withProviders([
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        App\Providers\AcmeServiceProvider::class,
    ])

    We also deleted app/Http/Kernel.php and moved its middleware configuration into the withMiddleware callback.

  4. Update .env: Add explicit database configuration since we removed config/database.php:

    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=acme_crm
    DB_USERNAME=acme_user
    DB_PASSWORD=secret_password

    For Redis:

    
    REDIS_HOST=127.0.0.1
    REDIS_PASSWORD=null
    REDIS_PORT=6379
  5. Check for removed facade aliases: In Laravel 10, we might have used facades without importing them if we relied on class aliases from config/app.php:

    // Laravel 10 - might work with aliases
    Cache::put('key', 'value', 3600);

    In Laravel 11, you should import facades explicitly:

    use Illuminate\Support\Facades\Cache;
    // ...
    Cache::put('key', 'value', 3600);

    Run a grep across your codebase to find facade usage without imports:

    $ grep -r "Cache::" app/ --include="*.php" | grep -v "use Illuminate"

    Add imports where missing.

Step E: Test the upgraded application

$ php artisan migrate --force
Migrating: 2023_01_01_000000_create_users_table
Migrated:  2023_01_01_000000_create_users_table (0.15 seconds)
$ php artisan route:cache
Route cache cleared successfully!
Routes cached successfully!
$ php artisan config:cache
Configuration cache cleared successfully!
Configuration cached successfully!

Now start the server:

$ php artisan serve
Server running on http://127.0.0.1:8000

We browse to the application—login works, CRUD operations function. We run our test suite:

$ composer test
   PASS  Tests\Feature\UserTest
   PASS  Tests\Unit\QrGeneratorTest
   ...

   Tests: 48 passed, 2 failed, 0 skipped

   Failed Tests:
   1) Tests\Feature\CacheTest::it_caches_user_profiles
   2) Tests\Unit\MiddlewareTest::throttle_middleware_works

The cache test fails because our Redis configuration wasn’t migrated correctly. We fix that by ensuring our config/cache.php (if we kept it) references environment variables properly:

'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'),
    'options' => [
        'cluster' => env('REDIS_CLUSTER', 'redis'),
        'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'))),
    ],
    'default' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_DB', '0'),
    ],
],

The middleware test fails because we misconfigured the throttle middleware in bootstrap/app.php. We fix that by adding the correct middleware configuration:

$middleware->route([
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
]);

After fixing both issues:

$ composer test
   PASS  Tests\Feature\UserTest
   PASS  Tests\Unit\QrGeneratorTest
   PASS  Tests\Feature\CacheTest
   PASS  Tests\Unit\MiddlewareTest
   ...

   Tests: 50 passed, 0 failed

Success. Commit the changes:

$ git add .
$ git commit -m "Upgrade Laravel 10 to 11"

You’re now running Laravel 11.

Alternative Approach: Laravel Shift

The manual upgrade, while methodical, demands significant time and attention to detail. For teams with limited bandwidth or complex applications, consider Laravel Shift.

Laravel Shift is a paid automated service that upgrades your Laravel application by processing your Git repository and making the necessary code changes. Here’s how it works:

  1. You authorize Shift to access your repository (read-only)
  2. Shift creates a pull request with all the framework version changes
  3. You review, test, and adjust as needed

Shift handles:

  • Composer dependency updates
  • Code refactoring for breaking changes (like dispatchNow()dispatchSync())
  • Configuration file migrations
  • Removal of deprecated features

Prices vary by shift type: Individual Shift for Laravel 8→11 costs $99 as of 2024. Team and business plans include additional support.

The trade-off: Shift accelerates the mechanical parts of the upgrade—but it doesn’t eliminate the need for testing and validation. The generated pull request still requires human review. If your application has unique customizations or unusual patterns, Shift might miss edge cases.

I’ve used Shift on projects ranging from small apps to multi-package ecosystems. My recommendation: if you have a well-tested application following Laravel conventions, Shift can cut upgrade time by 70% or more. If you’ve built many custom abstractions or rely on unmaintained packages, the manual approach forces you to confront architectural decisions that need attention anyway.

Note: The upgrade guide you’re reading now assumes a manual upgrade. If you use Shift, review its output against version-specific upgrade guides—Shift may not catch every nuance.

Common Pitfalls and Troubleshooting

Based on experience upgrading dozens of Laravel applications, here are frequent stumbling blocks:

Configuration Cache Won’t Clear

If you encounter errors like “Class ‘App\Http\Kernel’ not found” after upgrading, you likely have stale caches. Clear all caches explicitly:

$ php artisan optimize:clear
Clearing configuration cache...
Clearing route cache...
Clearing view cache...
Clearing event cache...
Clearing file cache...

Then regenerate:

$ php artisan config:cache
$ php artisan route:cache

Facade Not Found Errors

Laravel 11 tightened autoloading and removed the default class aliases from config/app.php. Facade calls like Route:: or Cache:: now require proper imports:

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    public function index()
    {
        Cache::remember('users', 3600, fn() => User::all());
        return view('users.index');
    }
}

Run this command to find missing imports automatically:

$ grep -rP "(?<!use.*\bfacade\b)(Route|Cache|Log|DB|Storage)::" app/ --include="*.php" | cut -d: -f1 | sort -u

Then add the appropriate use statements.

Third-Party Package Incompatibility

Some packages haven’t been updated for Laravel 11. Check composer why-not laravel/framework ^11.0 to see which packages block the upgrade:

$ composer why-not laravel/framework ^11.0
vendor/package 2.0.0 requires laravel/framework (^8.0|^9.0) -> no matching package found.

You have options:

  1. Find an alternative package that supports Laravel 11
  2. Fork the package and update its composer.json and code yourself
  3. Replace the functionality with in-house code
  4. Stay on Laravel 10 until the package updates (but this delays the upgrade)

Scheduled Tasks Not Running

Laravel 11 changed how the scheduler bootstraps the framework. Your app/Console/Kernel.php remains mostly intact, but the scheduler’s output file paths might need adjustment if you rely on absolute paths. Check that scheduled commands run:

$ php artisan schedule:run

Run this every minute via system cron as usual:

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

Mix or Vite Asset Compilation

If you use Laravel Mix (webpack.mix.js), it still works—but consider migrating to Vite, which Laravel 11 scaffolds by default. The migration involves:

  1. Removing laravel-mix package
  2. Installing vite and @vitejs/plugin-laravel
  3. Updating vite.config.js to match your mix configuration
  4. Changing Blade @mix directives to @vite

This is optional but recommended for better development experience.

Testing Your Upgrade Thoroughly

After completing the mechanical migration, thorough testing is non-negotiable. Don’t rely solely on automated tests—they might miss behavioral changes in edge cases.

Checklist for Manual Testing

Go through these critical paths in a development environment:

  • User registration, login, password reset flows
  • Admin dashboard functionality (if applicable)
  • File uploads and storage (check that files appear in correct disks)
  • Email sending (both transactional and bulk)
  • Queue processing (run php artisan queue:work and verify jobs complete)
  • Scheduled tasks (php artisan schedule:run)
  • API endpoints (with Postman or similar)
  • Payment integrations (if any)
  • Search functionality (especially if using Scout)
  • Caching behavior (Redis/Memcached connections)

Pay special attention to middleware behavior—Laravel 11’s HTTP kernel restructuring might affect custom middleware order or parameters.

Performance Validation

Laravel 11 promises performance improvements. Validate that your application actually benefits:

# Before and after (use Apache Bench or wrk)
$ ab -n 1000 -c 50 http://localhost:8000/

Compare requests per second and response times. If performance regressed, investigate:

  • Are you running in production mode (APP_DEBUG=false)?
  • Is config caching enabled?
  • Are routes cached?

You can also use Laravel Telescope or Clockwork to profile database queries and identify N+1 problems that might have been masked before.

What About Direct Upgrades from 8 to 11?

We’ve emphasized the sequential path—8→9→10→11. Could we automate all three steps into one jump? Technically, yes: if you use Laravel Shift, you can select “Laravel 8 to 11” as a single upgrade. Shift handles the intermediate transitions internally.

The manual approach, however, makes more sense stepwise. The Laravel team designed upgrade guides for adjacent versions; combining them without testing at each stage risks missing version-specific breaking changes. For example, if you upgrade directly from 8 to 11, you might miss that Laravel 9 introduced a deprecation warning that became an error in Laravel 10—and the fix differs between versions.

If you absolutely must skip versions (perhaps your Laravel 8 app is so out-of-date that you want to converge on the latest), allocate extra time for debugging. Expect to resolve issues that would have been easier to isolate in a stepwise approach.

Rolling Back If Needed

Even with careful planning, upgrades sometimes reveal show-stopper issues. Before you begin, ensure you can roll back:

# If the upgrade branch hasn't been merged to main yet
$ git checkout main
$ git reset --hard origin/main   # caution: discards any uncommitted work

# If you've already merged and need to revert
$ git revert <commit-hash-of-upgrade> --no-edit
$ git push origin main

Have a rollback plan documented before you start. The cost of an unsuccessful upgrade isn’t just developer time—it’s potential downtime or data inconsistencies. Work first on a staging environment that mirrors production before touching the live system.

Long-Term Maintenance Considerations

Once you’ve reached Laravel 11, you’re positioned for the next three years of LTS support. But the upgrade process reveals architectural debt that might need addressing:

  • Custom service providers: Are they still necessary, or can their functionality be moved to more appropriate places (e.g., route service providers)?
  • Configuration files: Have you fully embraced environment-variable-based configuration? This simplifies future upgrades
  • Facade usage: Are you overusing facades? Consider dependency injection for testability
  • Package selection: Which packages are well-maintained? Which show signs of abandonment?

The upgrade isn’t just about changing versions—it’s an opportunity to modernize your codebase.

Conclusion

Upgrading from Laravel 8 to 11 is a substantial but manageable undertaking. We’ve walked through each version upgrade, examined breaking changes, and provided concrete migration examples. The sequential nature—8→9→10→11—cannot be skipped without increased risk; treat each step as a checkpoint.

Before we part, let’s summarize the essential workflow:

  1. Prepare: Ensure tests exist, create a Git branch, back up the database
  2. Upgrade Laravel 8→9: Update composer.json, handle Swift→Symfony Mailer, move lang/ directory
  3. Upgrade Laravel 9→10: Update to PHP 8.1, replace dispatchNow(), handle removed features
  4. Upgrade Laravel 10→11: Update to PHP 8.2, migrate to bootstrap/app.php, remove config files, adjust for automatic provider discovery
  5. Test: Run automated tests, then manual critical path testing
  6. Optimize: Clear and recache configs/routes, verify performance

Laravel will continue evolving. By understanding this upgrade process—the philosophy behind the structural changes, the patterns for adapting your code—you’re better prepared for future framework versions. The skills you develop here transfer to other upgrade scenarios, whether in Laravel or in different ecosystems entirely.

Good luck with your upgrade. When in doubt, the Laravel documentation and community forums provide excellent resources. And remember: methodical, tested progress beats rushing through the steps.

Sponsored by Durable Programming

Need help with your PHP application? Durable Programming specializes in maintaining, upgrading, and securing PHP applications.

Hire Durable Programming