تیلور (Taylor) به تازگی نسخه بتا Laravel Folio را منتشر کرده است. در حال حاضر، این پکیج فقط شامل یک فایل readme است که سادگی آن را نشان می دهد. با توجه به ذات ساده آن، میتوانیم به راحتی نحوه کارکرد داخلی آن را حدس بزنیم. من تصمیم گرفتهام که به طور عمیق این پکیج را بررسی کنم. در این سکان پلاس، همراه من سفر کنید تا مکانیسمهای داخلی آن را کشف کنیم.
Laravel Folio چیست؟
به زبان ساده، Laravel Folio یک سیستم مسیردهی (routing) بر پایه صفحه برای برنامه Laravel شماست. تنها کاری که باید انجام دهید، ایجاد یک فایل blade است. نیازی به نوشتن مسیرها یا ایجاد متدهای کنترلر برای بازگرداندن view نیست. همانطور که اشاره شد، این کار بسیار آسان است.
چگونه از آن استفاده کنیم؟
در اینجا به جزئیات استفاده از Folio نخواهم پرداخت ، زیرا همانند هر پکیج رسمی Laravel دیگر، نصب و اجرای دستورات آن تنها حدود 5 دقیقه زمان میبرد. همچنین به خاطر داشته باشید که این پکیج هنوز در مرحله بتا قرار دارد. به این معنا که قبل از انتشار نسخه 1 ممکن است تغییرات قابل توجهی در آن اعمال شود.
با این حال، میتوانم شما را به فایل LaravelFolioServiceProvider در مسیر app/Providers هدایت کنم، جایی که پس از نصب، کد زیر را در متد boot آن خواهید یافت:
public function boot(): void
{
Folio::route(resource_path('views/pages'), middleware: [
'*' => [
//
],
]);
}
کمی بعدتر به این که متد route چه کارمی کند میپردازیم. اما در نگاه اول، واضح است که این کد تمام فایلهای مسیر view/pages را اسکن میکند.
نگاه نزدیک به Folio
قبل از اینکه به سورس کدهای Folio بپردازیم، فکر میکنم مفیدتر است به شما نشان دهم که چگونه میتوانیم یک نسخه ساده از آن را خودمان ایجاد کنیم. بیایید با همکاری یکدیگر، یک نسخه ابتدایی از Folio پیادهسازی کنیم.
پیاده سازی یک نسخه ابتدایی از Folio
اولین مرحله، ایجاد یک فایل blade با نام profile.blade.php در مسیر resources/views/pages است. این فایل فقط یک رشته '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 مسیری با آدرس {something} ایجاد میکند که view('pages/{something}') را بر می گرداند. مانند یک متد create عادی در کنترلر ها. حالا وقتی به localhost/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 اضافه میشود. این به ما نشان می دهد که اگر view های ما در انواع دایرکتوریها پخش شده باشند، میتوانیم متد route را به دفعات دلخواه اجرا کنیم.
پس از ایجاد شیء MountPath، ابتدا بررسی میکند که آیا URI به صفحه اصلی برنامه ما اشاره دارد یا خیر. اگر اشاره داشته باشد، یک handler را به عنوان fallback برای مسیر ثبت میکند. اگر اشاره نکند، یک روت با مسیر تعیین شده تعریف میکند و همان handler را برای آن استفاده می کند.
احتمالاً برای صفحه اصلی خود از 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 یک Closure برمیگرداند که زمانی که مسیریاب لاراول یک درخواست جدید را پردازش میکند، فراخوانی میشود.
حالا، بیایید به بررسی کلاس RequestHandler بپردازیم.
class RequestHandler
{
/**
* Create a new request handler instance.
*/
public function __construct(protected MountPath $mountPath,
protected ?Closure $renderUsing = null,
protected ?Closure $onViewMatch = null)
{
}
/**
* Handle the incoming request using Folio.
*/
public function __invoke(Request $request, string $uri): mixed
{
}
/**
* Get the middleware that should be applied to the matched view.
*/
protected function middleware(MatchedView $matchedView): array
{
}
/**
* Create a response instance for the given matched view.
*/
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);
});
}
متد با پیدا کردن یک view مطابق شروع به کار میکند، که به یک فایل blade ارجاع دارد. سپس یک پایپ لاین (pipeline) را ساخته تا وظایف زیر را انجام دهد:
- درخواست را به لیست middlewares ارسال کند
- اگر onViewMatch callback تعریف شده باشد، آن را اجرا کند
- view را رندر کند
حالا، بیایید به متد $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() را بررسی کنیم.
middleware های inline در Folio
Folio به شما اجازه میدهد که middleware را مستقیماً در داخل فایل blade خود تعیین کنید. در ادامه، یک مثال آورده شده است:
<?php
use function Laravel\Folio\middleware;
middleware(['auth']);
$message = 'Hello World';
?>
<div>
{{ $message }}
</div>
در این مثال، میدل ویر auth برای بررسی اجازهی دسترسی کاربر به محتوای استفاده شده است. اگر کاربر لاگین نکرده باشد، به صفحهی ورود هدایت میشود، در غیر اینصورت محتوا به او نمایش داده میشود.
این قابلیت بسیار قدرتمندی است که به شما اجازه میدهد که قوانین middleware را مستقیماً در داخل فایلهای blade تعیین کنید و به راحتی بررسیهای لازم برای مسیرها را انجام دهید.
توجه کنید که باید کد PHP را در داخل تگهای باز <?php //here?> قرار دهیم و نمیتوانیم از دستورالعمل blade مانند:
@php
use function Laravel\Folio\middleware;
middleware(['auth']);
@endphp
استفاده کنیم. اگر از دستورالعمل های blade استفاده کنید، middleware کار نخواهد کرد زیرا رندر کردن قالبهای blade بعد از مرحله بررسی middleware انجام میشود. بنابراین تابع 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;
}
یک Closure را درون متد whenListening از کلاس InlineMetadataInterceptor قرار میدهد که آرایهای از middleware ها را برمیگرداند.
توجه کنید که Metadata::instance() در اینجا یک شیی Singleton است. با این حال، بعد از هر درخواستی که توسط Folio پردازش میشود، پاک میشود. بنابراین حتی با استفاده از Laravel Octane، نیازی به نگرانی در این باره نیست.
InlineMetadataInterceptor
حالا به $matchedView->inlineMiddleware() بازگردید و ببینید که چه کاری انجام میدهد:
public function inlineMiddleware(): Collection
{
return app(InlineMetadataInterceptor::class)->intercept($this)->middleware;
}
حالا متد intercept کار جادویی را انجام میدهد تا میدل ویر ['auth'] که در فایل blade تعریف کردهایم، resolve شود:
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;
}
در اینجا چیزهای زیادی اتفاق میافتد، اما قسمت کلیدی، Closure استاتیک است که فایل blade را اجرا میکند. این عملیات به طور اصولی فایل را require میکند تا اجازه دهد هر کدی که در داخل تگهای باز و بسته PHP تعریف شده است، اجرا شود. بنابراین، به عنوان مثال، تابع middleware(['auth']) اجرا میشود.
توجه کنید که در خط ۲۵، یک نمونه از شی Metadata را دریافت میکند و سپس آن را پاک میکند.
بازگشت به __invoke
حالا به __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 سریع و پروژههای سادهتر از آن استفاده کنم. من مفهوم آن و نحوهی عملکرد داخلی آن را دوست دارم.