Principales novedades en Laravel 9

Laravel 9 fue lanzado el 8 febrero de 2022 e incluye múltiples mejoras y nuevas funcionalidades. Agrupación de rutas por controlador, una nueva API para definir accesores y mutadores, un nuevo motor de base de datos para Laravel Scout, y más mejoras.


Agrupando rutas por controlador

Supongamos que tenemos algunas rutas configuradas en nuestro archivo routes/web.php para una web de podcast.

Route::get('/podcasts', [PodcastController::class, 'index']);
Route::get('/podcasts/{podcast}', [PodcastController::class, 'show']);
Route::post('/podcasts', [PodcastController::class, 'store']);

En Laravel 9 podemos ahora agrupar las rutas por controlador con Route::controller.

Route::controller(PodcastController::class)->group(function () {
	Route::get('/podcasts', 'index');
	Route::get('/podcasts/{podcast}', 'show');
	Route::post('/podcasts', 'store');
});

Donde asumirá que el controlador será PodcastController para cualquier ruta declarada dentro del grupo.

Podemos probar las rutas con el comando php artisan route:list.

Lista de rutas

De hecho, el resultado del comando artisan route:list ha sido rediseñado también en Laravel 9. En versiones anteriores lucía así.

Lista de rutas v8

Clases de migración anónimas

Desde la versión 9, las clases de migraciones han dejado de tener nombre.

// migrations/2014_10_12_000000_create_users_table.php

return new class extends Migration
{
	// ...
}

Anteriormente, tendrían el nombre class CreateUsersTable. Incluso las migraciones generadas con el comando Artisan make:migration serán con una clase anónima.

php artisan make:migration create_podcasts_table
<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('podcasts', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('podcasts');
    }
};

Nuevos helpers

Laravel 9 introduce dos nuevos helpers. str() y to_route().

Helper str()

str() es un nuevo helper para generar un objeto Stringable. Escencialmente es un alias de Str::of(). Por ejemplo, Str::of('Oliver')->upper() y str('Oliver')->upper() devuelven lo mismo. De esta manera, evitamos tener que importar la clase Str cada vez que queramos manipular un string.

- use Illuminate\Support\Str;

- Str::of('Oliver')->upper(); // OLIVER

+ str('Oliver')->upper(); // OLIVER

Helper to_route()

to_route es un nuevo helper para realizar redirecciones de una forma más limpia. Por ejemplo, redirect()->route('welcome') y to_route('welcome') redirigen a la misma ruta con nombre welcome.

// ...

- return redirect()->route('welcome');

+ return to_route('welcome');

Rediseño de la página de error

La página de error de Ignition ha sido rediseñada para Laravel 9. Con un nuevo menú superior para acceder al stack, el contexto, la documentación y configuración de ignition. Quizá más intuitiva ahora.

Ignition en v9

En Laravel 8, lucía así.

Ignition en v8

Renderizar un string con Blade

Con el método Blade::render(), ahora es posible convertir un string con sintaxis Blade a su equivalente en HTML. Por ejemplo, Blade::render('Hola {{ $user->name }}', ['user' => auth()->user()]) devolverá "Hola Oliver".

// ...

return Blade::render('Hola {{ $user->name }}', ['user' => auth()->user()]);

// Hola Oliver

Scoped bindings forzados

Cuando vinculamos múltiples modelos Eloquent a una misma ruta podemos hacerlos de tal manera que el segundo modelo de Eloquent pertenezca al primer modelo con el método scopeBindings. Por ejemplo, en la siguiente ruta, buscará y devolverá el podcast únicamente si pertenece al usuario en la ruta. De lo contrario devolverá un error 404 de que no fue encontrado.

use App\Models\Podcast;
use App\Models\User;

Route::get('/users/{user}/podcasts/{podcast}', function (User $user, Podcast $podcast) {
    return $podcast;
})->scopeBindings();

Cobertura de tests

Laravel 9 introduce una nueva opción --coverage al comando Artisan test. Habilitando esta opción nos genera un reporte de la cobertura que tienen nuestros tests en el proyecto.

Esta opción requiere que tengamos instalado XDebug. En Mac y con Homebrew instalado, podemos hacerlo con el comando pecl install xdebug. Modificamos nuestro archivo php.ini para añadir la configuración XDEBUG_MODE=coverage y reiniciamos PHP.

Ahora podemos ejecutar el comando Artisan php artisan test --coverage para ejecutar los tests y generarnos un reporte. En este caso la cobertura es del 54.2%.

Cobertura

Si queremos establecer una cobertura mínima en nuestro proyecto, podemos hacerlos con la opción --min, php artisan test --coverage --min=70 De esta manera nos arrojará error si no cumplimos con la mínima cobertura.

Cobertura ---

Motor "base de datos" para Laravel Scout

Para bases de datos pequeñas, Laravel ahora incluye un motor "base de datos" para usarlo junto con Laravel Scout. De esta manera, no tenemos que utilizar o instalar un servicio como Algolia o MeiliSearch.

Para utilizar este motor, simplemente debemos añadir la variable SCOUT_DRIVER=database en nuestro archivo .env.

Hay que tomar en cuenta que este motor únicamente funciona para bases de datos MySQL y Postgres.


Índices Full Text

En bases de datos MySQL o PostgreSQL podemos definir columnas del tipo fullText para generar índices Full Text:

$table->text('description')->fullText();

De esta manera podemos realizar consultas del tipo MATCH AGAINST, en el caso de MySQL:

$podcasts = Podcast::whereFullText('description', 'Laravel')->get();

Bastante útil para realizar búsquedas de texto simples. Aporta un mejor rendimiento que con consultas del tipo where like.


Nueva API para accesores y mutadores

Ahora podemos definir nuestro accesores y mutadores de una forma más limpia.

Anteriormente, para definir un accesores que nos devuelva el valor en mayúsculas y un mutador que guarde el valor en minúsculas utilizábamos la convención getXAttribute y setXAttribute:

public function getUsernameAttribute($value)
{
	return str($value)->upper();
}

public function setUsernameAttribute($value)
{
	return str($value)->lower();
}

Ahora, podemos hacerlo con un solo método que declare que devolveremos un objeto Illuminate\Database\Eloquent\Casts\Attribute.

use Illuminate\Database\Eloquent\Casts\Attribute;

// ...

public function username(): Attribute
{
	return new Attribute(
		get: fn($value) => str($value)->upper(),
		set: fn($value) => str($value)->lower()
	);
}