本文首发于「 深刻浅出 Laravel 路由执行原理」,转载请注明出处。
这篇文章咱们将学习 Laravel 项目中一个很重要的主题 --「路由」。php
能够说几乎全部的框架都会涉及到「路由」的处理,简单一点讲就将用户请求的 url 分配到对应的处理程序。laravel
那么还等什么,赶忙上车吧!git
这节咱们将重点讲解如何加载咱们在 routes 目录下的定义的 web.php 路由配置文件(仅考虑典型的 Web 应用)。github
经过以前 Laravel 内核解读文章咱们知道在 Laravel 中,全部的服务都是经过「服务提供者」的 register 方法绑定到「Laralvel 服务容器」中,
以后才能够在 Laravel 项目中使用。web
我想你天然的会想到:加载路由文件任务本质是一种服务,它实现的功能是将路由文件中定义的路由加载到 Laravel 内核中,
而后再去匹配正确的路由并处理 HTTP 请求。因此,这里咱们应该查找到与路由有关的「服务提供者」去注册和启动路由相关服务。bootstrap
如今让咱们到 config/app.php 配置文件中的 providers 节点去查找与路由相关的「服务提供者」,没错就是 App\Providers\RouteServiceProvider::class 类。segmentfault
提示:有关「服务提供者」的运行原理,你能够阅读「 深刻剖析 Laravel 服务提供者实现原理」一文,这篇文章深刻讲解「服务提供者」
注册和启动原理。对此不太了解的朋友能够后续补充一下这方面知识。
这里有必要简单介绍下「服务提供者」的加载和执行过程:api
「服务提供者」的注册和启动处理由 Illuminate\Foundation\Http\Kernel 这个 HTTP 内核程序完成。缓存
了解完「服务提供者」的基本概念后,咱们不难知道 RouteServiceProvider 路由提供者服务,一样由 注册(register) 和 启动(boot) 这两个处理去完成服务加载工做。session
进入到 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')); } }
没错阅读方便,我删除了源码中部分的注释和空白行。
因此,咱们仅须要将目光集中到 RouteServiceProvider 的 boot 方法中就能够了,其实在它方法体中只是去调用父类的 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']); } } }
「路由服务提供者」启动过程总结起来一共分为如下几个步骤:
学习到这,你们对路由的整个加载过程应该已经创建起一个比较宏观上的概念了。
创建起宏观上的路由加载流程后,咱们百尺竿头更进一步,继续深刻到 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 服务依次执行:
大体如此,咱们继续,看看它是如何执行 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 方法里去执行路由文件引入处理:
到这咱们就完整的分析完路由文件的加载流程,因为涉及到的模块较多,还须要读者朋友们再琢磨琢磨才能消化。
提示:在 Laravel 中门面是一种提供了操做简单的可以使用静态方法来方式访问 Laravel 服务的机制。对「门面 Facade」不太了解的朋友能够阅读「 深刻浅出 Laravel 的 Facade 外观系统」。
这一节咱们主要讲解 HTTP 如何被分发到相关路由并执行路由设置的回调(或控制器)。
若是你有了解过 Laravel 生命周期的话,应该知道全部的 HTTP 请求都是由 IlluminateFoundationHttpkernel::class 内核处理的,而捕获 HTTP 请求操做位于项目的入口文件 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() );
具体一点讲就是先从服务容器解析出 IlluminateContractsHttpKernel::class 服务实例,再执行服务的 handle 方法处理 HTTP 请求。
本文不涉及讲解如何捕获一个 HTTP 请求 IlluminateHttpRequest::capture(),若是后续有时间会开设一篇文章详细讲解一下,做为本文的补充资料。但在这里你只须要知道,咱们的 handle 处理器接收用户的 Request 做为参数,而后去执行。
因此咱们须要深刻到 handle 才能知道 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 请求的过程分完几个阶段:
对于前两个阶段的处理能够阅读我给出的相关文章。另外补充两篇有关中间件的文章 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)而后执行路由设置是配置的闭包(或控制器)函数,整个过程包括:
最后,让咱们进入 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 有所帮助。
感谢一下相关 Laravel 学习资料。