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.
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:
-
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
:
-
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:
-
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:
-
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
andhavingNotNull
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
andStr::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
andunless
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.
Join to participate in the discussion