Laravel 路由匹配和执行底层源码剖析


入口方法

通过上一篇教程,我们知道路由匹配和处理逻辑入口位于 vendor/laravel/framework/src/Illuminate/Routing/Router.phpdispatch 方法:

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

真正的执行逻辑在 dispatchToRoute 方法:

public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request, $this->findRoute($request));
}

主体逻辑分为两部分,首先是通过 findRoute 方法进行路由匹配,然后通过 runRoute 执行对应的路由逻辑。接下来,我们从这两个方法切入,一来来剖析下 Laravel 路由的底层逻辑。

路由匹配

我们首先来看 findRoute 方法,该方法获取到匹配路由之后将其绑定到容器,然后返回:

protected function findRoute($request)
{
    $this->current = $route = $this->routes->match($request);

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

    return $route;
}

这里的核心逻辑是调用 Illuminate\Routing\RouteCollectionmatch 方法匹配路由:

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 实例数组,以 GET 方式访问 http://laravel58.test 首页为例,此时对应的 $routes 变量值如下:

以上都是应用已经定义的 GET 路由。接下来调用 $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);
    });
}

在该方法中,首先通过集合方法 partition 通过判断路由是否是兜底路由 将传入的 $routes 分为两部分,是兜底路由划分到 $fallbacks 集合,否则划分到 $routes 集合。然后调用 merge 方法将兜底路由放到 $routes 集合最后,再在合并后集合上调用 first 方法返回第一个匹配的路由作为当前请求匹配的路由返回。

对应的路由匹配逻辑源码则是 Illuminate\Routing\Routematches 方法:

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\RouteCollectionmatch 方法,如果匹配到定义的路由,则返回路由信息:

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\RouterrunRoute 方法:

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\Routerun 方法返回:

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

好了,至此,关于路由匹配和执行的分析就到此为止,希望可以抛砖引入,帮助你更好的理解 Laravel 底层原理并打开你阅读底层源码之门,如果你自己看起来有些吃力,可以借助 XDebug 之类的调试工具打个断点,一步步去分析。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: Laravel 中间件底层源码剖析

>> 下一篇: 异常处理篇之底层源码剖析