بررسی پشت صحنه لاراول برای درک عمیق تر - قسمت 2

تو پست قبلی اگه یادتون باشه جلسه قبل فقط یه Application از جنس Laravel ساختیم که بوت نشده بود و فقط چنتا Service Provider و Alias رو انداخته بودیم (register کردیم) داخل نمونه Application ساخته شده و البته کمی درباره Container و DI صحبت کردیم. امروز این بحث ها رو ادامه میدیم و وارد مفاهیم Kernel یا همون هسته پردازش درخواست ها توی Laravel میشیم (البته فقط خود هسته رو میبینیم و پست های بعدی درباره Routing و خود عملیات پردازش میشیم)

حالا قبل اینکه وارد بحث فنی بشیم چنتا نکته مهم بگم:

  • نسخه ای که ازش توی توضیحات استفاده میکنم نسخه 7.28.0 هستش. (آخرین نسخه در زمان این پست)
  • توضیحات سعی میکنم طوری باشه که برای کسی هم که Laravel بلد نیست قابل فهم باشه (هر چند لازمه شاید چند بار مطالب خونده بشه). اگه هم موردی بود که متوجه نشدین توی کامنت ها بپرسین، با هم دیگه سعی میکنیم به جواب برسیم. مثل هر مطلب علمی در صورتی که ایراد فنی نیز داشت خوشحال میشم برطرف کنیم.
  • شاید برخی مطالب رو توی پاورقی آخر این پست و یا پست های بعدی توضیح میدم و لینکشون رو همین پست آپدیت میکنم.

رجیستر کردن Kernel

خط کدی که جلسه قبل بررسی میکردیم، این قسمت پایین از فایل bootstrap\app.php بود که توش فقط یه Application با ورودی مسیر کدهای برنامه بود.

<?php

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

حالا تو ادامه کدهای این فایل این قسمت رو داریم که موضوع بحث اصلی امروز از اینجا شروع میشه:

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

return $app;

ما اینجا اومدیم دو تا Kernel یکی Http و یکی هم Console داخل Container گذاشتیم. این Kernel ها وظیفه مدیریت درخواست های ارسال شده به Laravel رو دارن. فرقشون چیه؟

  • کرنل HTTP: وقتی درخواست از طرف کلاینت میاد سمت برنامه، یا به عبارتی از طریق route، این کرنل وظیفه مدیریت اونو داره.
  • کرنل Console: وقتی که درخواست از طرف محیط کنسول یا همون Artisan بدیم، این کرنل مدیریت میکنه.

بحث ما امروز البته درباره HTTP Kernel هست.

پایین هم Exception Handler رو اضافه کرده که بحث ما نیست الان. آخرشم نمونه Application به همراه Bindingها رو برمیگردونه و داخل متغیر app$ توی public/index.php میزاره.

قبل جلو رفتن اگه بخواین یکم فنی تر DI در بحث بالا رو باز کنیم پینوشت اول رو پایین همین پست بخونین.

ساختن Kernel

تا اینجا کار ما فقط دو تا Kernel اضافه کردیم به Application و عملا باز اتفاق خاصی نیافتاده و حتی خود Kernelها هم نمونه سازی نشدن. خط بعدی که توی فایل public/index.php اجرا میشه این دستوره:

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

اینجا در واقع میایم یه نمونه از Kernel رو به کمک Container میسازیم. همونجور که بالا گفتیم وقتی که بخوایم این نمونه رو بسازیم، Container میاد در واقع کلاس App\Http\Kernel رو برامون میسازه. خب اگه فایل App\Http\Kernel رو باز کنیم میبینیم که در نگاه اول تقریبا محتوای زیادی نداره. کل فایل رو با حذف کامنت ها و مقادیر داخل آرایه ببینیم یه چیز تو این مایه هاست:

<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
    protected $middleware = [ /*  .... */ ];
    protected $middlewareGroups = [ /*  .... */ ];
    protected $routeMiddleware = [ /*  .... */ ];
}

تو نگاه اول یه چنتا middleware و grouping اونا هست که معمولا اونایی که درگیر مثلا اضافه کردن یه middleware جدید به برنامه خودشون شدن این فایل و این آرایه ها یه سری زدن و تغییر دادن. چیزی که اینجا مهمه اینه که این کلاس در واقع میاد از Illuminate\Foundation\Http\Kernel ارث بری میکنه. در واقع هر اتفاقی که توی HTTP Kernel میافته اونجا پیاده سازی شده. اگه تابع سازنده این رو ببینیم کدهای پایین رو داریم:

public function __construct(Application $app, Router $router)
{
    $this->app = $app;
    $this->router = $router;
    $this->syncMiddlewareToRouter();
}

خب سازنده 2 پارامتر ورودی داره ولی بالا وقتی ما make کردیم چیزی نفرستادیم، اینا از کجا اومد؟ جواب این سوال در واقع ساده هست، از طریق مکانیزم Dependency Injection که تو Laravel داریم، خود این کلاس ها اتوماتیک نمونه سازی میشن و به سازنده پاس داده میشن (اصطلاح فنی میگن constructor injection). توجه کنین که قسمت قبلی یادتون باشه اونجا ما Application و Router رو داخل Container گذاشته بودیم.

چجوری این constructor injection تو Laravel کار میکنه؟ پینوشت دوم پایین همین پست رو بخونید یکم توضیح میدم.

خب دو خط اول ساده هست و اینجا وقتی constructor injection انجام شد، در واقع نمونه کلاس Application که قبلا ساخته بودیم رو برمیگردونه (یادتون باشه singleton بود)، ولی یه Router از مسیر Illuminate\Routing\Router میسازه میاره و به متغیرهای داخل کلاس میذاره.

حالا سازنده Router رو نگاه کنیم ببینیم چی داریم:

public function __construct(Dispatcher $events, Container $container = null)
{
    $this->events = $events;
    $this->routes = new RouteCollection;
    $this->container = $container ?: new Container;
}

سازنده Router رو نگاه کنیم، عملا کار خاصی نمیکنه و دقت کنین اینجا هم constructor injection داریم که قبلا داخل Container گذاشته بودیم و الان اونا رو برمیداره. مشخصه دیگه چرا، ما الان داخل کلاس Router هستیم و اونم نیاز داره به event dispatcher و container دسترسی داشته باشه، ما هم میایم بهش اونا رو میدیم. علاوه بر این Router باید مجموعه ای Route های مارو ذخیره کنه. برا همین یه RouteCollection جدید میسازیم و میدیم بهش. دقت کنین که هنوز مسیرای داخل فایل route.php لود نشدن.

خط آخر سازنده Kernel رو نگاه کنیم یه تابع دیگه به اسم syncMiddlewareToRouter فراخوانی میکنه که محتواش این پایین هست:

protected function syncMiddlewareToRouter()
{
    $this->router->middlewarePriority = $this->middlewarePriority;

    foreach ($this->middlewareGroups as $key => $middleware) {
        $this->router->middlewareGroup($key, $middleware);
    }
    foreach ($this->routeMiddleware as $key => $middleware) {
        $this->router->aliasMiddleware($key, $middleware);
    }
}

تو این تابع ما در واقع میایم اطلاعات مربوط به middlewareها که داخل App\Http\Kernel هست رو در اختیار Router که ساخته بودیم میذاریم. اسم تابع هم معنیش رو میرسونه (sync middleware to router). در واقع router باید بتونه middleware ها رو روی route ها اعمال کنه و باید اونا رو به همراه اولویتشون داشته باشه.

خب تا اینجا ما یه Http Kernel رو اول داخل Container اومدیم bind کردیم و بعدش هم ازش نمونه ساختیم. حالا که پیش زمینه ها آماده شده تا درخواست کلاینت رو جواب بدیم.

دریافت درخواست کلاینت

خط بعد فایل index.php اینه:

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

اون Kernel که بالا ساختیم میایم تابع handle اونو فراخوانی میکنیم و به عنوان پارامتر ورودی نتیجه تابع استاتیک capture داخل کلاس Illuminate\Http\Request رو پاس میدیم.

خب باز کنیم این قسمت رو ببینیم چی شد.

همه میدونیم که ابتدا عبارت داخل پرانتز پردازش میشه و بعد قسمت بیرونی. یعنی اول اون قسمت capture اجرا میشه و بعد که نتیجه تولید شد تابع handle با نتیجه اون اجرا میشه. حالا بریم capture رو ببینیم.

تابع capture یه تابع استاتیک با کد پایین هست:

public static function capture()
{
    static::enableHttpMethodParameterOverride();
    return static::createFromBase(SymfonyRequest::createFromGlobals());
}

خط اول رو نگاه کنیم یه تابع استاتیک از خود کلاس فراخوانی میکنه. این تابع میاد قابلیت method override رو برای ما فعال میکنه. حالا این قابلیت چیه؟ اگه دقت کرده باشین توی Route ما میتونیم از put, delete و ... استفاده کنیم. ولی در پشت صحنه در عمل Laravel از POST استفاده میکنه (به دلایل امنیتی همیشه توی وب سرورها فقط get, head, post فعال میکنن بقیه غیر فعاله). حالا چجوری Laravel میفهمه درخواستی که اومده از چه نوعیه؟ خب توی پارامترایی که توی درخواست میفرستیم مشخص میکنیم (پارامتر method_). مثلا توی فرم میایم از دستور Blade مثل method@ استفاده میکنیم و میگیم که فرم ارسالی از چه نوعی باشه. به این قابلیت اصطلاحا میگیم method override که تو این خط از کد فعال میکنیم اینو.

بریم خط بعد که مهمه. این خط میاد در واقع به کمک متغیرهای super global توی PHP استفاده میکنه و یه درخواست از نوع SymfonyRequestمیسازه. super global ها همون مقادیر پارامترهای SERVER، GET, POST COOKIE_$ و ... هستش. Laravel هم میاد از Request چارچوب Symfony استفاده میکنه (مزایای دنیای open source :D). حالا اگه بیایم این request که ساخته شده رو dd کنیم یه چیزی شبیه پایین میاد تو خروجی (یکم از پارامترها حذف کردم).

Illuminate\Http\Request {#43 ▼
  #json: null
  +server: Symfony\Component\HttpFoundation\ServerBag {#47 ▼
    #parameters: array:27 [▼
      &quotSERVER_SOFTWARE&quot => &quotPHP 7.3.10 Development Server&quot
      &quotSERVER_NAME&quot => &quot127.0.0.1&quot
      &quotREQUEST_URI&quot => &quot/&quot
      &quotREQUEST_METHOD&quot => &quotGET&quot
    ]
  }
  +headers: Symfony\Component\HttpFoundation\HeaderBag {#49 ▼
    #headers: array:13 [▼
      &quotuser-agent&quot => array:1 [▶]
    ]
  }
  method: &quotGET&quot
  format: &quothtml&quot
}

بررسی Handle از Kernel

حالا اگه یادتون باشه ما باید این request ساخته شده بالا رو به تابع handle از Kernel بفرستیم. خب تابع handle رو باز کنیم ببینیم چی داره.

public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();
        $response = $this->sendRequestThroughRouter($request);
    } catch (Throwable $e) {
        $this->reportException($e);
        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new RequestHandled($request, $response)
    );

    return $response;
} 

خط اولش که آشناس. میاد method override رو فعال کنه. چرا دوباره نوشته اینو؟ نمیدونم شاید واسه محکم کاری بوده که اگه به هر دلیلی بعد از فعال کردنش، کتابخونه symfony عوض بشه و بیاد دستکاری کنه اونو دوباره ما اینجا فعالش کنیم (big maybe)! بعد میایم تابع sendRequestThroughRouter رو فراخوانی کرده و request رو پاس میدیم بهش و اونم برای ما response تولید میکنه. اگه هم مشکلی پیش بیاد در این حین از مکانیزم exception handling میاد استفاده میکنه که درگیر نمیشیم. در نهایت هم میاد event مربوط به تموم شدن کارای request رو dispatch میکنیم و response هم بر میگردونیم.

توی کد بالا بعدی تابع sendRequestThroughRouter برای ما زیاد مهمه چون عملا کارای مربوط به routing و middleware و ... اینجا انجام میشه توی request. حالا خود تابع رو ببینیم.

protected function sendRequestThroughRouter($request)
{
    $this->app->instance('request', $request);
    Facade::clearResolvedInstance('request');
    $this->bootstrap();

    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

خط اول میاد این request دریافتی رو میزاره داخل Container که بعدا هر جای برنامه که لازمش شد بتونه ازش استفاده کنه و خط بعدی هم میاد تا الان اگه نمونه قبلی از request توی Container باشه رو پاک میکنه. بعد میایم تابع bootstrap رو اجرا میکنیم. این تابع همونجور که از اسمش مشخصه میاد Laravel رو میاره بالا. یعنی چی؟ ببینیم کدش رو میفهمیم:

public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }
}

اگه جلسه قبلی یادتون باشه همیشه وقتی Application رو dd میکردیم، مقدار isBootstraped برابر false بود. داخل این شرط هم اول چک میکنه که برنامه بوت نشده باشه. بعد میاد تابع bootstrapWith از کلاس Application رو با پارامتر ورودی آرایه bootstrappers که داخل Kernel قرار داره فراخوانی میکنه. تابع bootstrapWith کار خاصی نمیکنه لیستی از bootstrapper که میگیره میاد برای اونا اول یه event اینکه در حال بوت شدن رو میده بعد یه نمونه از اونو میسازه و تابع bootstrap داخلشو فراخوانی میکنه و در نهایت هم یه event اینکه بوت شد و تمام شد میده.

public function bootstrapWith(array $bootstrappers)
{
    $this->hasBeenBootstrapped = true;
    foreach ($bootstrappers as $bootstrapper) {
        $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);
        $this->make($bootstrapper)->bootstrap($this);
        $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
    }
}

خوبه که به آرایه bootstrapper نگاه کنیم و یکم حرف بزنیم:

protected $bootstrappers = [
    \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
    \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
    \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
    \Illuminate\Foundation\Bootstrap\BootProviders::class,
];

دقت کنین به اسامی چیزای جالبی دیده میشه. اینجا عملا قسمت های اصلی برنامه بوت میشه و بالا میاد. یکی یکی اگه ببینیم:

  • کلاس LoadEnvironmentVariables: این میاد محتویات فایل env. رو لود میکنه.
  • کلاس LoadConfiguration: میاد تنظیمات موجود در مسیر config رو میخونه و لود میکنه.
  • کلاس HandleExceptions: که برای مدیریت exception ها هست بالا میاد.
  • کلاس RegisterFacades: این میاد Facadeها (مثلا Auth, DB و ...) رو رجیستر میکنه تو Container.
  • کلاس RegisterProviders: این میاد تابع همه service provider که تو برنامه ما هست رو ایجاد میکنه و تابع register اونا رو فراخوانی میکنه.
  • کلاس BootProviders: این میاد تابع boot همه service provider ما رو فراخوانی میکنه.
این دو تا آخری رو ببینین متوجه میشین فرق register با boot توی service provider رو. اول تابع register همه service providerها فراخوانی میشه و بعد میاد boot همه رو فراخوانی میکنه.

خب برای این پست تا همینجا کافیه و خط بعدی برنامه تو تابع sendRequestThroughRouter وارد فاز routing میشه نگه میداریم برای جلسه بعد. این پست رو خلاصه کنیم میتونیم بگیم که:

  • با نحوه register و resolve شدن Kernelهای Http و Console آشنا شدیم.
  • دیدیم Laravel چجوری میاد درخواست کلاینت رو Capture میکنه و بعدش ازش کلاس Request میسازه.
  • درباره مکانیزم Method Override هم آشنا شدیم.
  • بعد دیدیم که چجوری Request رو همراه با middleware میفرسته به Router.
  • فهمیدیم که قابلیت های اصلی Laravel مثل env. و config و providerها و facadeها کی ساخته میشن.
پست بعدی درباره Routing صحبت میکنیم.

پینوشت اول - Dependency Injection

داخل کد فایل app.php یه کدی مثل پایین دیدیم:

$app->singleton(Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);

این خط ها در واقع Kernel رو داخل Container ما به صورت singleton میاد bind میکنه. حالا این یعنی چی؟ قسمت قبلی درباره مفاهیم Dependency Injection توی Container یکم صحبت کردیم و گفتیم که مثل get و set توی کلاس کار میکنن؛ یعنی شما داخل Container یه چیزی میزاری، بعدا که لازمت شد اونو میخوای برداری Laravel برات میاد یه نمونه از اون رو میسازه میده؛ اصطلاحا یه ترکیب از Contract و Concrete اینجا داریم. اینجا چجوری میشه؟ ما اینجا به Container میگیم، وقتی که بعدا ازت خواستم برامIlluminate\Contracts\Http\Kernel بسازی (Contract) ، کلاس App\Http\Kernel برام بساز (Concrete) بده (یا اگه قبلا ساخته شده همونو برگردون).

این که میگیم "اگه ساخته شده قبلا اونو برگردون" چجوری شد؟ چون ما اینجا singleton استفاده کردیم و توی کل برنامه فقط میخوایم یه دونه Kernel از هر نوع داشته باشیم؛ وقتی که یه چیز singleton رو دوبار بخوایم بسازیم، دفعه دوم همون قبلی رو برمیگردونه. (خوب متوجه نشدین این تیکه رو توضیحات Container قسمت قبلی یا مفاهیم Dependency Injection و interface binding رو بخونین)

پینوشت دوم - Constructor Injection

یکی از روش های Dependency Injection معروف هست به constructor injection. این جوریه که شما کلاس هایی که میخواین براتون نمونه سازی بشه به عنوان پارامتر ورودی تابع سازنده میدین. برای مثال یه کلاس که constructor injection داره ببینیم:

class Router {
    public function __construct(Application $app, Router $router) { ... }
}

وقتی بخوایم از این کلاس یه نمونه بسازیم مینویسیم:

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

دقت کنین برای نمونه سازی از تابع make داخل Application که همون Container ما هست استفاده کردیم. حالا چرا خودمون new نمیزنیم و میدیم Laravel این کار رو بکنه؟ واقعیت اینه که constructor injection به صورت پیش فرض توی PHP نیست و توی این برنامه خودمون ما باید از Laravel واسه این کار استفاده کنیم.

حالا خود Laravel از کجا میفهمه که تابع سازنده اون کلاس این پارامترها رو داره؟ در واقع اینجا Laravel میاد از یکی از قابلیت های خیلی باحال PHP به اسم ReflectionClass استفاده میکنه. این کتابخونه قابلیت های زیادی داره و به طور کلی شما میتونین همه چیزایی که داخل کلاس ها و توابع و ... هست رو بدون اجرا کردن اون بخونین و بررسی کنین. برای مثال ببینین که یه کلاس چه متغیرایی داره، چه توابعی داره، نوع متغیرها چین و ،چیزی که اینجا برای ما مهمه، پارامتر ورودی یه تابع چیا هستن.

به این صورت که Laravel موقع resolve کردن یک کلاس، به constructor نگاه میکنه ببینه پارامتر ورودی قابل resolve شدن داره یا نه. در صورتی که باشه میره اونم نمونه سازی و resolve میکنه. جالبیش اینه که حتی خود اون کلاس هم باز خودش constructor injection داشته باشه اونم resolve میکنی و همینجوری به صورت بازگشتی میاد بالا.


⇒ پست قبلی (Application Container)

منابع مطالعه بیشتر:

Laravel Doc: Service Container - Service Providers

Laravel Behind the Scenes: Lifecycle - Boot the framework

DI in Laravel : Dependency Injection in Laravel - Laravel&amp;amp;amp;amp;#x27;s Dependency Injection Container in Depth - Constructor Injection in Laravel