To use this site please enable javascript on your browser! What's New in Laravel 9?

We use cookies, as well as those from third parties, for sessions in order to make the navigation of our website easy and safe for our users. We also use cookies to obtain statistical data about the navigation of the users.

See Terms & Conditions

What's New in Laravel 9?

by Bryce Andy 07:02 Feb 06 '22

The upcoming long term support (LTS) version for Laravel was scheduled for a release September last year. But since the yearly releases begun, we will have Laravel 9 released February 8th.

Laravel 9

New Features in Laravel 9

  • Minimum PHP Version

Starting Laravel 9, you will require to have a minimum of PHP 8.0. This is because of the introduction of Symphony 6 which requires the PHP version as a dependency.

  • Updated event:list command

The artisan command to list events and listeners used to display those defined in the $listen property.

As this is not the only place to register events and listeners (you may register using Event::listen()), now the event:list command will list all events and listeners, even the ones used by the framework:

Artisan command event list screenshot

  • Anonymous migrations

When you create migrations using the migration:make command in Laravel 9, the migrations will not have the default class name.

This is to prevent migration classes with conflicting or similar names:

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('cache', function (Blueprint $table) {
            $table->string('key')->primary();
            // ...
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('cache');
    }
};
  • Updated table display for route:list command

In the previous versions of Laravel, when running this command it would display the table with overlapping rows due to long controller or names.

The default display in Laravel 9 has been shortened and redesigned, while the previous design can be obtained by adding the verbose flag -v:

Laravel artisan route list command screenshot

  • Test coverage option

The artisan test command has a new --coverage option that will show your test coverage. Be sure to have Xdebug installed with your PHP version:

Laravel artisan test --coverage screenshot

  • Eloquent accessors and mutators

The creator of Laravel added a new way to define eloquent accessors/mutators.

Let's assume you have a title attribute and wanted to define its accessor and mutator. Before this release, you would need to define them in the following way:

public function getTitleAttribute($value)
{
    return ucfirst($value);
}
 
public function setTitleAttribute($value)
{
    $this->attributes['title'] = $value;
}

In Laravel 9, you will now define one method for both and name it after the attribute. The return should be of type Illuminate\Database\Eloquent\Casts\Attribute:

use Illuminate\Database\Eloquent\Casts\Attribute;
 
protected function title(): Attribute
{
    return new Attribute(
        get: fn ($value) => ucfirst($value),
        set: fn ($value) => $value,
    );
}

Using a protected method is important to prevent it from being accessed outside its scope.

For the case of value-objects that make use of transforming multiple model attributes, you may define the accessor by using a second $attributes argument while returning the object and the mutator should return an array of the attributes, the same way object-value custom casts are defined:

use App\Support\Address;
use Illuminate\Database\Eloquent\Casts\Attribute;

public function address(): Attribute
{
    return new Attribute(
        get: fn ($value, $attributes) => new Address(
            $attributes['address_line_one'],
            $attributes['address_line_two'],
        ),
        set: fn (Address $value) => [
            'address_line_one' => $value->lineOne,
            'address_line_two' => $value->lineTwo,
        ],
    );
}

By default the object value will be cached so you may disable by wrapping a withoutObjectCaching method:

return (new Attribute(...))->withoutObjectCaching();
  • Query, eloquent & relation Builder interface

If you rely on type-hints for static analysis, refactoring or code completion in your IDEs, you may realize it's difficult to differentiate between Query\Builder, Eloquent\Builder and Eloquent\Relation.

Laravel 9 comes with an interface Illuminate\Contracts\Database\Query\Builder that will be shared by these three classes and solve this problem:

use Illuminate\Contracts\Database\Query\Builder;

return Model::query()
  ->whereNotExists(function(Builder $query) {
    // no need to know that $query is a Query\Builder
  })
  ->whereHas('relation', function(Builder $query) {
    // no need to know that $query is an Eloquent\Builder
  })
  ->with('relation', function(Builder $query) {
    // no need to know that $query is an Eloquent\Relation
  });
  • Implicit route enum binding

After the introduction of native enum support in PHP 8.1, Laravel has in turn added various features in the framework related to enums. One of them is binding routes to enums:

enum OrderStatus: int
{
    case PENDING = 1;
    case PLACED = 2;
    case PROCESSED = 3;
    case DELIVERED = 4;
}

Route::get('order-status/{orderStatus}', function (OrderStatus $status) {
    return $status->name;
});

If an enum that doesn't exist is called, the route will default to the usual NotFound response.

  • Enum casting

Another enum related feature is the ability to cast attributes to enums in eloquent:

use App\Enums\OrderStatus;
 
/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'status' => OrderStatus::class,
];

The casted attribute may then make use of its many cases:

$order->status === OrderStatus::PROCESSED
    ? $order->prepareForDelivery()
    : $user->notify(new CompleteOrderPayment($order));
  • Updated ignition exception page

The package Laravel uses to display error messages has a brand new look:

Laravel Ignition

  • Route controller groups

Within routes you may now group routes in the same controller using the controller method, and specifying the controller class:

Route::controller(PostController::class)->group(function () {
    Route::get('posts', 'index');
    Route::get('posts/{post}', 'show');
    Route::post('posts', 'store');
});
  •  New helper functions

The first is the str() helper which is an alias of the stringable Str::of:

str('hello world')->slug();
// Equivalent to Str::of('hello world')->slug()

Other helpers are a result of PHP 8 string functions. str_contains(), str_starts_with() and str_ends_with():

str_contains("Laravel is awesome", "awe");
// true

str_ends_with("Tailwind CSS", "scss");
// false

The other helper is the to_route() helper. This will force a redirect response and you may add data to redirect with:

return to_route('posts.show', ['post' => $id);
  • Optional scoped bindings

This feature was released in the later versions of Laravel 8 where you may scope child models to their parents, otherwise a child that doesn't belong to the parent returns a 404 response:

Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
    return $post;
})->scopeBindings();
  • Full text indexing

Full text indexes are supported in Laravel 9 with PostgreSQL and MySQL.

To define full text indexes, use the fullText method in the migration column definition:

$table->longText('body')->fullText();

You may then query using where clauses for the attribute with a full text index:

$posts = Post::query()
    ->whereFullText('body', $searchTerm)
    ->get();
  • Database engine for Laravel Scout

For applications that are not too large, you now have the ability to use the new database engine for Laravel Scout.

  • Rendering inline blade templates

With blade you may now use the render method to transform a raw Blade template string into HTML. You may also add an optional array of data as the second argument:

use Illuminate\Support\Facades\Blade;
 
return Blade::render('Hello, {{ $name }}', ['name' => 'Bryce Andy']);

Minor Features in Laravel 9

  • Testing facades with expects

When mocking facades in tests, you would normally call the shouldReceive method. Now you may also use the expects method the same way it's done in Pest:

// Before
Cache::shouldReceive('remember')
    ->with('testing', 'is', 'important')
    ->once();

// After
Cache::expects('remember')
    ->with('testing', 'is', 'important');
  • Global scope generation command

Use the artisan command make:scope to automatically generate a general scope file. The new scope will be located in the app/Models/Scopes directory.

php artisan make:scope FilterScope
  • Fluent redirect helper with setIntendedUrl

The redirector helper can be used to chain more methods after setting the intended URL. Previously you would need to set the intended URL separately but now you can chain methods:

redirect()
    ->setIntendedUrl('https://shop.bryceandy.com/checkout')
    ->to(route('login'));
  • Override blade component props

In nested blade components, you can override prop values from the top to bottom:

<!-- input.blade.php -->
@props(['disabled' => false])

<input type="text" {{ $disabled ? 'disabled' : '' }} />

<!-- input-label.blade.php -->
@props(['label'])

<label>{{ $label }}</label>
<x-input {{ $attributes }}></x-input>

<!-- Usage -->
<x-input-label disabled label="First name" />
  • Default column names

All contributions by @jasonmccreary, there are new default column names when defining migrations for IP addresses, MAC addresses and UUIDs:

// Before
$table->ipAddress('ip_address');
$table->macAddress('mac_address');
$table->uuid('uuid');

// After
$table->ipAddress();
$table->macAddress();
$table->uuid();
  • Update timestamps using the attribute name

The touch method can be used to update timestamps by providing an array containing names of the attribute and the timestamp.

In Laravel 9 you can now update by providing the attribute name only:

// Before
$model->update(['attribute' => now()]);
// or
$model->touch(['attribute' => now()]);

// After
$model->touch('attribute');
  • Age option when flushing failed jobs

When flushing failed jobs we normally delete all the jobs, but now there is a new option --age, that deletes failed jobs older than the age number in days:

php artisan queue:flush --age=30
// Failed jobs older than 30 days are deleted
  • Support for keys in FormRequest validated method

To quickly fetch the validated input value in Laravel 9, we now have the ability to insert the input key in the validated method:

$name = $request->validated('name');
  • Using iterables in whereBetween queries

Using the query builder method, you may use an iterable as the second argument. This could be an array, Illuminate\Support\Collection, Carbon\CarbonPeriod, etc:

$period = now()->toPeriod(now()->addDay());

User::whereBetween('created_at', $period)->get();

// or

User::whereBetween('id', collect([1,2]))->get();
  • Query builder havingNull and havingNotNull

New methods for the query builder:

// Instead of this
User::select(...)
    ->join(...)
    ->groupBy(...)
    ->havingRaw('users.sample_column is null')
    ->get();

// You may use this
User::select(...)
    ->join(...)
    ->groupBy(...)
    ->havingNull('sample_column')
    ->get();
  • Ignore case for Str::contains and Str::containsAll

In Laravel 9, you may add a boolean argument whose value determines if letter casing is ignored. The default is false as expected:

use Illuminate\Support\Str;

Str::contains("Laravel is awesome", "laravel", true);
// true
  • Passing closures as conditionals to when and unless

The query builder and collection methods (when and unless) used to accept only boolean values but now closures are also accepted:

collect($payload['users'])
    ->unless(fn ($users) => $users->has('is_admin'))
    ->put('is_admin', true);

// Or using a higher order collection proxy
collect($payload['users'])
    ->unless->has('is_admin')
    ->put('is_admin', true);
  • Queue prune-failed command

The artisan command queue:prune-failed will delete all failed jobs just like queue:flush. It also has an hours option that will delete failed jobs older than the given number:

php artisan queue:prune-failed --hours=30
// Failed jobs older than 30 hours are deleted. Default is 24 hours

...

There will be many more current and upcoming features. You may notice some of the minor features listed here might be undocumented.

Happy coding artisans.

Updated 03:02 Feb 09 '22

If you like this content, please consider buying me coffee.
Thank you for your support!

Become a Patron!