What’s New in Laravel 9: An In-Depth Review

ProfilePicture of Guilherme Assemany
Guilherme Assemany
Technical Interviewer
Two characters next to Laravel 9 logo

Laravel is an open-source PHP web application framework that provides a structure and starting point for building both simple and complex web apps.

Developers love it because it’s intuitive, versatile and scalable. Laravel handles essential functions in the background, like app security, permission control, routing and dependency injection. This has led it to become one of the most widely used PHP frameworks today.

The latest version of the framework, Laravel 9, was released in February 2022. That will be the focus of this article, including a look at what changes were made and new features added. Before we dive into what’s new, though, I’ll first share some background on what makes Laravel such a useful framework.

Table Of Contents

Why Use Laravel?

There are many reasons to choose Laravel for your next project, but these are my top three:

  1. Robust yet easy to use: Laravel is simple enough for junior developers to create elegant, functional apps, and also supports senior developers with complex features and robust tools, from unit testing to real-time events. 
  1. Scalable: Laravel has built-in support for distributed caching systems and has a vast ecosystem with well-written and tested solutions. Laravel Vapor, its AWS-powered serverless deployment platform, is just one example of how scaling with Laravel is a breeze.
  1. Developer resources and community: Laravel is an open-source project and the most developer-friendly framework I have used. In addition to that, there are thousands of packages available to use in your app to speed up development and the framework structure makes it easy to create your packages. Laravel also has a friendly and strong community, with developers always willing to help you solve problems and share what they’ve learned. 

Changes and Deprecations of Laravel 8

Before we review Laravel 9’s new features, it’s worth noting some important changes that you’ll need to consider if you’re currently using Laravel 8 for a project. If you’re starting a new project, most of the information in this section won’t affect you. 

PHP Version Requirement

The new version of the framework requires a minimum PHP version of 8.0, while the previous version, Laravel 8, requires a minimum PHP version of 7.3. To update your project to run with Laravel 9, you’ll need to change the PHP version you’re running on the hosting server and check that all third-party packages used on your app satisfy the requirement.

There are also some PHP 8 features that may be affected by upgrading to Laravel 9, most notably of which is named arguments. When working with Laravel, you should know that its backward compatibility guidelines don’t cover named arguments. Essentially, this means you should avoid using named arguments on methods from the Laravel core because the names of the parameters can change without warning. You can read more in Laravel’s release notes

Symfony Mailer

Since SwiftMailer library is no longer maintained, the Laravel team decided to drop it from the framework and implemented the new version called Symfony Mailer. This library is probably well known by all Symfony developers, and pretty easy for any PHP developer to learn. There are no significant changes from a technical perspective, but it’s worth checking the upgrade guide to make sure everything will work fine.

FlySystem 3.x

This dependency is responsible for all interactions with the filesystem. If you haven’t heard of FlySystem, you may recognize it when I tell you that it supercharges the `Storage` facade, which is the class responsible for handling all filesystem interactions in a Laravel app.

If you’re migrating to Laravel 9, there’s a good chance that this change will impact your app. While the Laravel team made this transition as seamless as possible, it’s still a good idea to review the upgrade guide.

The last version of Flysystem doesn’t change the concept of the package, but it improves the developer experience significantly. Because it’s a complete rewrite, it makes the code better and easier to maintain.

The Server.php File

This file is only used to run `PHP artisan serve` command. In the new version, it was moved to the core of the framework, meaning you can delete this file from the root of your project to make it cleaner.

New Features in Laravel 9

As I said, the changes described above are much more critical for people migrating from Laravel 8 to 9 than to newcomers or projects that started in the new version. Let’s take a look at some cool features that you can use on the latest version of the framework next. It’s worth noting that major version updates are released every year around February, although the Laravel team may deploy minor changes and patch releases every week. Laravel 9 will receive bug fixes until August 2023 and security fixes until February 2024.


Enum Attribute Casting

Since PHP 8.1, the language has built-in support for Enums. Starting from Laravel 9, you can use Eloquent to cast attribute values to PHP enums. You only need to use the $casts property array and specify the attribute and Enum class. 

For example:

1// App\Enums\BloodGroup
2enum BloodGroup: string
4 case A = 'a';
5 case B = 'b';
6 case O = 'o';
7 case AB = 'ab';
10// App\Models\BloodGroup
11use App\Enums\BloodGroup;
14 * The attributes that should be cast.
15 *
16 * @var array
17 */
18protected $casts = [
19 'blood_group' => BloodGroup::class,

And now Eloquent should understand how to cast from and to the Enum class.

1// from
2if ($person->blood_group == BloodGroup::A) {
3 $person->someMethod();
6// to
7 $person->blood_group = BloodGroup::B;
8 $person->save();

Implicit Route Bindings with Enums

You can also type-hint an Enum in your route definition and the framework will only invoke the route if that route segment is a valid Enum value.

1Route::get('/blood_groups/{blood_group}', function (BloodGroup $blood_group) {
2 return $blood_group->value;

Improved Eloquent Accessors/Mutators

The latest version of Laravel offers a new way to define Eloquent accessors and mutators. Prior to this version, there was only one way to do that, and it required the PHP developer to define prefixed methods on the model, like this example:

1public function getBloodGroupAttribute($value)
3 return strtoupper($value);
6public function setNameAttribute($value)
8 $this->attributes['blood_group'] = $value;

The new way to define an accessor and mutator requires only a single, non-prefixed method by type-hinting a return type of Illuminate\Database\Eloquent\Casts\Attribute.

1use Illuminate\Database\Eloquent\Casts\Attribute;
3public function blood_group(): Attribute
5 return new Attribute(
6 get: fn ($value) => strtoupper($value),
7 set: fn ($value) => $value,
8 );


Controller Route Groups

This new feature allows you to define a common controller for all routes inside a specific group. Using this is particularly helpful when you have an extensive routes file, and cleaning it up could help to make it easier to understand:

1use App\Http\Controllers\BloodGroupController;
3Route::controller(BloodGroupController::class)->group(function () {
4 // Calls BloodGroupController::show method
5 Route::get('/blood_group/{id}', 'show');
6 // Calls BloodGroupController::store method
7 Route::post('/blood_group', 'store');

Forced Scoping of Route Bindings

Consider the following scenario:

1use App\Models\User;
2use App\Models\Donations;
4Route::get('/users/{user}/donations/{donation}', function (User $user, Donation $donation) {
5 return $donation;

Usually, you’d expect the donation object to be automatically scoped, considering the user, meaning that the parameter {donation} is always related to the same user from the URL. But that’s not the case.

Let’s imagine we have two users and two donations on our database. 

  • User 1 owns Donation 1
  • User 2 owns Donation 2 

If you make a GET request for this route (mixing user and donation):

1HTTP GET /users/1/donations/2

Sure enough, the route will still return information from Donation 2. 

It happens because Laravel doesn’t automatically scope the models from route binding. In the previous version of Laravel, it was possible to create this scope using a custom keyed implicit binding for the nested route parameter.

1// Laravel 8 example
2use App\Models\User;
3use App\Models\Donations;
5Route::get('/users/{user}/donations/{donation:uuid}', function (User $user, Donation $donation) {
6 return $donation;

This behavior isn’t evident enough for anyone to understand what’s going on under the hood. To fix it, Laravel 9 includes an explicit `->scopeBindings()` method to instruct the framework to scope “child” bindings even without a custom key.

1// Laravel 9 example
2use App\Models\User;
3use App\Models\Donations;
5Route::get('/users/{user}/donations/{donation}', function (User $user, Donation $donation) {
6 return $donation;

Tip: You can use the same approach for route groups.

Anonymous Migration Classes

Anonymous migrations are now a default behavior when you create a new migration file. It’s a simple change, but it helps avoid name collisions between migration classes.

It won’t affect how you code the migrations, and you can use this along with other non-anonymous migration classes.

Here’s an example of what it looks like now:

3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
7return new class extends Migration
9 /**
10 * Run the migrations.
11 *
12 * @return void
13 */
14 public function up()
15 {
16 Schema::table('blood_groups', function (Blueprint $table) {
17 $table->string('active')->default(1);
18 });
19 }
21 /**
22 * Reverse the migrations.
23 *
24 * @return void
25 */
26 public function down()
27 {
28 Schema::table('blood_groups', function (Blueprint $table) {
29 $table->dropColumn('active');
30 });
31 }


Inline Blade Templates

While the use cases for this feature aren’t very common, it does come in handy in key instances. For instance, if you need to turn a raw Blade template string into valid HTML, you can through the render method on the Blade facade. This method accepts two parameters, the template string, and an optional array to provide data to the template.

1use Illuminate\Support\Facades\Blade;
3return Blade::render('New blog article, {{ $title }}', ['title' => 'What is new in Laravel 9']);

Slot Name Shortcut

Another simple but effective change is naming a slot on a blade component, which you can do using a shorter, more convenient syntax than before:

1<!-- Before -->
2<x-slot name="version">
3 Laravel 8.x
6<!-- After -->
8 Laravel 9.x

Checked / Selected Directives

There are two new directives to use on a blade file: @checked and @selected. These directives can be used to quickly indicate the state of a given HTML checkbox/select input.

Both will change the status of the HTML input by setting “checked”/”selected” on them if the provided condition evaluates to true.

1{{-- Checkbox --}}
2<input type="checkbox"
3 name="is_version_9"
4 value="is_version_9"
5 @checked(old('is_version_9', $laravel->is_version_9)) />
7{{-- Select --}}
8<select name="version">
9 @foreach ($laravel->versions as $version)
10 <option value="{{ $version }}" @selected(old('version') == $version)>
11 {{ $version }}
12 </option>
13 @endforeach

Pagination Views with Bootstrap Five Support

Laravel adopted Tailwind as the default CSS framework for some of the required views and packages, but it’s up to you if you want to use it or not. 

To use Bootstrap 5 with the Laravel pagination directive, you should first change the default on the AppServiceProvider class by calling the Paginator’s useBootstrapFive method.

1use Illuminate\Pagination\Paginator;
3public function boot()
5 Paginator::useBootstrapFive();

Doing this will change the HTML structure returned from `->links()` method to be compatible with Bootstrap Five, instead of Tailwind.

Improved Validation of Array with Nested Data

The new `Rule::forEach` method makes it easy to validate nested array values. It accepts a closure that gets invoked for each iteration of the array attribute under validation. That closure should return an array of rules to use for the array element. 

1use Illuminate\Support\Facades\Validator;
2use Illuminate\Validation\Rule;
4//Each blood group element, will get the ID attributed validate against the rules specified
5$validator = Validator::make($request->all(), [
6 'blood_groups.*.id' => Rule::forEach(function ($value, $attribute) {
7 return [
8 Rule::exists(BloodGroup::class, 'id'),
9 'is_numeric',
10 'required'
11 ];
12 }),

Improved Exception Page

Debugging errors with Laravel is an easy task, and the new version has made it even easier. Laravel uses Ignition, an open-source package by Spatie.

Screenshot of Ignition package with light and dark customizable "open in editor" functionality for Laravel 9.

Image Source: Laravel Release Notes

The latest version of the Ignition package, which is part of Laravel, includes:

  • Light and dark themes 
  • Customizable “open in editor” functionality
  • Suggestions of solutions for common mistakes 

These, among many other user-friendly features, make it a unique experience.

Improved Route Listing

While it’s not news that Laravel has a command to list all routes inside the application, this feature just got reworked. Made with the developer experience in mind, it’s now easier to visualize the routes.

The new output you’ll get after running `php artisan route:list` is: 

Screenshot of the new route:list CLI output improved for Laravel 9 release.

Image Source

Test Coverage Using Artisan

Now you can test coverage using the well-known Laravel command: `php artisan test`. This command now has a `–coverage` option available that can help you keep track of the amount of code coverage your tests provide.

Once you run it, this is the result you get:

Screenshot of a converge test using the command php artisan test for Laravel 9.

Image Source

And there’s more to it. You can use the `–min` option to define a minimum threshold for your tests.

For example: 

1`php artisan test --coverage --min=90`

Collections Are Now Easier For IDEs to Understand

I never get tired of saying that Laravel is always being iterated upon to create the best possible experience for PHP developers. From significant changes to small ones like this, it’s undoubtedly one of the main concerns of the Laravel team.

The latest Laravel version added an improved “generic” style type definition to the collections component. This makes it easier for IDEs such as PHPStorm or static analysis tools like PHPStan to understand Laravel collections and help developers with autocompletion.

Generic style type definitions improvements to the collections component, IDE and static analysis support for Laravel 9.

Image Source

Helper Functions

Laravel has an extensive list of helper functions available. Many of them are used internally, but they are also available to use anywhere in your app.

In the new release, there are two new helpers, and you’ll probably need them at some point:

1 – str()

This function is equivalent to the `Str::of()` method, meaning that everything available is also available in the str() helper. Take a look at this example:

2$string = str('Scalable')->append(' Path'); // 'Scalable Path'

If you don’t pass an argument to the str() function, it will return an instance of Illuminate\Support\Str, and you still can chain methods and pass the string to them.

2$upper = str()->upper('scalable path'); // SCALABLE PATH

2 – to_route()

Before this update, it was necessary to do something like this to redirect the user where you wanted:

2return redirect()->route('blood_groups.show', ['blood_group' => 'A']);

It’s not bad, but it could be shorter and more straightforward. This helper does precisely that. 

Using the to_route function generates a redirect HTTP response for the route you’ve passed to it. 

1return to_route('blood_groups.show', ['blood_group' => 'A']);

You can also use the third and fourth arguments of the to_route() helper to pass the HTTP status code and any additional responder headers needed, respectively.

1return to_route('blood_groups.show', ['blood_group' => 'A'], 302, ['X-Custom-Header' => 'Laravel9'])

Full-Text Indexes

Since Laravel 8, migrations can add a full-text index to a column through the `fullText()` method. And we have two new methods: `whereFullText` and `orWhereFullText`. 

You can use them just like any other method on queries:

1$posts = DB::table('posts')
2 ->whereFullText('text', 'GraphQL')
3 ->get();

The difference here is that both methods will generate an appropriate SQL query for the database engine to query a full-text column. For example, if you are using a MySQL database, instead of generating a `LIKE` clause to compare text, Laravel would automatically generate a `MATCH AGAINST` clause. This can significantly improve the performance of the app.

New Laravel Scout Database Engine

Laravel Scout is directly related to full-text search on Laravel Apps, specifically on Eloquent models. It provides an excellent way to add this feature to your app in a matter of minutes. 

Before Laravel 9, the only way to use Scout was through services like Algolia or Meilisearch. Now we have a new database driver, which allows full-text searches using a common database.

Even though it’s not the best solution from a performance perspective, it’s still interesting if your application interacts with databases that aren’t too large or have a small workload.  

Should I Upgrade My App to Laravel 9?

The short answer is yes. It’s best to always keep up with the latest version to benefit from the new features and a longer period of bug and security fixes for the core of your application.

Still, sometimes it’s complicated to update your application because you need to deal with third-party packages, hosting, and many other things related to your project.

My advice: don’t rush to update your application right away if it’s not necessary. Take some time to plan the update, check if the packages your app depends on also have a newer version supporting Laravel 9 and PHP 8, and don’t forget to change the hosting environment accordingly.
Remember to read carefully through the official upgrade guide. You’ll find all major and minor changes that you need to adapt to make your app 100% compatible with Laravel 9.

Additional Resources

There are some essential resources to sharpen your Laravel knowledge and keep your skills up to date. Of course, my first suggestion is the official documentation website, which I think is some of the best documentation available for web frameworks and a reason alone to choose Laravel for your project.

If you’re a visual learner, you can also go to laracasts.com, which is an excellent video resource to learn Laravel and PHP, Techniques, Testing, and even JavaScript frameworks. You can also check out this free video series about Laravel 9 which explains the new features through quick and compelling videos.


Laravel has been the most popular PHP framework for a long time for good reason. Regardless of whether you’re building a simple or enterprise-level app, Laravel has features with substantial capabilities, a vast ecosystem, and a highly active community. 

The future of Laravel will undoubtedly follow the directives established by the internal team regarding new releases, avoiding breaking changes, and a regular release schedule. The future likely holds even more features and new tools to improve the developer experience and help create amazing apps.

Looking to hire?

Join our newsletter

Join thousands of subscribers already getting our original articles about software design and development. You will not receive any spam. just great content once a month.


Read Next

Browse Our Blog