這篇文章咱們將學習 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 類。api
提示:有關「服務提供者」的運行原理,你能夠閱讀「深刻剖析 Laravel 服務提供者實現原理」一文,這篇文章深刻講解「服務提供者」
註冊和啓動原理。對此不太瞭解的朋友能夠後續補充一下這方面知識。
這裏有必要簡單介紹下「服務提供者」的加載和執行過程:緩存
「服務提供者」的註冊和啓動處理由 Illuminate\Foundation\Http\Kernel 這個 HTTP 內核程序完成。session
瞭解完「服務提供者」的基本概念後,咱們不難知道 RouteServiceProvider 路由提供者服務,一樣由 註冊(register) 和 啓動(boot) 這兩個處理去完成服務加載工做。閉包
進入到 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. www.xsjtv.org將 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.www.xsjtv.org 解析控制器。 * * @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)); } }