Работа с новой архитектурой в Laravel 11

от автора

В одном из прошлых постов было озвучено изучение мидлварей в Laravel 11 до его релиза. Что изменилось с тех пор и с чем мы столкнулись на практике, рассмотрим ниже.

Основная «киллер-фича» фреймворка Laravel версии 11 — «плоский код«. Под капот убрано всё, что большинством разработчиков не используется, по сути являющееся «мусором». А также убраны некоторые действительно полезные вещи.

Изменения

То, что сразу бросается в глаза при установке проекта:

  1. Отсутствуют мидлвари;

  2. Отсутствуют некоторые сервис-провайдеры;

  3. Появился файл bootstrap/providers.php;

  4. Определение роутов, команд, мидлварь, ошибок и другого перенесено в файл bootstrap/app.php (раньше мидлвари определялись в файле app/Http/Kernel.php, эксепшены в app/Exceptions/Handler.php, крон-команды в app/Console/Kernel.php);

  5. Опция из настроек окружения для определения хранилища кэша переименована с CACHE_DRIVER на CACHE_STORE.

Теперь рассмотрим более детально и как мы с этим работаем.

bootstrap/app.php

Сказать по-правде, управление всем внутри одного конкретного файла, мягко говоря, очень неудобно, не функционально и не красиво. Поэтому для распределения мы используем invoke классы.

В конечном итоге, наш файл выглядит так:

<?php  use App\Console\Handler as ScheduleHandler; use App\Exceptions\Handler as ExceptionHandler; use App\Http\Middleware\Handler as MiddlewareHandler; use Illuminate\Foundation\Application;  return Application::configure(basePath: dirname(__DIR__))     ->withMiddleware(new MiddlewareHandler())     ->withExceptions(new ExceptionHandler())     ->withSchedule(new ScheduleHandler())     ->withCommands([__DIR__ . '/../app/Console/Commands'])     ->withRouting(         api: __DIR__ . '/../routes/api.php',         commands: __DIR__ . '/../routes/console.php',         apiPrefix: ''     )->create();

Наше приложение работает исключительно как API и в нём нет таких понятий как web, blade, html, ts/js и т.п., за исключением страниц ошибок при попытке открыть их из адресной строки браузера. Вследствие чего и префикс api у всех роутов нам также не нужен, поэтому мы его «обнуляем».

Мидлвари

При чистой установке проекта Laravel предлагает описывать мидлвари в файле bootstrap/app.php, что является неудобным способом. Поэтому, при помощи invoke метода мы создали класс в привычном месте:

<?php  namespace App\Http\Middleware;  use App\Http\Middleware\Authorizations\AuthInternalMiddleware; use App\Http\Middleware\Authorizations\AuthPrivateMiddleware; use App\Http\Middleware\Authorizations\AuthPublicMiddleware; use Illuminate\Foundation\Configuration\Middleware as BaseMiddleware;  class Handler {     protected array $aliases = [         'auth.private'  => AuthPrivateMiddleware::class,         'auth.public'   => AuthPublicMiddleware::class,         'auth.internal' => AuthInternalMiddleware::class,     ];      public function __invoke(BaseMiddleware $middleware): BaseMiddleware     {         if ($this->aliases) {             $middleware->alias($this->aliases);         }          return $middleware;     } }

Кроме алиасов, можно также работать с группами и всем, что умеют «обычные» мидлвари. Просто выглядит это немного иначе. В данном случае нужны только алиасы.

Таким образом, всё взаимодействие с мидлварями вынесено во внешний файл и легко читается любым разработчиком, работающим с Laravel.

Объявляем мы этот файл в методе withMiddleware файла bootstrap/app.php:

<?php  use App\Http\Middleware\Handler as MiddlewareHandler; use Illuminate\Foundation\Application;  return Application::configure(basePath: dirname(__DIR__))     ->withMiddleware(new MiddlewareHandler())     // ...     ->create();

Обработчик ошибок

Также как и с мидлварями, эксепшены лишились файла app/Exceptions/Handler.php, который мы также вернули в логически корректное место добавив свои объявления. В итоге, файл выглядит следующим образом:

<?php  namespace App\Exceptions;  use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Configuration\Exceptions as BaseExceptions; use Illuminate\Http\Request; use Illuminate\Support\Facades\View; use Sentry\Laravel\Integration; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Throwable;  class Handler {     protected string $jsonFlags = JSON_UNESCAPED_SLASHES ^ JSON_UNESCAPED_UNICODE;      public function __invoke(BaseExceptions $exceptions): BaseExceptions     {         $this->renderUnauthorized($exceptions);         $this->renderNotFound($exceptions);          $this->reportSentry($exceptions);          return $exceptions;     }      protected function renderUnauthorized(BaseExceptions $exceptions): void     {         $exceptions->renderable(             fn (AuthenticationException $e, ?Request $request = null) => $this->response(                 message: __('Unauthorized'),                 code: 401,                 asJson: $request?->expectsJson() ?? false             )         );     }      protected function renderNotFound(BaseExceptions $exceptions): void     {         $exceptions->renderable(             fn (NotFoundHttpException $e, ?Request $request = null) => $this->response(                 message: __('Not Found'),                 code: 404,                 asJson: $request?->expectsJson() ?? false             )         );     }      protected function reportSentry(BaseExceptions $exceptions): void     {         $exceptions->reportable(             fn (Throwable $e) => Integration::captureUnhandledException($e)         );     }      protected function response(string $message, int $code, bool $asJson): Response     {         if ($asJson) {             return response()->json(compact('message'), $code, options: $this->jsonFlags);         }          $this->registerErrorViewPaths();          return response()->view($this->view($code), status: $code);     }      protected function view(int $code): string     {         return view()->exists('errors::' . $code) ? 'errors::' . $code : 'errors::400';     }      protected function registerErrorViewPaths(): void     {         View::replaceNamespace(             'errors',             collect(config('view.paths'))                 ->map(fn (string $path) => "$path/errors")                 ->push($this->vendorViews())                 ->all()         );     }      protected function vendorViews(): string     {         return __DIR__ . '/../../vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/views';     } }

Пути к view фалам переопределены для того, чтобы мы могли отображать переведённые расшифровки ошибок, например, «404 | Не найдено» вместо «404 | Not Found» для российской локализации.

Например, файл resources/views/errors/404.blade.php содержит следующий код:

@extends('errors::minimal')  @section('title', __('http-statuses.404')) @section('code',  404) @section('message', __('http-statuses.404'))

Объявляем мы этот файл в методе withException файла bootstrap/app.php:

<?php  use App\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Application;  return Application::configure(basePath: dirname(__DIR__))     ->withExceptions(new ExceptionHandler())     // ...     ->create();

Выполнение заданий по расписанию (schedule)

Внезапно в Laravel 11 определение запуска заданий по расписанию, которые принято называть «кроном» (cron), стали… барабанная дробь… РОУТАМИ!

Да, Вы не ослышались. Согласно новым правилам запуск консольных команд определяется в файле routes/console.php.

И нам с этим нужно срочно что-то делать начиная с того, что в нашей команде принято файл routes/console.php исключать из репозитория, так как его цель заключается в хранении личных наборов команд разработчика не беспокоясь о том, что она может случайно попасть в репозиторий. Такое постоянно случалось до введения этой практики, когда мы создавали новые «dev» классы в папке app/Console/Commands.

Поэтому создаём новый старый класс App\Console\Handler:

<?php  namespace App\Console;  use Illuminate\Cache\Console\PruneStaleTagsCommand; use Illuminate\Console\Scheduling\Schedule;  class Handler {     public function __invoke(Schedule $schedule): void     {         $schedule->command(PruneStaleTagsCommand::class)->hourly();          // ...     } }

Объявляем мы этот файл в методе withSchedule файла bootstrap/app.php:

<?php  use App\Console\Handler as ScheduleHandler; use Illuminate\Foundation\Application;  return Application::configure(basePath: dirname(__DIR__))     ->withSchedule(new ScheduleHandler())     // ...     ->create();

Консольные команды

Инициализация консольных команд происходит в методе withCommands файла bootstrap/app.php. Здесь нужно передать массив абсолютных путей к директориям. Таким образом, мы просто передаём параметр на папку app/Console:

<?php  use Illuminate\Foundation\Application;  return Application::configure(basePath: dirname(__DIR__))     ->withCommands([__DIR__ . '/../app/Console/Commands'])     // ...     ->create(); 

Здесь ничего сложного. Просто объявили и из консоли уже можно вызывать php artisan <name>.

Маршрутизация

Помимо прочего, новая версия Laravel также лишилась сервис-провайдера RouteServiceProvider и теперь объявлять маршруты нужно непосредственно в файлах папки routes. Так как у нас нет web зоны, мы решили оставить файл routes/api.php, добавив в него вызов групп:

<?php  app('router')     ->middleware('auth.private')     ->name('private.')     ->prefix('v1/private')     ->group(__DIR__ . '/api/private.php');  app('router')     ->middleware('auth.public')     ->name('public.')     ->prefix('v1/public')     ->group(__DIR__ . '/api/public.php');  app('router')     ->middleware('auth.internal')     ->name('internal.')     ->prefix('v1/internal')     ->group(__DIR__ . '/api/internal.php');

Сервис-провайдеры

Несмотря на то, что объявление сервис-провайдеров в новой версии фреймворка было вынесено в файл bootstrap/providers.php, старое расположение всё же работает, и работает по принципу array_merge.

Например, наше приложение имеет русскую fallback локализацию, но Laravel не умеет переводить текст из json файлов на неё. То есть условный __('Whoops!') отдаст как Whoops! вместо Упс!. Решает эту проблему пакетное решение JSON Fallback, но речь не столько о нём, сколько о принципе его подключения.

Так вот, для работы этого пакета нужно заменить дефолтный сервис-провайдер Illuminate\Translation\TranslationServiceProvider на другой. В случае с новой структурой приложения сделать это попросту невозможно — файл bootstrap/providers.php возвращает массив строк, где строка — прямая ссылка на сервис-провайдер. А нам мало того, что нужно заменить провайдер, дак ещё и заменить дефолтный, то есть тот, который находится где-то под капотом. И что делать? — спросите. Вот здесь нам на помощь и приходит новая старая опция providers в файле config/app.php. Просто добавляем её, объявляя дефолтные сервис-провайдеры, и всё. Приложение дальше само возьмёт их, сделает нужные подмены и подгрузит содержимое файла bootstrap/providers.php, следствием чего станет корректная работа приложения с заменяемыми сервис-провайдерами.

// config/app.php  <?php  use Illuminate\Support\ServiceProvider; use Illuminate\Translation\TranslationServiceProvider as BaseTranslationServiceProvider; use LaravelLang\JsonFallback\TranslationServiceProvider as JsonTranslationServiceProvider;  return [     'name' => env('APP_NAME', 'Laravel'),      // ...      'providers' => ServiceProvider::defaultProviders()->replace([         BaseTranslationServiceProvider::class => JsonTranslationServiceProvider::class,     ])->toArray(), ]; 

Фишка в том, что под капотом Laravel как раз прокидывает дефолтные провайдеры в этот самый путь конфигурации и, объявляя их таким образом, мы их подменяем.

routes/console.php

Данный файл каждый разработчик использует в своих личных целях и он исключён из попадания в репозиторий.

Аргумент commands метода withRouting в файле bootstrap/app.php позволяет принимать в качестве значения путь, который под капотом валидируется на существование. Таким образом, нам не нужно самостоятельно это делать.

В итоге, получаем следующее объявление:

<?php  use Illuminate\Foundation\Application;  return Application::configure(basePath: dirname(__DIR__))     ->withRouting(         api: __DIR__ . '/../routes/api.php',         commands: __DIR__ . '/../routes/console.php',         apiPrefix: ''     )->create();

И убеждаемся в наличии строчки /routes/console.php в файле .gitignore, а также в отсутствии файла в самом репозитории.

<?php  use Illuminate\Support\Facades\Artisan;  Artisan::command('foo', function () {     dump(         'I like it! Move it! Move it!'     ); });

Если какой-либо файл попал в репозиторий, хотя должен игнорироваться и правило на него описано в файле .gitignore, удалите файл и создайте коммит. Уже записанные в репозиторий файлы игнорируют эти параметры.

Заключение

Вернув старые новые файлы на свои места, мы решаем сразу несколько проблем при переходе на Laravel 11:

  • Нелогичность размещения объявлений управления;

  • Непонимание разработчиков при работе с органами управления;

  • Перегрузка роутов кроном;

  • Отсутствие специализированного файла, исключённого из репозитория, для хранения консольных команд разработчиков на личных машинах.

Главное, чтобы инструмент помогал решать задачи, а не пытался изменять архитектурное расположение колёс на полном ходу, где одно из них оказывается на заднем сиденье.

Именно эта причина и сподвигла меня написать эту статью на случай, если кому-то она окажется полезной.

Всех благ и удачных проектов!


ссылка на оригинал статьи https://habr.com/ru/articles/822185/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *