瞭解laravel的http請求過程(路由)

1,我們都知道 laravel 框架的入口是 public/index.php 文件,我們看一下源碼:

/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/

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

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

從源碼我們看到,是先從服務容器中解析出  Illuminate\Contracts\Http\Kernel::class 的服務實例,再執行服務的 handle 方法處理 HTTP 請求。

我們找到了 Illuminate\Foundation\Http\kernel::class 服務實例

<?php

namespace Illuminate\Foundation\Http;

use Exception;
use Throwable;
use Illuminate\Routing\Router;
use Illuminate\Routing\Pipeline;
use Illuminate\Support\Facades\Facade;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Http\Kernel as KernelContract;
use Symfony\Component\Debug\Exception\FatalThrowableError;

class Kernel implements KernelContract
{

    /**
     * Handle an incoming HTTP request. 處理 HTTP 請求
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php#L111
     * 
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

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

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

        return $response;
    }

    /**
     * Send the given request through the middleware / router. 將用戶請求發送到中間件和路由
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    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());
    }

    /**
     * Get the route dispatcher callback. 獲取分發路由回調(或者控制器)
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Http/Kernel.php#L171
     * @return \Closure
     */
    protected function dispatchToRouter()
    {
        return function ($request) {
            $this->app->instance('request', $request);

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

在 Kernel 的類中看到 handle 方法調用 sendRequestThroughRouter 方法

在這個方法中全局中間件運行完之後,會調用 dispatchToRouter 方法返回的回調方法,我們看一下這個方法:

當前類 Illuminate\Foundation\Http\Kernel 的 $router 屬性是 Illuminate\Routing\Router 類的對象,下面我們看
Illuminate\Routing\Router 類的 dispatch 方法,如下:

<?php

namespace Illuminate\Routing;

...

class Router implements RegistrarContract, BindingRegistrar
{
    ...
    /**
     * Dispatch the request to the application. 將 HTTP 請求分發到應用程序。
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
     */
    public function dispatch(Request $request)
    {
        $this->currentRequest = $request;

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

    /**
     * Dispatch the request to a route and return the response. 將請求分發到路由,並返回響應。
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    public function dispatchToRoute(Request $request)
    {
        return $this->runRoute($request, $this->findRoute($request));
    }

    /**
     * Find the route matching a given request. 查找與請求 request 匹配的路由。
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Routing\Route
     */
    protected function findRoute($request)
    {
        // 從 RouteCollection(由 Router::get('/', callback) 等設置的路由) 集合中查找與 $request uri 相匹配的路由配置。
        $this->current = $route = $this->routes->match($request);

        $this->container->instance(Route::class, $route);

        return $route;
    }

    /**
     * Return the response for the given route. 執行路由配置的閉包(或控制器)返回響應 $response。
     *
     * @param  Route  $route
     * @param  Request  $request
     * @return mixed
     */
    protected function runRoute(Request $request, Route $route)
    {
        $request->setRouteResolver(function () use ($route) {
            return $route;
        });

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

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

    /**
     * Run the given route within a Stack "onion" instance. 運行給定路由,會處理中間件等處理(這裏的中間件不同於 Kernel handle 中的路由,是僅適用當前路由或路由組的局部路由)。
     *
     * @param  \Illuminate\Routing\Route  $route
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    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(

                                // $route->run() 將運行當前路由閉包(或控制器)生成結果執行結果。
                                $request, $route->run()
                            );
                        });
    }

    /**
     * Create a response instance from the given value.
     *
     * @param  \Symfony\Component\HttpFoundation\Request  $request
     * @param  mixed  $response
     * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
     */
    public function prepareResponse($request, $response)
    {
        return static::toResponse($request, $response);
    }
    ...
}

繼續調用 dispatchToRoute 方法,如下:

我們今天的重點是 findRoute 方法,也就是尋找路由。

這裏的核心邏輯是調用 Illuminate\Routing\RouteCollection 的 match 方法匹配路由:

public function match(Request $request)
{
    $routes = $this->get($request->getMethod());

    // First, we will see if we can find a matching route for this current request
    // method. If we can, great, we can just return it so that it can be called
    // by the consumer. Otherwise we will check for routes with another verb.
    $route = $this->matchAgainstRoutes($routes, $request);

    if (! is_null($route)) {
        return $route->bind($request);
    }

    // If no route was found we will now check if a matching route is specified by
    // another HTTP verb. If it is we will need to throw a MethodNotAllowed and
    // inform the user agent of which HTTP verb it should use for this route.
    $others = $this->checkForAlternateVerbs($request);

    if (count($others) > 0) {
        return $this->getRouteForMethods($request, $others);
    }

    throw new NotFoundHttpException;
}

在上述方法定義中,首先通過 $this->get($request->getMethod()) 獲取當前請求方法(GET、POST等)下的所有路由定義,該方法返回結果是 Illuminate\Routing\Route 實例數組

接下來調用 $this->matchAgainstRoutes($routes, $request) 通過當前請求實例 $request 從返回的路由數組 $routes 中匹配路由:

protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
    [$fallbacks, $routes] = collect($routes)->partition(function ($route) {
        return $route->isFallback;
    });

    return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
        return $value->matches($request, $includingMethod);
    });
}

這個方法分爲 2 部分,第一部分把所有的 get 路由分爲 2 個容器,第一個容器只包含回退路由,第二個容器包含除去回退路由的所有 get 路由。第二部分把上面的 2 個容器合併,注意了此時的回退路由在所有 get 路由的的最後面。合併完之後遍歷容器中的每一個路由,找到第一個符合規則的路由,上面的 first 方法的每一個參數都是我們註冊的路由對象,Laravel 中的每一個路由對象都是 Illuminate\Routing\Route 類的實例

對應的路由匹配邏輯源碼則是 Illuminate\Routing\Route 的 matches 方法:

public function matches(Request $request, $includingMethod = true)
{
    $this->compileRoute();

    foreach ($this->getValidators() as $validator) {
        if (! $includingMethod && $validator instanceof MethodValidator) {
            continue;
        }

        if (! $validator->matches($this, $request)) {
            return false;
        }
    }

    return true;
}

在該方法中,會通過 $this->getValidators() 返回的四個維度的數據對當前請求進行匹配,分別是請求路徑URI、請求方法(GET、POST等)、Scheme(HTTP、HTTPS等) 和域名。這裏面應用了責任鏈模式,只要一個匹配校驗不通過,則退出校驗,只有所有四個維度數據校驗都通過了,纔算通過,具體每個維度數據校驗都是一個獨立的類來完成,感興趣的可以自己去看下,這裏就不深入展開了。

接下來,代碼控制流程回到 Illuminate\Routing\RouteCollection 的 match 方法,如果匹配到定義的路由,則返回路由信息:

if (! is_null($route)) {
    return $route->bind($request);
}

否則檢查下其它請求方式有沒有與當前請求匹配的路由,如果有的話拋出 MethodNotAllowed 異常:

$others = $this->checkForAlternateVerbs($request);

if (count($others) > 0) {
    return $this->getRouteForMethods($request, $others);
}

如果也沒有的話才拋出 NotFoundHttpException 異常,返回 404 響應。

處理路由業務邏輯

如果在路由匹配中找到了匹配路由,沒有拋出異常,則代碼控制流程進入執行路由階段,對應的方法是 Illuminate\Routing\Router 的 runRoute 方法:

protected function runRoute(Request $request, Route $route)
{
    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

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

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

該方法中第一段代碼將匹配到的路由設置到當前請求的路由解析器屬性中,然後觸發一個路由匹配事件RouteMatched,最後通過 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()
                        );
                    });
}

首先還是判斷系統是否禁用中間件,如果沒有的話從匹配路由實例 $route 中通過 gatherRouteMiddleware 方法解析出當前路由上應用的路由中間件,並通過管道方式執行,中間件校驗邏輯都通過之後,調用 prepareResponse 方法處理請求,準備返回響應,響應內容由 Illuminate\Routing\Route 的 run 方法返回:

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

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

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

在該方法中,會判斷如果路由是由控制方法定義,則執行對應的控制器方法並返回結果:

return $this->runController();

如果路由是通過閉包函數定義的話,則執行對應的閉包函數並返回處理結果:

return $this->runCallable();

不管採用那種方式定義,返回的響應內容都會經由前面提到的 prepareResponse 進行處理,最終通過 toResponse 方法進行處理後準備發送給客戶端瀏覽器

public static function toResponse($request, $response)
{
    if ($response instanceof Responsable) {
        $response = $response->toResponse($request);
    }

    if ($response instanceof PsrResponseInterface) {
        $response = (new HttpFoundationFactory)->createResponse($response);
    } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
        $response = new JsonResponse($response, 201);
    } elseif (! $response instanceof SymfonyResponse &&
               ($response instanceof Arrayable ||
                $response instanceof Jsonable ||
                $response instanceof ArrayObject ||
                $response instanceof JsonSerializable ||
                is_array($response))) {
        $response = new JsonResponse($response);
    } elseif (! $response instanceof SymfonyResponse) {
        $response = new Response($response);
    }

    if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
        $response->setNotModified();
    }

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

以上就是整個路由的處理過程

下面我用大白話來敘述一下:

1.當用戶發起一個請求時,首先進入的是public/index.php文件中,這個文件解析了一個Kernel類,再執行了一個handle方法
2.在這個方法中調用了一個 sendRequestThroughRouter 方法,這個方法將請求發送到中間件和路由分發 dispatchToRouter 中,這個由管道組件完成,
3.在 dispatchToRouter 方法中又調用了 一個 dispatch 方法,
4.在 dispatch 中又調用了 dispatchToRoute 方法,這個方法中的主體邏輯分爲兩部分,首先是通過 findRoute 方法進行路由匹配,然後通過 runRoute 執行對應的路由邏輯,
    4.1 findRoute 方法是獲取到匹配路由之後將其綁定到容器,然後返回,在findRoute方法中調用了一個重要的方法是match方法,在match方法中首先通過 $this->get($request->getMethod()) 獲取當前請求方法(GET、POST等)下的所有路由定義,該方法返回結果是 Illuminate\Routing\Route 實例數組,接下來調用 matchAgainstRoutes,在該方法中,這個方法分爲 2 部分
        4.1.1  第一部分把所有的 get 路由分爲 2 個容器,第一個容器只包含回退路由,第二個容器包含除去回退路由的所有 get 路由。
        4.1.2 第二部分把上面的 2 個容器合併,注意了此時的回退路由在所有 get 路由的的最後面。合併完之後遍歷容器中的每一個路由,找到第一個符合規則的路由,
            4.2.1 在這個過程中會調用matchs方法,在該方法中,會通過 $this->getValidators() 返回的四個維度的數據對當前請求進行匹配,分別是請求路徑URI、請求方法(GET、POST等)、Scheme(HTTP、HTTPS等) 和域名,只要一個匹配校驗不通過,則退出校驗,只有所有四個維度數據校驗都通過了,纔算通過,如果沒有匹配到路由的話,就會去匹配其他的請求方式有沒有有沒有與當前請求匹配的路由,如果有的話拋出 MethodNotAllowed 異常,如果也沒有的話才拋出 NotFoundHttpException 異常,返回 404 響應。
            
5.如果在路由匹配中找到了匹配路由,就進入到了執行路由階段runRoute,在方法中觸發一個路由匹配事件RouteMatched,最後通過 runRouteWithinStack 方法執行路由業務邏輯,首先判斷系統是否禁用中間件,如果沒有的話從匹配路由實例 $route 中通過 gatherRouteMiddleware 方法解析出當前路由上應用的路由中間件,並通過管道方式執行,中間件校驗都通過之後,調用 prepareResponse 方法處理請求,準備返回響應,響應內容由 Illuminate\Routing\Route 的 run 方法返回,在該方法中,會判斷路由是由控制器方法定義還是閉包函數定義,不管採用那種方式定義,返回的響應內容都會經由前面提到的 prepareResponse 進行處理,最終通過 toResponse 方法進行處理後準備發送給客戶端瀏覽器

 

 

參考1:https://learnku.com/articles/13622/the-principle-of-laravel-routing-execution

參考2:https://learnku.com/articles/38503

參考3:https://xueyuanjun.com/post/19565.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章