نحوه اجرا شدن یک Request در فرم ورک لاراول


فرم ورک لاراول یکی از محبوبترین فرم ورکهای زبان php میباشد، برای ساخت این فرم ورک از Design pattern ها و تکنولوژی های به روزی استفاده شده است . در این آموزش میخوام نحوه اجرای یک Request در این فرم ورک را شرح بدم.

فرض میکنیم که شما یک لینک را در آدرس بار تایپ کردید این لینک به این نام میباشد Http://laravel.app و این لینک اشاره میکند به HomeController 

Class HomeController{

    function getIndex(){
        return view("index");
    }

}


Route::controller("/", "HomeController");

 نحوه کار کردن این فرم ورک به این شکل میباشد که هر  http Request  را به صفحه index.php ارسال میکند، اینکار را با استفاده از دستورات htaccess انجام میدهد .

RewriteRule ^ index.php [L]

در صفحه index.php دو فایل باید include شوند اولین فایل autoload.php و دومین فایل app.php میباشد .

فایل autoload.php جهت include کردن تمامی فایلها و package هایی میباشد که در لاراول به آن نیاز داریم .

فایل app.php حاوی کد زیر میباشد 

$app = new Illuminate\Foundation\Application(
    realpath(__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;

در ابتدا لاراول یه نمونه جدیدی از کلاس application را ایجاد میکند. کلاس application در لاراول از کلاس container symphony ارث بری انجام میدهد، این کلاس (Application) عملیاتی مثل ثبت یک سرویس در service container و همچنین boot کردن اولیه framework را به عهده دارد. بعد از ایجاد یک نمونه جدید از کلاس application میتوانیم به متدهایی مثل singleton, bind, make و متدهای دیگری دسترسی داشته باشیم .

singleton یک design pattern میباشد که تنها هدف این pattern این است که در طول عمر یک request فقط و فقط یک نمونه از یک کلاس را ایجاد کند و هیچ نمونه اولیه دیگه ایی را ایجاد نکند .

 بعد از ایجاد یک نمونه جدید از کلاس Application لاراول با استفاده از متد singleton یک سرویس را در container ثبت میکند که این متد (singleton) حاوی 2 آرگومان ورودی میباشد، اولین آرگومان Interface یا Abstraction Class میباشد که به عنوان یک Implementaion از آن استفاده میشود و دومین آرگومان نام کلاسی است که لاراول میخواهد آن را در Container ثبت کند. همانطور که مشاهده میکنید 3 بار این اتفاق افتاده و در هر 3 بار 3 کلاس مختلف در Container ثبت شده اند .

  • اولین کلاس Kernel میباشد که جهت Handle کردن http Request میباشد
  •  دومین کلاس Console/kernel میباشد که جهت Handle کردن دستورات Console یا همون دستوران php  در Command line  میباشد
  •  سومین کلاس ExceptionHandler میباشد

بعد از ثبت همه این کلاسها در Container، یک نمونه جدیدی از کلاس Application در متغیر app ذخیره و سپس به فایل index.php برگردانده میشود .

سپس در فایل index.php متغیر app در دسترس میباشد ، ما با استفاده از دستور make میتوانیم یک service از container را resolve کنیم . که بعد از صدا زدن متد make و ارسال نام interface میتوانیم به object کرنل دسترسی داشته باشیم .

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

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

بعد از این که آبجکت Kernel از توی Container برگشت داده شد با استفاده از متد handle میتوان Request مورد نظر را Resolve  یا بازگشایی کرد .

در متد handle متد دیگری به نام sendRequestThroughRouter وجود دارد .

public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Exception $e) {
        .
        .
        .

که در این متد لاراول عملیاتی دیگری را انجام میدهد 

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());
    }

از متد instance برای جایگزین کردن سرویس جدید با سرویس قبلی که در container وجود دارد، استفاده میشود .

از متد استاتیک clearResolvedInstance جهت حذف دسترسی به کلاس Request از طریق facade میباشد .

از متد bootstrap برای راه اندازی کردن و پیکر بندی framework استفاده میشود .

به صورت خلاصه متد bootstrap ، همانطور که از اسمش مشخصه برای را اندازی مجدد کلاسهایی مثل  LoadConfiguration  DetectEnvironmentRegisterProvidersکه هر کدوم از این کلاسهای عملیات مخصوص به خودشون رو جهت load کردن و register کردن انجام میدهند .

بعد از bootstrap کردن تمامی کلاسها ، لاراول یک کلاس به نام routeServiceProvider را بوت میکند که کار این کلاس در ورژن 5.3 اجرای تمامی route های موجود در فایل web.php میباشد . و سپس بعد از اجرا شدن تک تک route ها ، تک تک route ها مقادیر و url ها خود را کلاسی به نام routeCollection و خصوصیتی به نام routes ثبت میکنند ، بعدا لاراول با استفاده از RouteCollection میتواند بفهمد که route مورد نظر در framework تعریف شده است یا نه.

سپس لاراول از کلاس Pipeline استفاده کرده 

Pipeline یک design pattern میباشد که یک پروسه طولانی را به چندین عملکرد تقسیم میکند، هر کدام از وظایف به خودی خود میتوانند به چندین وظیفه دیگر تقسیم شوند. این Design pattern میتواند در reusability کمک زیادی انجام دهد .

در خط آخر لاراول از متد dispatchToRouter استفاده میکند که کدی که در این متد وجود دارد به این شکل میباشد .

 protected function dispatchToRouter()
    {
        return function ($request) {
            $this->app->instance('request', $request);

            return $this->router->dispatch($request);
        };
    }

دوباره در خط آخر از متد dispatch استفاده کرده که request را به آن متد ارسال میکند .

 public function dispatch(Request $request)
    {
        $this->currentRequest = $request;

        return $this->dispatchToRoute($request);
    }

در خط آخر لاراول از متد dispatchToRoute استفاده کرده که این متد شامل کد زیر میباشد ( این متد در کلاس router وجود دارد )

public function dispatchToRoute(Request $request)
    {
     
        $route = $this->findRoute($request);

        $request->setRouteResolver(function () use ($route) {
            return $route;
        });

        $this->events->fire(new Events\RouteMatched($route, $request));

        $response = $this->runRouteWithinStack($route, $request);


        return $this->prepareResponse($request, $response);
    }

اولین خط، لاراول مشخص میکند که آیا route مورد نظر در collection routes وجود دارد یا نه، اگر وجود داشت یک آبجکت از نوع route را بر میگرداند در غیر اینصورت از کلاس MethodNotAllowedHttpException جهت نمایش هشدار استفاده میکند .

از متد setRouteResolver جهت پر کردن routeResolver با استفاد از متغیر route میباشد .

از متد fire جهت register کردن یک event و صدا زدن تمامی listener های مربوط به آن است .

متد runRouteWithinStack حاوی کد زیر میباشد.

protected function runRouteWithinStack(Route $route, Request $request)
    {
        $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                                $this->container->make('middleware.disable') === true;

        $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

        return (new Pipeline($this->container))
                        ->send($request)
                        ->through($middleware)
                        ->then(function ($request) use ($route) {
                            return $this->prepareResponse(
                                $request, $route->run($request)
                            );
                        });
    }

در خط اول متد بالا لاراول با استفاده از متد bound از وجود داشتن این سرویس (middleware.disable) اطمینان حاصل میکند، که آیا این سرویس وجود دارد یا نه، در صورت وجود نداشتن خروجی null میباشد. در خط بعدی در صورت null بودن متغیر shouldSkipMiddleware از متد gatherRouteMiddleware استفاده شده است .

gatherRouteMiddleware همانطور که از اسم این متد مشخص است جهت بدست آوردن تمامی middleware های میباشد که در مسیر اجرای این request وجود دارند .

در خط بعدی از کلاس pipeline استفاده شده، که در صورت pass شدن request از middleware متد then اجرا میشود و سپس متد run که دومین آرگومان ورودی متد prepare response میباشد اجرا میشود .

محتوای متد run به صورت زیر میباشد .

public function run(Request $request)
    {
        $this->container = $this->container ?: new Container;

        try {

            if ($this->isControllerAction()) {
                return $this->runController();
            }

            return $this->runCallable();
        } catch (HttpResponseException $e) {
            return $e->getResponse();
        }
    }

در این متد در قسمت try ، لاراول از متد isControllerAction استفاده میکند، با استفاده از این متد لاراول پی میبرد که آیا request مورد نظر یک action در یک controller میباشد یا یک inline route میباشد .

در صورتی که request یک action باشد متد runController اجرا میشود. که این متد حاوی کد زیر میباشد .

 protected function runController()
    {
        return (new ControllerDispatcher($this->container))->dispatch(
            $this, $this->getController(), $this->getControllerMethod()
        );
    }

لاراول برای resolve کردن این کنترلر یک instance جدید از کلاس ControllerDispatcher ایجاد میکند

controller dispatcher جهت resolve کردن وابستگیهای مربوط به یک متد وهچنین صدا زدن action میباشد که آن را در url خواستار شدیم .

برای صدا زدن متد disptach میبایست 3 مقدار را به این متد ارسال کنیم که، this اشاره میکند به کلاس Route و متد

getController جهت resolve کردن کلاس HomeController و همچنین resolve کردن وابستگی های موجود در Constructor این کلاس میباشد .

در صورتی که از Container لاراول استفاده کنیم، و نام یک کلاس را به متد make ارسال کنیم ، در صورتی که نام این سرویس در Container وجود نداشته باشد لاراول بر اساس نام کلاس آن را در Container ثبت و سپس وابستگیهای مربوط به آن را resolve میکند .

getControllerMethod جهت بدست آوردن نام متدی است که لاراول میخواهد آن را صدا بزند .

محتوای متد getControllerMethod شامل کد زیر میباشد 

protected function getControllerMethod()
    {
        return explode('@', $this->action['uses'])[1];
    }

در کد بالا در خصوصیت action یک مقداری به نام uses ذخیره شده است، به صورت اتوماتیک وقتی کلاس Route ایجاد شود در constructor خود با استفاده از دستور parsAction یک آرایه را در خصوصیت action ذخیره میکند.

"middleware" => "web"
"uses" => "App\Http\Controllers\HomeController@getIndex"
"controller" => "App\Http\Controllers\HomeController@getIndex"
"namespace" => "App\Http\Controllers"
"prefix" => null
"where" => []
"as" => "/"

مقداری که در خصوصیت action ذخیره میشود در کد بالا قابل مشاهده میباشد .

حالا بر میگردیم به کلاس controllerDispatcher ، محتوای متد dispatch که در کلاس controllerDispatcher وجود دارد به شکل زیر میباشد .

 public function dispatch(Route $route, $controller, $method)
    {
        $parameters = $this->resolveClassMethodDependencies(
            $route->parametersWithoutNulls(), $controller, $method
        );

        if (method_exists($controller, 'callAction')) {
            return $controller->callAction($method, $parameters);
        }

        return call_user_func_array([$controller, $method], $parameters);
    }

در متد بالا در خط اول، لاراول از متدی به نام resolveClassMethodDependencies استفاده کرده است که از این متد جهت resolve کردن یا تفکیک کردن پارامترهای ورودی یک action در کنترلر استفاده میشود .

در خط بعد از تابع method_exists استفاده کرده است که با این تابع مشخص میکند آیا متد callAction در این کنترلر وجود دارد یا خیر. 

callAction یک متد میباشد که در کلاس baseController وجود دارد . لاراول با صدا زدن این متد، تابع

call_user_func_array([$this, $method], $parameters);

با مقادیر getIndex به عنوان متد و کلاس HomeController به عنوان کلاس و پارامترهای resolve شده صدا زده میشود .

در صورتی که متد callAction وجود نداشت لاراول از همان تابع موجود در متد callAction بعد از دستور شرطی method_exists استفاده میکند .

در صورتی که url درخواستی یک action در یک controller نبود باید به صورت inline route صدا زده شود، که این عمل با استفاده از متد runCallable انجام میشود .

کد موجود در این متد به شکل زیر میباشد .

 protected function runCallable()
    {
        $parameters = $this->resolveMethodDependencies(
            $this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses'])
        );

        $callable = $this->action['uses'];

        return $callable(...array_values($parameters));
    }

همانطور که از کد بالا مشخص است لاراول با استفاده از متد resolveMethodDependencies وابستگیهای مربوط به این route را بازگشایی میکند . سپس با استفاده از مقدار موجود در uses و صدا زدن آن به صورت یک clouser ، خروجی مربوط به یک route را بر میگرداند .

خوب لاراول بعد از صدا زدن متد یک کنترلر و یا inline route مقدار موجود در آن را بر میگرداند ، ما در متد getIndex از تابع view استفاده کردیم که صفحه home.blade را بر میگرداند . بعد از صدا زدن متد موجود در کنترلر و یا clouser مربوط به یک route مقادیر از متد run در کلاس Route برگشت داده میشوند . سپس متد prepare response که در متد then وجود دارد صدا زده میشود . (then یکی از متدهای کلاس pipeline میباشد).

متد prepareResponse حاوی کد زیر میباشد .

 public function prepareResponse($request, $response)
    {

        if ($response instanceof PsrResponseInterface) {
            $response = (new HttpFoundationFactory)->createResponse($response);
        } elseif (! $response instanceof SymfonyResponse) {
            $response = new Response($response);
        }

        return $response->prepare($request);
    }

 در صورتی که response مورد نظر از نوع symphonyResponse نباشد، محتوای response را به constructor کلاس response ارسال میشود تا یک نمونه از نوع symphonyResponse جهت بازگشت ایجاد شود و سپس با صدا زدن متد prepare تنضیماتی از قبیل Content-Length, http version , Transfer-Encoding , Content-Type را عملی میکند. سپس لاراول مقدار response ایجاد شده را به متد dispatchToRoute برگشت میدهد، سپس دوباره عملیات prepareResponse انجام شده و مقدار بازگشتی به متد dispatch برگشت داده میشود و سپس به متد dispatchToRouter و سپس به متد sendRequestThroughRouter و سپس به متد handle و سپس به صفحه index.php بعد از handle کردن request متد send از کلاس symphonyResponse صدا زده میشود . که کدی که در این متد وجود داره به این شکل میباشد .

 public function send()
    {
        $this->sendHeaders();
        $this->sendContent();

        if (function_exists('fastcgi_finish_request')) {
            fastcgi_finish_request();
        } elseif ('cli' !== PHP_SAPI) {
            static::closeOutputBuffers(0, true);
        }

        return $this;
    }

در متد sendHeaders لاراول مشخص میکند که آیا httpHeader ارسال شده است یا خیر، در صورت ارسال نشدن header لاراول httpHeader را دوباره ارسال میکند .

httpHeader که در php به عنوان تابع header قابل استفاده میباشد. میتوانیم با استفاده از این تابع نوع خروجی به صفحه را مشخص کنیم که آیا خروجی از نوع html باشد یا از نوع pdf یا img و یا فایل دانلودی باشد و متد sendContent جهت ست کردن محتوای یک صفحه میباشد .

fastcgi_finish_request این تابع جهت به پایان رساندن request و همچنین ارسال response به صفحه خروجی میباشد .

php هم میتواند از طریق http request اجرا شود و هم میتواند از طریق cli یا همون command line اجرا شود، اینکه php از چه محیطی در حال اجرا میباشد را میتوان با استفاده از ثابت php_sapi تشخیص داد.

بعد از بازگشت this که همون symphony response میباشد از متد terminate در کلاس kernel استفاده میکنیم . از این متد جهت ریست کردن مقادیری که در request , response  وجود دارد استفاده میشود .

در عکس پایین میتونیم متوجه بشیم که ارتباط بین service ها مختلف این چرخه عملیاتی به چه صورت میباشد .

 امیدوارم با این آموزش اطلاعات جدیدی یاد گرفته باشید .

مشکل ، مورد ، نظر ، ایده هر سوالی که به ذهنتون در مورد این آموزش میرسه لطفاً بیان کنید