Interested in generating passive income? Join our partnership program and receive a commission on each new client referral. Learn more.
12 min read
Interested in generating passive income? Join our partnership program and receive a commission on each new client referral. Learn more.
Upgrading from Laravel 9 to Laravel 11 can be quite a headache, especially when you’re dealing with a large codebase that needs extensive re-testing.
Even though it’s possible to manually go through the documentation and adjust the codebase, this process can be incredibly time-consuming. That’s why we turned to Laravel Shift to help us out. Laravel Shift made our upgrade journey much smoother by automatically adjusting our code for new features, resolving composer conflicts, and aligning dependencies. With just a few clicks, we managed to transition from Laravel 9 to Laravel 11.
In this guide, we'll be sharing the process of upgrading from laravel 9 to 11. We’ll explain how Laravel Shift simplified our work, highlight potential issues during the upgrade process, and detail the new features in Laravel 11.
So, let’s consider this your go-to cheatsheet to make your own upgrade experience faster and easier.
Laravel 11 introduced an overall structure upgrade, which has somewhat changed the way Laravel developers think. So, let’s focus on the potential problems you might face after the upgrade.
In EventsServiceProvider, we registered several events. To keep it simple, let’s consider one example: a listener that activated upon user registration to create resources. This listener was manually registered in the service provider. When we began testing, we came across a problem: two resources were being created for each user registration instead of one.
When understanding the issue, we discovered that Laravel’s auto-discovery feature was automatically handling events and listeners, which made manual registration unnecessary. Of course, we could disable this feature, but it seems like a great option. So we removed all listeners, and Laravel took care of auto-discovery. So, it's important to review your listeners during the upgrade.
In previous versions of Laravel, you would typically include the following lines in the EventServiceProvider:
...
use Illuminate\\Foundation\\Support\\Providers\\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
...
However, in Laravel 11, you won't find EventServiceProviderin the providers folder anymore. The Laravel team has tried to reduce clutter as much as possible in the app directory, so Illuminate\\Foundation\\Support\\Providers\\EventServiceProvider is now loaded automatically, and there is no need to have this in the providers folder.
Unfortunately, Laravel Shift did not remove this file or warn us of the issues. The problem was that the automatically registered IlluminateFoundationSupportProvidersEventServiceProvider and our custom provider were running simultaneously.
While this may not always cause issues, it’s unnecessary we have an automated way of handling events, so we try to remove this file and follow L11’s convention.
In Laravel 11, SendEmailVerificationNotification is registered in the vendor’s EventServiceProvider. If you register it elsewhere, users will receive two verification emails instead of one. This issue might be fixed in future releases, as there are indications in the vendor code that check whether an event is already registered.
Default middleware has been moved to the foundation level and is enabled by default.
If you need to modify, for example, TrimStrings or RedirectIfAuthenticated, new helper functions are available to assist with these changes:
TrimStrings::except(['secret']);
RedirectIfAuthenticated::redirectUsing(fn ($request) => route('dashboard'))
Previously, we needed to register our own middleware inside Kernel.php. Now, you should register middleware in bootstrap/app.php instead:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(LogRequestEndpoint::class);
})
If you want to prepend the middleware into the list, instead of appending it, you can run the following:
$middleware->web(prepend: LogRequestEndpoint::class);
//You can also use an array here:
$middleware->web(prepend: [LogRequestEndpoint::class]);
The exception handler middleware has also been removed. Instead, you should use:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->dontFlash();
//$exceptions->reportable..
//$exceptions->renderable..
})
All unchanged Laravel configuration files have been moved to the vendor directory, so they no longer need to be in your project's config directory.
Service providers are not being registered inside app.php anymore. Instead, they moved to bootstrap/providers.php
When you publish a config file and only include the changes you need, these changes will be merged with the original config file. This ensures your modifications overwrite the default settings while leaving the rest unchanged.
When creating a provider from the terminal, using php artisan make:provider it will be automatically registered in bootstrap/providers.php.
By default, api.php is no longer included in the routes folder. To add it, you need to run php artisan install:api. This command will install Sanctum, which is no longer included by default, update Bootstrap, and add api.php to the ->withRouting() function.
Previously, you needed to register EnsureFrontendRequestsAreStateful for API routes. Now, you only need to include these lines in app.php:
-›withMiddleware(function (Middleware $middleware) {
$middleware->statefulApi()
})
The same goes for broadcasting. If you need it to appear inside routes, you need to run php artisan install:broadcasting
This will also publish echo.js inside the resources folder.
Instead of registering scheduled tasks inside Kernel/console.php, you should now register them inside routes/console.php
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
}) -›purpose('Display an inspiring quote')->hourly();
//Using new schedule facade
Schedule::command('model:prune')->daily);
When you install Laravel using Composer, SQLite will be installed by default, and an empty SQLite file will be created automatically. This does not happen if you use the laravel new command or install Laravel using the Sail auto-installer script.
We have a new once() method. This method memorizes what we pass to it for one specific request lifecycle. For example, if we create a class and function inside the class that returns: return once(fn () => Str::uuid()); this will result in the same UUID no matter how many times we will call this function.
Keep in mind that different instances of the same class will cache results separately, so the caching is specific to each instance.
$someInstance = new someClass();
$someAnotherInstance = new someClass();
$someInstance->uuid(); //1e19ed43-0271-4e32-ad85-22db2a0e7f0a
$someInstance->uuid(); //1e19ed43-0271-4e32-ad85-22db2a0e7f0a
//Different instance will cache different, one uuid
$someAnotherInstance->uuid(); //40dd800c-4e49-4a47-8990-96bbb7b85df9
$someAnotherInstance->uuid(); //40dd800c-4e49-4a47-8990-96bbb7b85df9
Dump and dump-and-die methods are now available in chains, for example, for eloquent models.
User::with('notifications')->latest()->limit(5)->dd()->get();
//Or stringable class
str('HeLLo')->append('world')->dump()->apa()->stostring();
The difference between Laravel 10 and Laravel 11 is that now, everything uses one jumpable trait. In L10, the dump method was used differently for different classes. Using IlluminateSupportTraitsDumpable will allow any class to use dd and dump in the chain.
You can also overwrite the dump method for your class, allowing you to display/format data that you wish to see while using it.
In previous versions, if you eager-loaded users along with their posts and wanted to limit each user to, for example, five posts, you would expect:
Route::get('/dashboard', function () {
return view('dashboard', [
'users' => User::whereHas('posts')
->with('posts')
-›paginate(5),
]) :
});
This approach will load all posts for the 5 users you’ve paginated. However, if you want to retrieve 5 users along with only their latest 5 posts, you might think to:
->with(['posts' => fn ($query) => $query->latest()->limit(5)])
But if you try it, it will return 5 posts for only the first user. All 4 users will not have posts at all. For this to work, you needed to install a separate package. But in L11, you will get the expected results here.
You can’t directly use $casts = ["something" => AsCollection::using(SettingsCollectioniclass)] as a class property. It is simply not allowed to do. However, if you need to implement similar functionality for model casts, Laravel 11 introduces a way to handle this:
protected function casts(): array
{
return [
//...
];
}
If you use this function, now you are allowed to use the provided syntax.
In Laravel 11, you can now limit the number of requests per second. For instance, you can set a limit where a user can make up to 1,000 requests per minute but only 20 requests per second, like this:
return [
Limit::perSecond(20)->by(auth()->id(),
Limit::perMinute(1000)->by(auth()->id()),
];
Previously, Http::pool did not support retry functionality. Now, you can use it with retry options, such as:
$conversionRates = Http::pool(fn (Pool $pool) => [
$pool->as('something1')->retry(3)->get("<http://web.local/api/conversion/something1>"),
$pool->as('something2')->retry(3)->get("<http://web.local/api/conversion/something2>"),
$pool->as('something3')->retry(3)->get("<http://web.local/api/conversion/something3>"),
]) ;
This will make sure requests are retried three times if the first request fails.
There is a new env variable that makes encryption key rotation easy. This is APP_PREVIOUS_KEYS . It may contain one or many (comma delimited) previous keys. If new encryption key fails to decrypt for example session, it will try to decrypt with APP_PREVIOUS_KEYS . As soon as decryption is successful, it will update encrypted content (session).
This will not be automatic for manual columns, like if you encrypted the passport id number of all users, you will need to manually iterate through them all and run save() for each user.
In previous versions of Laravel, it was hard to assert the status of queues—whether they had failed, been deleted, or were released. Laravel 11 simplifies this process, making it easier to manage and verify queue statuses.
it('releases the job onto the queue if the video file is not available, function () {
$job = (new UploadVideo('/my-missing-video.mp4'))->withFakeQueueInteractions();
$job->handle();
$job->assertReleased(now()->addMinute());
});
Note withFakeQueueInteractions. This is what adds test assertions like assertFailed, assertDeleted and assertReleased . You can now assert job results way more easily.
Laravel 11 now automatically detects your testing framework (Pest or PHPUnit) and uses the appropriate stub when creating a test with the Artisan command.
We are a 200+ people agency and provide product design, software development, and creative growth marketing services to companies ranging from fresh startups to established enterprises. Our work has earned us 100+ international awards, partnerships with Laravel, Vue, Meta, and Google, and the title of Georgia’s agency of the year in 2019 and 2021.