12
本文首发于「深入浅出 Laravel 路由执行原理」,转载请注明出处。

这篇文章我们将学习 Laravel 项目中一个很重要的主题 --「路由」。

可以说几乎所有的框架都会涉及到「路由」的处理,简单一点讲就将用户请求的 url 分配到对应的处理程序。

那么还等什么,赶紧上车吧!

路由加载原理

这节我们将重点讲解如何加载我们在 routes 目录下的定义的 web.php 路由配置文件(仅考虑典型的 Web 应用)。

预备知识

通过之前 Laravel 内核解读文章我们知道在 Laravel 中,所有的服务都是通过「服务提供者」的 register 方法绑定到「Laralvel 服务容器」中,
之后才可以在 Laravel 项目中使用。

我想你自然的会想到:加载路由文件任务本质是一种服务,它实现的功能是将路由文件中定义的路由加载到 Laravel 内核中,
然后再去匹配正确的路由并处理 HTTP 请求。所以,这里我们应该查找到与路由有关的「服务提供者」去注册和启动路由相关服务。

现在让我们到 config/app.php 配置文件中的 providers 节点去查找与路由相关的「服务提供者」,没错就是 App\Providers\RouteServiceProvider::class 类。

提示:有关「服务提供者」的运行原理,你可以阅读「深入剖析 Laravel 服务提供者实现原理」一文,这篇文章深入讲解「服务提供者」
注册和启动原理。对此不太了解的朋友可以后续补充一下这方面知识。

这里有必要简单介绍下「服务提供者」的加载和执行过程:

  1. 首先,HTTP 内核程序会去执行所有「服务提供者」 register 方法,将所有的服务注册到服务容器内,这里的注册指的是将服务绑定(bind)到容器;
  2. 当所有「服务提供者」注册完后,会执行已完成注册「服务提供者」的 boot 方法启动服务。

「服务提供者」的注册和启动处理由 Illuminate\Foundation\Http\Kernel 这个 HTTP 内核程序完成。

了解完「服务提供者」的基本概念后,我们不难知道 RouteServiceProvider 路由提供者服务,同样由 注册(register)启动(boot) 这两个处理去完成服务加载工作。

深入 RouteServiceProvider 服务提供者

进入到 RouteServiceProvider 源码中,让我们看看它在注册和启动时究竟如何工作才能载入路由配置。

<?php
namespace App\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;

/**
 * @see https://github.com/laravel/laravel/blob/5994e242152764a3aeabd5d88650526aeb793b90/app/Providers/RouteServiceProvider.php
 */
class RouteServiceProvider extends ServiceProvider
{
    /**
     * This namespace is applied to your controller routes. 定义当前 Laravel 应用控制器路由的命名空间。
     */
    protected $namespace = 'App\Http\Controllers';

    /**
     * Define your route model bindings, pattern filters, etc. 定义路由绑定、正则过滤等。
     */
    public function boot()
    {
        parent::boot();
    }

    /**
     * Define the routes for the application. 定义应用的路由。
     */
    public function map()
    {
        $this->mapApiRoutes();
        $this->mapWebRoutes();
    }

    /**
     * Define the "web" routes for the application. 定义应用 Web 路由。
     * 
     * These routes all receive session state, CSRF protection, etc. 这里定义的所有路由都会处理会话状态和 CSRF 防护等处理。
     */
    protected function mapWebRoutes()
    {
        Route::middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }

    /**
     * Define the "api" routes for the application. 定义应用 API 路由。
     * 
     * These routes are typically stateless. 在此定义的路由为典型的无状态路由。
     */
    protected function mapApiRoutes()
    {
        Route::prefix('api')
             ->middleware('api')
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }
}

没错阅读方便,我删除了源码中部分的注释和空白行。

所以,我们仅需要将目光集中到 RouteServiceProviderboot 方法中就可以了,其实在它方法体中只是去调用父类的 boot 方法完成服务启动处理。

另外,在类的内部还声明了 mapXXX() 系列方法,这些方法是用于定义应用程序的路由的实际操作,有关 map 系列函数的解读会在稍后进一步讲解。

还是先让我们看看 Illuminate\Foundation\Support\Providers\RouteServiceProvider 父类是如何处理 启动(boot) 服务的吧:

<?php

namespace Illuminate\Foundation\Support\Providers;

use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Routing\UrlGenerator;

/**
 * @mixin \Illuminate\Routing\Router
 * @see https://github.com/laravel/framework/blob/5.4/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php
 */
class RouteServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot()
    {
        $this->setRootControllerNamespace();

        if ($this->app->routesAreCached()) {
            $this->loadCachedRoutes();
        } else {
            $this->loadRoutes();

            $this->app->booted(function () {
                $this->app['router']->getRoutes()->refreshNameLookups();
                $this->app['router']->getRoutes()->refreshActionLookups();
            });
        }
    }

    /**
     * Set the root controller namespace for the application. 设置应用控制器根命名空间。
     */
    protected function setRootControllerNamespace()
    {
        if (! is_null($this->namespace)) {
            $this->app[UrlGenerator::class]->setRootControllerNamespace($this->namespace);
        }
    }

    /**
     * Load the cached routes for the application. 从缓存中加载路由。
     */
    protected function loadCachedRoutes()
    {
        $this->app->booted(function () {
            require $this->app->getCachedRoutesPath();
        });
    }

    /**
     * Load the application routes. 加载应用路由。
     */
    protected function loadRoutes()
    {
        // 加载应用的路由通过执行服务容器的 call 方法调用相关加载类
        // 这里既是调用子类 App\\Providers\\RouteServiceProvider::class 的 map 方法读取配置。
        if (method_exists($this, 'map')) {
            $this->app->call([$this, 'map']);
        }
    }
}

「路由服务提供者」启动过程总结起来一共分为以下几个步骤:

  1. 将我们 Laravel 应用的控制器所在的命名空间设置到 URL 生成器中(UrlGenerator)供后续使用;
  2. 处于系统性能上的考量,会率先检测是否启用路由缓存。已缓存路由的话直接从缓存文件中读取路由配置;
  3. 未缓存则由 loadRoutes 方法执行缓存处理。最终回到由 App\Providers\RouteServiceProvider 类中定义的 map 方法执行路由载入处理。

学习到这,大家对路由的整个加载过程应该已经建立起一个比较宏观上的概念了。

深入研究 map 定义路由系列方法

建立起宏观上的路由加载流程后,我们百尺竿头更进一步,继续深入到 mapXXX() 系列方法,因为这些方法才是实际去执行路由加载处理的组件。

在之前的源码清单中,我们看到在 map 方法内部会分别调用并执行了 mapWebRoutes()mapApiRoutes() 这两个方法,它们的工作是分别加载 Web 路由和 Api 路由配置。

由于篇幅所限,这里我们只解析 Web 路由 mapWebRoutes 的载入原理,因为这两个加载路由处理过程几乎完全一样,不是么朋友?

...
    /**
     * Define the "web" routes for the application. 定义应用 Web 路由。
     * 
     * These routes all receive session state, CSRF protection, etc. 这里定义的所有路由都会处理会话状态和 CSRF 防护等处理。
     */
    protected function mapWebRoutes()
    {
        Route::middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }
...

mapWebRoutes 在处理 Web 路由加载时,通过 Route 门面(Facade)所代理的 Illuminate\Routing\Router 服务依次执行:

  1. 执行 Route::middleware('web')web 中间件注册到路由;
  2. 执行 namespace($this->namespace) 方法,将控制器命名空间设置到路由中;
  3. 最后执行以路由文件 base_path('routes/web.php') 目录为参数的 group 方法完成 Web 路由组的设置。

大致如此,我们继续,看看它是如何执行 middleware 等方法的 !

打开 Router 门面的服务 Illuminate\Routing\Router 类的内部,可能你无法找到 middleware 方法声明。

没错它是通过实现 __call 魔术方法动态的执行反射功能,完成调用 middleware 方法,并返回 RouteRegistrar 实例。

<?php

namespace Illuminate\Routing;

/**
 * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Routing/Router.php
 */
class Router implements RegistrarContract, BindingRegistrar
{
    /**
     * The route group attribute stack.
     */
    protected $groupStack = [];

    /**
     * Create a route group with shared attributes. 创建拥有公共属性(中间件、命名空间等)的路由组。
     */
    public function group(array $attributes, $routes)
    {
        $this->updateGroupStack($attributes);

        // Once we have updated the group stack, we'll load the provided routes and
        // merge in the group's attributes when the routes are created. After we
        // have created the routes, we will pop the attributes off the stack.
        $this->loadRoutes($routes);

        array_pop($this->groupStack);
    }

    /**
     * Update the group stack with the given attributes. 将给定属性(中间件、命名空间等)更新到路由组栈中。
     */
    protected function updateGroupStack(array $attributes)
    {
        if (! empty($this->groupStack)) {
            $attributes = RouteGroup::merge($attributes, end($this->groupStack));
        }

        $this->groupStack[] = $attributes;
    }

    /**
     * Load the provided routes. 载入定义的路由
     *
     * @param  \Closure|string  $routes
     * @return void
     */
    protected function loadRoutes($routes)
    {
        if ($routes instanceof Closure) {
            $routes($this);
        } else {
            $router = $this;

            require $routes;
        }
    }

    /**
     * Dynamically handle calls into the router instance. 动态处理 router 实例中的方法调用。
     */
    public function __call($method, $parameters)
    {
        // if (static::hasMacro($method)) {
        //     return $this->macroCall($method, $parameters);
        // }

        // 请看这里,在这里通过反射动态的调用 middleware 方法,完成中间件的处理
        if ($method == 'middleware') {
            return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
        }

        // return (new RouteRegistrar($this))->attribute($method, $parameters[0]);
    }
}

由于篇幅所限,这篇文章将不展开对 RouteRegistrar 源码的研究,感兴趣的朋友可以自行研究。

简短截说,最终在 RouteRegistrar::group 方法内部完成对 Illuminate\Routing\Router::group 方法的调用,实现载入路由文件处理。

最终在 Illuminate\Routing\Router::group 方法里去执行路由文件引入处理:

  1. 通过 updateGroupStack 方法,更新路由组中的属性(即由 Route::middleware(...)->namespace(...) 设置的中间件和命名空间等);
  2. 使用 loadRoutes 方法引入 base_path('routes/web.php') 文件中定义的路由。

到这我们就完整的分析完路由文件的加载流程,由于涉及到的模块较多,还需要读者朋友们再琢磨琢磨才能消化。

提示:在 Laravel 中门面是一种提供了操作简单的能够使用静态方法来方式访问 Laravel 服务的机制。对「门面 Facade」不太了解的朋友可以阅读「深入浅出 Laravel 的 Facade 外观系统」。

路由分发

这一节我们主要讲解 HTTP 如何被分发到相关路由并执行路由设置的回调(或控制器)。

如果你有了解过 Laravel 生命周期的话,应该知道所有的 HTTP 请求都是由 IlluminateFoundationHttpkernel::class 内核处理的,而捕获 HTTP 请求操作位于项目的入口文件 public/index.php 中。

接收 HTTP 请求

/*
|--------------------------------------------------------------------------
| 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()
);

具体一点讲就是先从服务容器解析出 IlluminateContractsHttpKernel::class 服务实例,再执行服务的 handle 方法处理 HTTP 请求。

本文不涉及讲解如何捕获一个 HTTP 请求 IlluminateHttpRequest::capture(),如果后续有时间会开设一篇文章详细讲解一下,作为本文的补充资料。但在这里你只需要知道,我们的 handle 处理器接收用户的 Request 作为参数,然后去执行。

所以我们需要深入到 handle 才能知道 HTTP 请求是如何被匹配路由和处理回调(或控制器)的。

由 HTTP 内核处理 HTTP 请求

此处略去 N 个解析,嗯,我们找到了 IlluminateFoundationHttpkernel::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);
        };
    }
}

处理整个 HTTP 请求的过程分完几个阶段:

  1. 清空已解析的请求(clearResolvedInstance);
  2. 执行应用的引导程序(bootstrap),这部分的内容请查阅 深入剖析 Laravel 服务提供者实现原理 的服务提供者启动原理小结。
  3. 将请求发送到中间件和路由中,这个由管道组件完成(Pipeline)。

对于前两个阶段的处理可以阅读我给出的相关文章。另外补充两篇有关中间件的文章 Laravel 中间件原理Laravel 管道流原理,可以去研究下 Laravel 中间件如何工作的。

路由分发处理

好了经历过千锤百炼后,我们的请求终于顺利到达 then($this->dispatchToRouter()) 路由处理了,真是不容易。那么现在,让我们看看 dispatchToRouter 是如何分发路由的。

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

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

从这段源码我们知道路由分发接收 $request 请求实例,然后执行分发(dispatch)操作,这些处理会回到 Illuminate\Routing\Router 服务中处理:

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

路由分发流程

Illuminate\Routing\Router 服务将接收被分发到的请求($request)然后执行路由设置是配置的闭包(或控制器)函数,整个过程包括:

  1. 从 RouteCollection 路由集合中查找出当前请求 URI($request)匹配的路由,由 **Router::findRoute($request)** 方法完成;
  2. 运行路由配置阶段所配置的闭包(或控制器方法),这个处理在 Router::runRoute(Request $request, Route $route) 方法完成;
    2.1 在运行路由闭包或控制器方法时,将采用类似 HTTP kernel 的 handle 执行方式去运行当前路由适用的局部中间件;
    2.2 在最终的 then 方法内部会执行 **$route->run()** 方法运行路由,$route(IlluminateRoutingRoute) 为 findRoute 方法查找到的路由;
  3. 生成 HTTP 响应(由 prepareResponse 方法完成)。

执行路由闭包或控制器

最后,让我们进入 IlluminateRoutingRoute 源码研究下一个路由闭包或控制器是如何被执行的:

<?php

namespace Illuminate\Routing;
...
class Route
{
    /**
     * Run the route action and return the response. 运行路由闭包或控制器,并返回响应结果。
     * @see https://github.com/laravel/framework/blob/5.5/src/Illuminate/Routing/Route.php#L163
     */
    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();
        }
    }

    /**
     * Checks whether the route's action is a controller. 判断路由处理函数是否为控制器。
     *
     * @return bool
     */
    protected function isControllerAction()
    {
        return is_string($this->action['uses']);
    }

    /**
     * Run the route action and return the response. 运行闭包路由处理函数,并返回响应结果。
     *
     * @return mixed
     */
    protected function runCallable()
    {
        $callable = $this->action['uses'];

        return $callable(...array_values($this->resolveMethodDependencies(
            $this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses'])
        )));
    }

    /**
     * Run the route action and return the response. 运行控制器路由处理方法,并返回响应结果。
     *
     * @return mixed
     *
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     */
    protected function runController()
    {
        // 在控制器路由分发器 Illuminate\Routing\ControllerDispatcher 中去执行(dispatch)控制器方法
        return $this->controllerDispatcher()->dispatch(
            $this, $this->getController(), $this->getControllerMethod()
        );
    }

    /**
     * Get the controller instance for the route. 从路由配置中解析出控制器实例。
     *
     * @return mixed
     */
    public function getController()
    {
        if (! $this->controller) {
            // 例如: web.php 中配置了 Router::get('/', 'HomeController@index'); 则从 'HomeController@index' 解析出 **HomeController** 控制器实例。
            $class = $this->parseControllerCallback()[0];

            $this->controller = $this->container->make(ltrim($class, '\\'));
        }

        return $this->controller;
    }

    /**
     * Get the controller method used for the route. 获取路由需要执行的控制器方法。
     *
     * @return string
     */
    protected function getControllerMethod()
    {
        // 从 'HomeController@index' 解析出 'index' 方法。
        return $this->parseControllerCallback()[1];
    }

    /**
     * Parse the controller. 解析控制器。
     *
     * @return array
     */
    protected function parseControllerCallback()
    {
        return Str::parseCallback($this->action['uses']);
    }

    /**
     * Get the dispatcher for the route's controller.
     *
     * @return \Illuminate\Routing\Contracts\ControllerDispatcher
     */
    public function controllerDispatcher()
    {
        if ($this->container->bound(ControllerDispatcherContract::class)) {
            return $this->container->make(ControllerDispatcherContract::class);
        }

        // 控制器分发器: Illuminate\Routing\ControllerDispatcher
        return new ControllerDispatcher($this->container);
    }
}

运行控制器方法

<?php

namespace Illuminate\Routing;

use Illuminate\Container\Container;
use Illuminate\Routing\Contracts\ControllerDispatcher as ControllerDispatcherContract;

class ControllerDispatcher implements ControllerDispatcherContract
{
    /**
     * Dispatch a request to a given controller and method. 将请求分发到给定的控制器及其方法。
     *
     * @see https://github.com/laravel/framework/blob/5.5/src/Illuminate/Routing/ControllerDispatcher.php#L38
     * @param  \Illuminate\Routing\Route  $route 路由
     * @param  mixed  $controller 控制器
     * @param  string  $method    方法
     * @return mixed
     */
    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 $controller->{$method}(...array_values($parameters));
    }
}

补充说明

另外,补充一些在查找匹配请求路由时未讲明的一些操作,大家可以自行去研究一下:

总结

在这篇文章我们主要学习一下几个有关路由处理的相关知识:

  • Laravel 中的路由如何被加载到项目中;
  • 如何接收 HTTP 请求;
  • 如何依据 HTTP 请求($request)查找所匹配的路由;
  • 运行路由闭包或控制器方法。

希望对大家在学习 Laravel 有所帮助。

参考资料

感谢一下相关 Laravel 学习资料。


柳公子
3.3k 声望1.5k 粉丝

学以致用,边学边用