The Laracon Online event was this Wednesday 9th of February! The event was a huge success because it had many speakers that talked about everything techy for the viewers and also the most anticipated release was announced, the release of Laravel 9!

Laravel is a web application framework with expressive and elegant syntax. Freeing you to create without sweating the small things. Laravel values beauty, that's why every feature has been thoughtfully considered to provide a wonderful developer experience.

Laravel 9 Is the next long-term support version (LTS)  and along with it comes a wide array of useful new features and tweaks. Since Laravel 9 will require Symfony 6.0 it has a minimum requirement of PHP 8 for you to be able to use it. This includes an improved accessor/mutator API, better support for Enum casting, forced scope bindings, a new database engine for Laravel Scout, and so much more that we will detail in this blog.

Before moving on to new features, we'd like to point out that starting with Laravel 9, Laravel will release a new major version every twelve months instead of the previous release every six months. Keeping that in mind Laravel 9 will receive bug fixes until February 2024 and security fixes until February 2025.

Now let's talk about some of the new main features of this release:

New Test Coverage Option

When running tests, it’s always a good idea to keep track of the coverage so you can sleep at night. Laravel 9 adds the --coverage option along with the --min option to set the minimum coverage allowed.

Output of php artisan test –coverage –min=85
Output of php artisan test –coverage –min=90

See it working on our GitHub repository’s actions.

Note: You need to spin up Xdebug with the coverage mode for this to work.

Controller Route Groups

Previously, the routes had some sort of repetition when it comes to specifying the controllers. You could have seen something like this:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
};

But now, we have a new Route::controller() method for a cleaner approach.

Route::controller(UserController::class)->group(function () {
    Route::get('/', 'index');
    Route::get('/{user}', 'show');
    Route::post('/', 'store');
    Route::patch('/{user}', 'update');
    Route::delete('/{user}', 'destroy');
});

Note: Yes, we know we could have used the apiResource() method instead, but we wanted to demonstrate Route::controller() 😛

Implicit Enum Route Bindings

With PHP 8.1, the long-awaited enums finally arrived and Laravel 9 knows how to deal with them. When the provided value ({status} in the example below) is not part of the enum, the route will be a 404.

use App\Enums\PostStatus;

Route::get('post-status/{status}', function (PostStatus $status) {
    $posts = Post::query()->where('status', $status->value);

    return PostResource::collection($posts->get());
});

New routes:list output

Usually, when debugging applications, especially huge applications, finding controllers through the thousands of routes can get tricky. You could have used the --compact option, and might have helped, however, Laravel 9 brings a shiny new render of this command.

Forced Scoped Bindings

This is a juicy one. In the past, even though having full control, you could have made policies, model scopes, in-controller filtering, etc. to scope child models to be queried, Laravel 9 introduces the scopeBindings() route method:

Route::get('/{user}/posts/{post:id}', 'showPost')->scopeBindings();

The above will make sure that the specified post ID has the given user as a parent (the usual one-many belongsTo and hasMany relationship). In other words posts.user_id = user.id.

New Alternative Eloquent Accessors / Mutators Syntax

I was never bothered with the previous way of creating mutators and accessors because I try to avoid them. But I must admit the new way looks awesome.

/**
 * Name accessor/mutator
 *
 * @return Attribute
 */
public function name(): Attribute
{
    return new Attribute(
        get: fn ($value) => strtoupper($value),
        set: fn ($value) => $value,
    );
}

Nice way of embracing named parameters, huh? Keep in mind that the previous way of defining accessors and mutators is still valid, this is an alternative way of doing it with modern syntax.

Enum Eloquent Attribute Casting

Continuing with the enum integration, Laravel 9 also comes with enum attribute casting support for your models. In the past, you could have used something like this in your migration:

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('posts', function (Blueprint $table) {
	    $table->enum('status', ['draft', 'publish']);
    });
}

But the main issue with this approach is that for every new enum case, you needed to add a migration so your teammates or even remote environments (even staging, yup) have the updated deal.

Not anymore! Keep your database enum-free, your migrations string full (when needed tho, don’t be so literal), and the integrity in PHP.

<?php

namespace App\Models;

use App\Enums\PostStatus;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $casts = [
        'status' => PostStatus::class,
    ];
}

Now Laravel is going to yell at you for not using a valid enum value.

Anonymous Migration Classes

This one might seem obvious until you find yourself in a situation where you need to get creative to name a migration on an existing, big application.

🤓 Anecdote time: I needed to implement soft deletes on a model once in an existing application, so I named it AddDeletedAtToPosts, and it turns out, there was a migration called AddDeletedAtToPosts already because it was implemented at some point but then a DropDeletedAtFromPosts migration reverted it. See? This feature is useful.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
};

New QueryBuilder and DecoratesQueryBuilder Interfaces

As a type-hint maniac, I thank the Laravel core team and community for this. Laravel 9 introduces the new QueryBuilder and DecoratesQueryBuilder contracts that will help your IDE identify invocable members of the Query\Builder, Eloquent\Builder and Eloquent\Relation classes, without relying on docblocks for magic methods and the infamous __call().

Laravel Scout Database Engine

When it comes to full-text search features until now we could only use Algolia or Meilisearch out of the box. Starting on Laravel 9 we can use our database as a driver and set it up is as simple as setting SCOUT_DRIVER=database and you’re done!

You may explicitly shape your searchable models by adding a toSearchableArray() method to your models.

/**
 * Convert the model to the searchable array.
 *
 * @return array<string, string>
 */
public function toSearchableArray(): array
{
    return [
        'title'   => $this->title,
        'content' => $this->content,
    ];
}

Full-Text Indexes / Where Clauses

When building searching endpoints you might have done something like this:

Post::query()->where('content', 'like', '%' . $request->input('q') . '%')->get();

And even Laravel Scout does that when using the database driver. Nothing wrong, or slow… Or well, it depends on your expectations, but now Laravel supports full-text indexes, which are significantly faster than WHERE LIKE queries because it uses MATCH/AGAINST.

Start by creating a full-text index in your migration like this:

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->text('content')->fulltext();
    });
}

And then search using the whereFullText() method.

Post::query()->whereFullText('content', $request->get('q'))->get();

Additionally, like we said earlier, Scout will do a WHERE LIKE by default, you can make Scout take advantage of the full-text benefits by adding the SearchUsingFullText attribute on the toSearchableArray() method of your model like so:

/**
 * Convert the model to the searchable array.
 *
 * @return array<string, string>
 */
#[SearchUsingFullText('content')]
public function toSearchableArray(): array
{
    return [
        'title'   => $this->title,
        'content' => $this->content,
    ];
}

The String Helper and PHP 8

Previously, there were some string helpers that managed to replicate some functions we see in other languages like JavaScript to achieve string parsing. For PHP devs, functions like String.prototype.includes() and String.prototype.startsWith() were like the kid meme asking his mom to buy him something, and then the mom saying “no Timmy, we have these at home”. The things at home were weird substr() and/or preg_match() approaches.

Not anymore! These functions have been made available natively since PHP 8, and Laravel 9 updates its helpers to stop using those workarounds and use the official ones. Thanks Tom Schlick!

Also, if you don’t want to use the FQN of the Str helper class, you can use the str().

$string = \Illuminate\Support\Str::of('Test');

// Can also be written as
$str = str('Test');

Out with Swiftmailer, in with Symfony Mailer

With the release of Symfony 6 (and 5.4 LTS) comes the end of maintenance on Swiftmailer in favor Symfony Mailer. There are some important aspects to get your existing applications compatible with this change, so make sure you go through the upgrade guide

My OCD is happier now that this is another symfony/* package.

Flysystem 3.x

Behavioral changes here and there, but some of the most important ones are those writing operations such as Storage::put(), Storage::write(), Storage::writeStream(), etc. now override existing files. So, you need to ask if the file exists now,

if (! Storage::exists('nft.zip')) {
    Storage::put('nft.zip', $zip);
}

Also, previously, when reading from nonexistent files thrown a FileNotFoundException exception, now they just return null.

$file = Storage::get('nonexisting.file'); // null

Laravel Breeze API & Next.js

The Laravel Breeze starter kit has received an "API" scaffolding mode and complimentary Next.js frontend implementation. This starter kit scaffolding may be used to jump-start your Laravel applications that are serving as an API with Laravel Sanctum authentication API for a JavaScript frontend.

Rendering Inline Blade Templates

Even though I believe everyone should have their space (applying SRP BTW), sometimes a Blade template file was overkill because you might just need to process a string variable instead of a view file. Or perhaps you receive the blade template from an external service, which aside from being a security concern, might also be tricky to parse successfully… Before Laravel 9 of course, because now you can do this:

use Illuminate\Support\Facades\Blade;

return Blade::render('{{ $company }} is hiring!', ['company' => 'Elaniin']);

Bootstrap 5 Pagination Views

If you are using Bootstrap, Laravel includes the pagination template especially for Bootstrap (because the default is Tailwind CSS), and you can enable them on your AppServiceProvider.

use Illuminate\Pagination\Paginator;

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    Paginator::useBootstrapFive();
}

Improved Ignition Exception Pages Style

Prettyyy! Debugging in Laravel is as visual as debugging can get thanks to Facade’s Ignition package and Laravel 9 comes with a refreshed style.

And many, many, many more! So make sure you upgrade your projects as soon as you can (so, at least whenever all the packages you use are compatible 😛) and help yourself with the upgrading guide.

If you’re interested in seeing some of these features applied in a test application, we created this repository when writing this blog post, take a look!