Глубокое погружение в Laravel Folio
Что такое Laravel Folio
Проще говоря, Laravel Folio — это страничный маршрутизатор для вашего Laravel-приложения. Все, что вам нужно сделать, — это создать blade-файл. Нет необходимости писать маршруты или создавать методы контроллера для возврата представления. Все просто и понятно.
Как его использовать
Я не буду вдаваться в подробности использования Folio, поскольку установка пакета composer и выполнение команды install
занимает всего около 5 минут, как и установка любого стандартного пакета Laravel. Также следует учитывать, что Folio находится в стадии бета-версии, а значит, до выхода версии 1 в него могут быть внесены существенные изменения.
Однако я могу указать вам на LaravelFolioServiceProvider
в каталоге app/Providers
, где вы найдёте этот код в методе boot
после установки:
public function boot(): void
{
Folio::route(resource_path('views/pages'), middleware: [
'*' => [
//
],
]);
}
Подробнее, что делает метод route
, мы поговорим чуть позже. Но с первого взгляда понятно, что он сканирует все файлы в пределах view/pages
и либо создаёт маршрут для каждого из них, либо отдельный обработчик, который обрабатывает запрашиваемую страницу.
Более пристальный взгляд на Folio
Прежде чем мы погрузимся в исходный код Folio, я думаю, будет полезнее показать, как создать простую версию самостоятельно. Давайте вместе реализуем базовую реплику Folio.
Базовая реплика Folio
Сначала создадим blade-файл по адресу resources/views/pages/profile.blade.php
. Этот файл будет просто содержать строку "Hello World":
<?php
$message = 'Hello World';
?>
<div>
{{ $message }}
</div>
Далее мы создадим класс, аналогичный классу Folio
. Назовём его Pager
.
namespace App;
use Illuminate\Support\Facades\Route;
class Pager
{
public static function route(string $path): void
{
$files = collect(scandir(resource_path('views/'.$path)))
->skip(3) // ['.', '..', '.gitkeep']
->filter(fn ($file) => str_ends_with($file, '.blade.php'))
->map(fn($file) => str_replace('.blade.php', '', $file));
$files->each(function ($view) use($path) {
Route::get($view, fn () => view($path.'/'.$view));
});
}
}
А в методе boot
AppServiceProvider
мы можем использовать Pager
, например
public function boot(): void
{
Pager::route('pages');
}
В методе route
указывается путь к директории resources/views
, содержащей все blade файлы, для которых мы хотим сгенерировать маршруты. Для каждого файла {something}.blade.php
мы создаём маршрут с URI {something}
, который возвращает view('pages/{something}')
, как и любой обычный метод create
внутри контроллера. Теперь, когда мы переходим по адресу app.test/profile
, он будет возвращать строку "Hello World". Это и есть базовая, упрощённая реализация Folio.
Конечно, наш пример не учитывает более сложные сценарии, такие как вложенные каталоги или динамические страницы типа views/pages/users/[id].blade.php
. В следующих разделах мы рассмотрим, как Folio справляется с такими ситуациями.
Ядро Folio
В Folio все начинается с FolioServiceProvider
, расположенного в config/app.php
вашего проекта.
public function boot(): void
{
Folio::route(resource_path('views/pages'), middleware: [
'*' => [
//
],
]);
}
Метод route
отвечает за сканирование пути и реализацию всех определённых нами middleware-правил.
Теперь давайте разберёмся, что в нем содержится.
public function route(string $path = null, ?string $uri = '/', array $middleware = []): static
{
$path = $path ? realpath($path) : config('view.paths')[0].'/pages';
if (! is_dir($path)) {
throw new InvalidArgumentException("The given path [{$path}] is not a directory.");
}
$this->mountPaths[] = $mountPath = new MountPath($path, $uri, $middleware);
if ($uri === '/') {
Route::fallback($this->handler($mountPath))
->name($mountPath->routeName());
} else {
Route::get(
'/'.trim($uri, '/').'/{uri?}',
$this->handler($mountPath)
)->name($mountPath->routeName())->where('uri', '.*');
}
return $this;
}
Он начинает с определения $path
, который будет использоваться, и имеет путь по умолчанию, если он не указан.
Далее он генерирует объект MountPath
и добавляет его в массив mountPaths
. Это говорит, что мы можем использовать метод route
столько раз, сколько нам нужно, если наши представления распределены по нескольким каталогам.
После создания объекта MountPath
проверяется, направлен ли URI на корневую страницу нашего приложения. Если да, то регистрируется обработчик в качестве резервного/fallback маршрута. Если нет, то определяется маршрут с целевым путём и передаётся тот же обработчик для разрешения всех маршрутов для наших представлений.
Скорее всего, мы будем использовать Folio для нашей корневой страницы, поэтому давайте сразу перейдём к вызову $this->handler
и посмотрим, что он делает.
protected function handler(MountPath $mountPath): Closure
{
return function (Request $request, string $uri = '/') use ($mountPath) {
return (new RequestHandler(
$mountPath,
$this->renderUsing,
fn (MatchedView $matchedView) => $this->lastMatchedView = $matchedView,
))($request, $uri);
};
}
Метод handler
возвращает замыкание, которое вызывается при обработке Laravel Router нового запроса.
Далее изучим класс RequestHandler
.
class RequestHandler
{
/**
* Создание нового экземпляра обработчика запросов.
*/
public function __construct(protected MountPath $mountPath,
protected ?Closure $renderUsing = null,
protected ?Closure $onViewMatch = null)
{
}
/**
* Обработка входящего запроса с помощью Folio.
*/
public function __invoke(Request $request, string $uri): mixed
{
}
/**
* Получение middleware, которое должно быть применено к соответствующему представлению.
*/
protected function middleware(MatchedView $matchedView): array
{
}
/**
* Создание экземпляра ответа для заданного соответствующего представления.
*/
protected function toResponse(MatchedView $matchedView): Response
{
}
}
Рассмотрим метод __invoke
public function __invoke(Request $request, string $uri): mixed
{
$matchedView = (new Router(
$this->mountPath->path
))->match($request, $uri) ?? abort(404);
return (new Pipeline(app()))
->send($request)
->through($this->middleware($matchedView))
->then(function (Request $request) use ($matchedView) {
if ($this->onViewMatch) {
($this->onViewMatch)($matchedView);
}
return $this->renderUsing
? ($this->renderUsing)($request, $matchedView)
: $this->toResponse($matchedView);
});
}
Метод начинает работу с поиска подходящего представления, которое ссылается на blade-файл. Затем он создаёт конвейер/пайплайн для выполнения следующих задач:
- Передать запрос в список middleware
- Выполнить коллбэк onViewMatch, если он определён.
- Рендеринг представления
Теперь перейдём к рассмотрению метода $this->middleware
.
protected function middleware(MatchedView $matchedView): array
{
return Route::resolveMiddleware(
$this->mountPath
->middleware
->match($matchedView)
->prepend('web')
->merge($matchedView->inlineMiddleware())
->unique()
->values()
->all()
);
}
По сути, метод возвращает массив middleware. Особо следует рассмотреть метод $matchedView->inlineMiddleware()
.
Folio встроенный Middleware
Folio позволяет реализовать проверки middleware непосредственно в blade файле. Вот пример:
<?php
use function Laravel\Folio\middleware;
middleware(['auth']);
$message = 'Hello World';
?>
<div>
{{ $message }}
</div>
Обратите внимание, что мы должны размещать PHP-код внутри открывающих тегов <?php //здесь ?>
, и мы не можем использовать директиву blade, например:
@php
use function Laravel\Folio\middleware;
middleware(['auth']);
@endphp
Если использовать директиву, то middleware не будет работать, поскольку рендеринг blade шаблонов происходит гораздо позже, чем шаг проверки middleware. Поэтому при использовании директивы она будет определяться при рендеринге blade файла, что фактически ничего не даёт.
Как работает функция Middleware
Если мы рассмотрим функцию middleware
:
function middleware(Closure|string|array $middleware = []): PageOptions
{
Container::getInstance()->make(InlineMetadataInterceptor::class)->whenListening(
fn () => Metadata::instance()->middleware = Metadata::instance()->middleware->merge(Arr::wrap($middleware)),
);
return new PageOptions;
}
Она устанавливает замыкание в методе InlineMetadataInterceptor
whenListening
и передаёт ему замыкание, возвращающее массив middleware.
Обратите внимание, что Metadata::instance()
здесь является объектом-синглтоном. Однако он очищается после каждого запроса, обработанного Folio. Так что даже в Laravel Octane об этом можно не беспокоиться.
InlineMetadataInterceptor
Вернёмся к $matchedView->inlineMiddleware()
и посмотрим, чего она достигает:
public function inlineMiddleware(): Collection
{
return app(InlineMetadataInterceptor::class)->intercept($this)->middleware;
}
Теперь метод intercept
выполняет магические действия по разрешению промежуточного модуля ['auth']
, который мы определили в файле blade:
public function intercept(MatchedView $matchedView): Metadata
{
if (array_key_exists($matchedView->path, $this->cache)) {
return $this->cache[$matchedView->path];
}
try {
$this->listen(function () use ($matchedView) {
ob_start();
[$__path, $__variables] = [
$matchedView->path,
$matchedView->data,
];
(static function () use ($__path, $__variables) {
extract($__variables);
require $__path;
})();
});
} finally {
ob_get_clean();
$metadata = tap(Metadata::instance(), fn () => Metadata::flush());
}
return $this->cache[$matchedView->path] = $metadata;
}
Здесь происходит довольно много всего, но ключевой частью является статическое закрытие, которое выполняет blade-файл. По сути, он должен разрешить выполнение любого кода, определённого в открывающем и закрывающем PHP тегах. Таким образом, срабатывает что-то вроде функции middleware(['auth'])
.
И обратите внимание, что в строке 25 он получает экземпляр объекта metadata
и затем стирает его.
Вернёмся к __invoke
public function __invoke(Request $request, string $uri): mixed
{
$matchedView = (new Router(
$this->mountPath->path
))->match($request, $uri) ?? abort(404);
return (new Pipeline(app()))
->send($request)
->through($this->middleware($matchedView))
->then(function (Request $request) use ($matchedView) {
if ($this->onViewMatch) {
($this->onViewMatch)($matchedView);
}
return $this->renderUsing
? ($this->renderUsing)($request, $matchedView)
: $this->toResponse($matchedView);
});
}
После запуска middleware выполняется подготовка содержимого blade к отображению.
Заключение
Laravel Folio — впечатляющий пакет, который я бы, скорее всего, использовал для быстрого MVP и более простых проектов. Мне нравится его концепция и внутреннее устройство.