В одном из прошлых постов было озвучено изучение мидлварей в Laravel 11 до его релиза. Что изменилось с тех пор и с чем мы столкнулись на практике, рассмотрим ниже.
Основная «киллер-фича» фреймворка Laravel версии 11 — «плоский код«. Под капот убрано всё, что большинством разработчиков не используется, по сути являющееся «мусором». А также убраны некоторые действительно полезные вещи.
Изменения
То, что сразу бросается в глаза при установке проекта:
-
Отсутствуют мидлвари;
-
Отсутствуют некоторые сервис-провайдеры;
-
Появился файл
bootstrap/providers.php
; -
Определение роутов, команд, мидлварь, ошибок и другого перенесено в файл
bootstrap/app.php
(раньше мидлвари определялись в файлеapp/Http/Kernel.php
, эксепшены вapp/Exceptions/Handler.php
, крон-команды вapp/Console/Kernel.php
); -
Опция из настроек окружения для определения хранилища кэша переименована с
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/
Добавить комментарий