Laravel 11

Рубикон

«Старый свет погибает с теми, кто не способен понять новое». Mr. Freeman, part 62

# Что произошло?

Laravel 11 вышел в марте 2024 года и в отличии от 6 - 10 версий, которые не сильно отличались друг от друга, 11-ая получила обновленную структуру scaffold-а. Вся суть обновления заключается в минималистичности стартового приложения и некоторые привычные моменты были спрятаны под "капот". Это коснулось конфигурации, мидлварок, планировщика, маршрутов, обработчика ошибок и сервис-провайдеров.

Функционал фреймворка работает как и прежде, ничего важного не удалили и не добавили. Но если ты привык использовать предыдущие версии, то в 11-ой у тебя скорей всего возникнут вопросы как сделать привычные вещи или почему что-то стало работать не так.

# Обновление до 11

Согласно «Upgrade Guide» не рекомендуется переносить новую структуру 11-ой версии в существующее приложение, т.к. сохранена полная совместимость.

Это не значит, что обновляться до 11-ой версии не рекомендуется. Речь идет именно за структуру (файлы/классы и директории), поэтому можно сохранить текущую структуру приложения.

Конечно, в дальнейшем это может создать двойственность и запутать начинающих разработчиков без опыта в предыдущих версиях, но так ли это важно? Обновление структуры в существующем проекте может потребовать много усилий и не дать соразмерного профита, но в перспективе будет проще обновляться до новых версий.

Для обновления с 10-ой версии необходимо ознакомиться с «Upgrade Guide» , а также можно воспользоваться GitHub comparison tool для более детального изучения изменений в исходном коде. Обновление происходит так же, как и во всех предыдущих версиях.

# Что изменилось?

Пересказывать «Release Notes» не имеет смысла, в них отражены все важные моменты. Мы же разберем популярные и неочевидные проблемы, с которыми столкнулись разработчики.

# Основные моменты

  • Содержимое app после установки:

    App\Http\Controllers\Controller App\Models\User App\Provider\AppServiceProvider

  • Удалено: app/Exceptions app/Console app/Http/Middleware app/Http/Kernel.php lang
  • Регистрация сервис-провайдеров перенесена из конфига config/app.php в файл bootstrap/providers.php
  • Регистрация middleware перенесена из app/Http/Kernel.php в файл bootstrap/app.php
  • Обработчик ошибок перенесен из app/Exceptions/Handler.php в файл bootstrap/app.php
  • Планировщик (cron) перенесен из app/Console/Kernel.php в файл bootstrap/app.php

# Service Providers

Регистрация провайдеров в config/app.php всё еще доступна, но по умолчанию была перемещена в bootstrap/providers.php. В массиве указываются свои провайдеры, например, AppServiceProvider. Альтернатива: передавать массив с помощью метода withProviders() в bootstrap/app.php.

Для удаления дефолтных провайдеров фреймворка необходимо прибегнуть к старому способу через конфиг, с помощью ServiceProvider::defaultProviders()->except().

Стоит отметить, что были удалены многие провайдеры и все регистрации предлагается совершать через App\Providers\AppServiceProvider. С одной стороны это сосредоточило логику настройки приложения, с другой может сильно загрязнить провайдер. Конечно, никто не запрещает создать любые необходимые провайдеры и подключить их, тем самым распределив ответственность.

# Middleware

Все middleware были перемещены без изменений в \Illuminate\Foundation\Configuration\Middleware.

Согласно документации , управление осуществляется в методе withMiddleware().

Для этого в распоряжении есть объект \Illuminate\Foundation\Configuration\Middleware , со всеми необходимыми методами: append, prepend, remove, replace, group, use и т.п.
С полным списком можно ознакомиться в API или в документации .

Пример, как это выглядит в 11 версии:

1use Illuminate\Foundation\Configuration\Middleware;
2 
3Application::configure()
4 ->withMiddleware(function (Middleware $middleware) {
5 $middleware->redirectTo(
6 guests: fn () => route('login'),
7 users: fn () => route('dashboard'),
8 );
9 
10 $middleware->encryptCookies(except: [
11 'name',
12 ]);
13 
14 $middleware->preventRequestsDuringMaintenance(except: [
15 '/horizon*',
16 ]);
17
18 $middleware->statefulApi(); // для Sanctum SPA
19 $middleware->throttleApi(); // throttle по умолчанию выключен
20
21 /* Вместо старого свойства $middleware */
22 $middleware->append([
23 GlobalMiddleware::class,
24 ]);
25 
26 /* Вместо старого свойства $middlewareGroups */
27 $middleware->web(append: [
28 WebMiddleware::class,
29 ]);
30 
31 $middleware->api(remove: [
32 SubstituteBindings::class,
33 ]);
34 
35 /* Вместо старого свойства $middlewareAliases */
36 $middleware->alias([
37 'custom-alias' => CustomMiddleware::class,
38 ]);
39 })
highlight by torchlight.dev

# Router

App\Providers\RouteServiceProvider удален и настройки производятся в withRouting() , где указываются абсолютные пути к файлам маршрутов через аргументы: web, api, channels, commands.

По умолчанию API маршруты не подключены, установить можно с помощью команды php artisan install:api, смотри документацию .

Префикс для API маршрутов можно изменить с помощью аргумента: apiPrefix

Route::pattern() и RateLimiter::for() можно определить в AppServiceProvider::boot().

# Exceptions

Обработка исключений перемещена из App/Exceptions/Handler в withExceptions().

Глобально ничего не изменилось, принцип такой же как с middleware, доступны все необходимые методы в \Illuminate\Foundation\Configuration\Exceptions .

Подробнее смотри документацию или пример в секции «Наводим порядок».

# Redirect

За переадресацию пользователей раньше отвечали отдельные middleware, теперь за это отвечает метод $middleware->redirectTo(), который принимает два аргумента:

guests - для переадресации c auth маршрутов, если пользователь не аутентифицирован.

users - для переадресации с гостевых маршрутов, если пользователь аутентифицирован.

Оба аргумента могут принимать callback, в который передается Request, это может быть полезно для переадресации с учетом запроса. Пример смотри выше в секции «Middleware».

Обрати внимание, что callback должен возвращать строку, путь куда будет перенаправлен пользователь. Возвращать Response нельзя.

Так же стоит отметить, что для не аутентифицированных пользователей фреймворк самостоятельно вызовет $request->expectsJson() и для запросов ожидающих json редиректа не будет. Но для аутентифицированных, такого поведения нет и всегда будет редирект, что может оказаться нежелательным поведением для API, поэтому, возможно, понадобится дополнительная проверка в callback.

# Casts

Изменился способ определения кастов полей в моделях. Старый способ через свойство protected array $casts; доступен тоже. В новом способе предлагается использовать метод protected function casts(): array, который возвращает массив (формат массива не изменился). Подробнее смотри в документации .

Обрати внимание, что при одновременном использовании двух способов, массивы сливаются и ключи перечисленные в методе casts() могут перетереть ключи из массива в свойстве $casts, поэтому рекомендую выбрать один способ и придерживаться его.

# Throttling

По умолчанию RateLimiter теперь отключен для API маршрутов.

Для включения необходимо вызвать $middleware->throttleApi(); в withMiddleware().

Пример смотри в секции «Middleware».

# CORS

Конфиг config/cors.php по умолчанию отсутствует. Для публикации конфига используй команду: php artisan config:publish cors

Подробнее смотри в документации .

# Service Aliases

Хоть альясы сервисов и не являются рекомендательной практикой, но если использовались, то в 11 версии альясы можно определить в AppServiceProvider

1use Illuminate\Foundation\AliasLoader;
2 
3public function register(): void
4{
5 $aliasLoader = AliasLoader::getInstance();
6 $aliasLoader->alias('CustomService', CustomService::class);
7}
highlight by torchlight.dev

# Наводим порядок

Изменения в 11 версии были восприняты сообществом неоднозначно. В основном из-за подхода к настройке middleware, exceptions, scheduler. Всё было вынесено в bootstrap/app.php, что в реальном проекте сильно "раздувает" код и выглядит лаконично только в документации.

Есть очень простое решение, как можно "вернуть" старую структуру не отказываясь от новой.

Методы withExceptions, withMiddleware, withSchedule и т.п. принимают тип callable, а значит мы можем передать invokable объект, тем самым вынеся логику в привычные и отдельные классы.

Создаем классы с магическим методом __invoke(), имена и расположение могут быть любыми, и переносим всю логику из callback.

App\Exceptions\ExceptionHandler

1namespace App\Exceptions;
2 
3use Illuminate\Foundation\Configuration\Exceptions;
4
5final readonly class ExceptionHandler
6{
7 public function __invoke(Exceptions $exceptions): void
8 {
9 $exceptions->dontReport([
10 SilentException::class,
11 ]);
12
13 $exceptions->dontFlash([
14 'secret_key',
15 ]);
16
17 $exceptions->reportable(function (Throwable $e) {
18 Integration::captureUnhandledException($e);
19 });
20 
21 $exceptions->render(function (AppException $exception, Request $request) {
22 // any logic
23 });
24 }
25}
highlight by torchlight.dev

App\Console\ScheduleHandler

1namespace App\Console;
2 
3use Illuminate\Console\Scheduling\Schedule;
4
5final readonly class ScheduleHandler
6{
7 public function __invoke(Schedule $schedule): void
8 {
9 $schedule
10 ->command(SendEmailCommand::class)
11 ->withoutOverlapping()
12 ->everyTenMinutes();
13 }
14}
highlight by torchlight.dev

App\Http\Middleware\MiddlewareHandler

1namespace App\Http\Middleware;
2
3use Illuminate\Foundation\Configuration\Middleware;
4
5final readonly class MiddlewareHandler
6{
7 public function __invoke(Middleware $middleware): void
8 {
9 $middleware->redirectTo(
10 guests: fn () => route('login'),
11 );
12 
13 $middleware->statefulApi();
14 $middleware->throttleApi();
15 }
16}
highlight by torchlight.dev

Применяем классы в bootstrap/app.php

1use App\Console\ScheduleHandler;
2use App\Exceptions\ExceptionHandler;
3use App\Http\Middleware\MiddlewareHandler;
4use Illuminate\Foundation\Application;
5 
6return Application::configure(basePath: dirname(__DIR__))
7 ->withExceptions(new ExceptionHandler)
8 ->withMiddleware(new MiddlewareHandler)
9 ->withSchedule(new ScheduleHandler)
10 ->withRouting(
11 web: __DIR__ . '/../routes/web.php',
12 api: __DIR__ . '/../routes/api.php',
13 )
14 ->create();
highlight by torchlight.dev

Технически ничего не изменилось, но логика была распределена по своим классам и bootstrap/app.php стал выглядеть намного чище и проще для восприятия.