生命週期 6--一個 request 是如何匹配到具體路由的原始碼分析

hustnzj發表於2018-11-17

前面已經分析了request物件是如何通過全域性中介軟體過濾後到達路由器

  • request是如何匹配具體路由。
  • 如何根據request物件的資訊獲取路由和控制器中介軟體的。
  • 如何穿透這些中介軟體的。
  • 穿過後,是如何進入控制器與應用程式互動的(一般寫程式只寫這麼一點。。)。
  • 互動後,返回結果又是如何再次通過路由和控制器中介軟體和全域性中介軟體的。
  • 最後的得到response物件。
  • 限於時間關係,只說大致思路,不會深入太多。
  • 不解釋屬性含義,程式碼中有,自行查閱即可。
  • 不嚴格區分方法是從子類和父類執行,除非子類和父類中有相同方法。
  • 能夠通過註釋講清楚的,優先寫註釋。
  • 方法所屬名稱空間,不影響理解的都會忽略,自行查閱或debug即可。
  • route: 單個路由
  • routes: 多個路由(RouteCollection)
  • router: 路由器(放置routes的容器)

程式碼分析起點選擇在vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:176,你我的可能略有差異,如下:

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

上面這句之所以沒有寫上return,是因為限於篇幅和時間限制,本文遠遠分析不到執行return的地方。

dispatch方法如下:

    /**
     * Dispatch the request to the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
     */
    public function dispatch(Request $request)
    {
        //使用request物件設定kernel的currentRequest屬性
        $this->currentRequest = $request;   

        //將request傳送到一個路由(哪個?)並返回response
        return $this->dispatchToRoute($request);
    }

dispatchToRoute方法如下:

    /**
     * Dispatch the request to a route and return the response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    public function dispatchToRoute(Request $request)
    {
        //通過request物件找到一個匹配的路由,並執行此路由並返回結果。
        return $this->runRoute($request, $this->findRoute($request));
    }

本文重點來了,如何找到路由

$this->findRoute($request)

findRoute方法如下:

    /**
     * Find the route matching a given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Routing\Route
     */
    protected function findRoute($request)
    {
        //找到匹配request的第一個路由,如果找不到丟擲NotFoundHttpException
        $this->current = $route = $this->routes->match($request);

        //設定route物件快取
        $this->container->instance(Route::class, $route);

        //返回路由供後面run
        return $route;
    }

match方法如下:

    /**
     * Find the first route matching a given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Routing\Route
     *
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     */
    public function match(Request $request)
    {
        //通過request的method來獲取router中的該方法型別的routes
        $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.

        //從上述的routes中,再通過route物件的matches方法來獲取匹配
        //request的具體路由route
        $route = $this->matchAgainstRoutes($routes, $request);

        //將獲得的route繫結到request,並返回給findRoute方法。
        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.

        //如果上面沒找到對應route,那麼會檢查其他的HTTP動詞動詞
        $others = $this->checkForAlternateVerbs($request);

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

        //如果還找不到,就會丟擲NotFoundHttpException
        throw new NotFoundHttpException;
    }

routes物件的matchAgainstRoutes方法就是神奇匹配的地方:

    /**
     * Determine if a route in the array matches the request.
     *
     * @param  array  $routes
     * @param  \Illuminate\Http\Request  $request
     * @param  bool  $includingMethod
     * @return \Illuminate\Routing\Route|null
     */
    protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
    {
        //將傳入的routes按照是否有Fallback路由分為兩部分。
        //注意是Fallback而非callback。。
        [$fallbacks, $routes] = collect($routes)->partition(function ($route) {
            return $route->isFallback;
        });

        //然後遍歷合併後的routes,對每個route,都使用其matches方法來判斷
        //request和路由是否完全匹配!
        return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
            return $value->matches($request, $includingMethod);
        });
    }

route物件的matches方法:

    /**
     * Determine if the route matches given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  bool  $includingMethod
     * @return bool
     */
    public function matches(Request $request, $includingMethod = true)
    {
        //將當前路由(route物件)編譯為一個Symfony CompiledRoute instance
        //並賦值給route物件的compiled屬性,後面會多次用到這個屬性。
        $this->compileRoute();

        //然後是獲取當前路由的validators,預設有4個。
        //UriValidator, MethodValidator,
        //SchemeValidator, HostValidator,
        //然後遍歷驗證器陣列,每個驗證器處理一個點。
        foreach ($this->getValidators() as $validator) {
            if (! $includingMethod && $validator instanceof MethodValidator) {
                continue;
            }

            //只要有一個驗證器沒有通過,返回false到前面的match方法中
            if (! $validator->matches($this, $request)) {
                return false;
            }
        }
        //全部通過,返回true
        return true;
    }

最後,將找到的route物件返回到dispatchToRoute方法中。

$this->runRoute($request, $this->findRoute($request))

下一步,似乎就可以runRoute了。

runRoute方法

    /**
     * Return the response for the given route.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Routing\Route  $route
     * @return mixed
     */
    protected function runRoute(Request $request, Route $route)
    {
        //設定request物件的路由解析器
        $request->setRouteResolver(function () use ($route) {
            return $route;
        });

        //觸發註冊過的路由匹配事件!
        $this->events->dispatch(new Events\RouteMatched($route, $request));

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

觸發註冊過的路由匹配事件!

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

預設是沒有註冊這個事件的,但可以在appServiceProvide的boot方法或自己的serviceProvider中的boot方法中自定義,比如在vendor/hieu-le/active/src/ActiveServiceProvider.php中。

    public function boot()
    {
        // Update the instances each time a request is resolved and a route is matched
        $instance = app('active');
        if (version_compare(Application::VERSION, '5.2.0', '>=')) {
            app('router')->matched(  //就是這裡
                function (RouteMatched $event) use ($instance) {
                    $instance->updateInstances($event->route, $event->request);
                }
            );
        } else {
            app('router')->matched(
                function ($route, $request) use ($instance) {
                    $instance->updateInstances($route, $request);
                }
            );
        }
    }

然後就是如何根據request物件的資訊獲取路由和控制器中介軟體的(下一篇文章再寫)

  • serviceProvider的register方法都是用來註冊類到serviceContainer中的或者起別名的。
  • 其他工作比如新增中介軟體,新增事件監聽器,載入配置資訊,環境變數,檢視檔案等等就放到boot方法中。畢竟有了類,才能使用其中的方法來幹事情。
  • serviceContainer和各個元件之間往往通過互為屬性的關係來達到資料共享。有時也可以通過明確引用傳遞引數的方式。
  • 以上程式碼分析都比較淺,如果深入研究下去,可以點亮更多技能,是否有用另說。
  • 閉包回撥非常重要,幾乎所有的難點都集中在這上面。
  • 看原始碼就是在學PHP的各種高階用法,開拓思路,同時理解了框架原理,以後用起來會得心應手。
  • 讀原始碼時,先看介面方法,再看實現類中的屬性和方法,然後再看執行原始碼過程。執行原始碼時,先看是哪個物件,然後執行哪個方法或者賦值給哪個屬性。

相關文章