這篇文章主要介紹了關於Laravel路由(Route)解讀,有著一定的參考價值,現在分享給大家,有需要的朋友可以參考一下
路由是外界訪問Laravel應用程式的通路或者說路由定義了Laravel的應用程式向外界提供服務的具體方式:通過指定的URI、HTTP要求方法以及路由參數(可選)才能正確訪問到路由定義的處理常式。無論URI對應的處理常式是一個簡單的閉包還是說是控制器方法沒有對應的路由外界都訪問不到他們,今天我們就來看看Laravel是如何來設計和實現路由的。
我們在路由檔案裡通常是向下面這樣來定義路由的:
Route::get('/user', 'UsersController@index');
通過上面的路由我們可以知道,用戶端通過以HTTP GET方式來請求 URI "/user"時,Laravel會把請求最終派發給UsersController類的index方法來進行處理,然後在index方法中返迴響應給用戶端。
上面註冊路由時用到的Route類在Laravel裡叫門面(Facade),它提供了一種簡單的方式來訪問綁定到服務容器裡的服務router,Facade的設計理念和實現方式我打算以後單開博文來寫,在這裡我們只要知道調用的Route這個門面的靜態方法都對應服務容器裡router這個服務的方法,所以上面那條路由你也可以看成是這樣來註冊的:
app()->make('router')->get('user', 'UsersController@index');
router這個服務是在執行個體化應用程式Application時在構造方法裡通過註冊RoutingServiceProvider時綁定到服務容器裡的:
//bootstrap/app.php$app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../'));//Application: 構造方法public function __construct($basePath = null){ if ($basePath) { $this->setBasePath($basePath); } $this->registerBaseBindings(); $this->registerBaseServiceProviders(); $this->registerCoreContainerAliases();}//Application: 註冊基礎的服務提供器protected function registerBaseServiceProviders(){ $this->register(new EventServiceProvider($this)); $this->register(new LogServiceProvider($this)); $this->register(new RoutingServiceProvider($this));}//\Illuminate\Routing\RoutingServiceProvider: 綁定router到服務容器protected function registerRouter(){ $this->app->singleton('router', function ($app) { return new Router($app['events'], $app); });}
通過上面的代碼我們知道了Route調用的靜態方法都對應於\Illuminate\Routing\Router
類裡的方法,Router這個類裡包含了與路由的註冊、定址、調度相關的方法。
下面我們從路由的註冊、載入、定址這幾個階段來看一下laravel裡是如何?這些的。
路由載入
註冊路由前需要先載入路由檔案,路由檔案的載入是在App\Providers\RouteServiceProvider
這個伺服器提供者的boot方法裡載入的:
class RouteServiceProvider extends ServiceProvider{ public function boot() { parent::boot(); } public function map() { $this->mapApiRoutes(); $this->mapWebRoutes(); } protected function mapWebRoutes() { Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); } protected function mapApiRoutes() { Route::prefix('api') ->middleware('api') ->namespace($this->namespace) ->group(base_path('routes/api.php')); }}
namespace Illuminate\Foundation\Support\Providers;class RouteServiceProvider extends ServiceProvider{ 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(); }); } } protected function loadCachedRoutes() { $this->app->booted(function () { require $this->app->getCachedRoutesPath(); }); } protected function loadRoutes() { if (method_exists($this, 'map')) { $this->app->call([$this, 'map']); } }}class Application extends Container implements ApplicationContract, HttpKernelInterface{ public function routesAreCached() { return $this['files']->exists($this->getCachedRoutesPath()); } public function getCachedRoutesPath() { return $this->bootstrapPath().'/cache/routes.php'; }}
laravel 首先去尋找路由的快取檔案,沒有快取檔案再去進行載入路由。快取檔案一般在 bootstrap/cache/routes.php 檔案中。
方法loadRoutes會調用map方法來載入路由檔案裡的路由,map這個函數在App\Providers\RouteServiceProvider
類中,這個類繼承自Illuminate\Foundation\Support\Providers\RouteServiceProvider
。通過map方法我們能看到laravel將路由分為兩個大組:api、web。這兩個部分的路由分別寫在兩個檔案中:routes/web.php、routes/api.php。
Laravel5.5裡是把路由分別放在了幾個檔案裡,之前的版本是在app/Http/routes.php檔案裡。放在多個檔案裡能更方便地管理API路由和與WEB路由
路由註冊
我們通常都是用Route這個Facade調用靜態方法get, post, head, options, put, patch, delete......等來註冊路由,上面我們也說了這些靜態方法其實是調用了Router類裡的方法:
public function get($uri, $action = null){ return $this->addRoute(['GET', 'HEAD'], $uri, $action);}public function post($uri, $action = null){ return $this->addRoute('POST', $uri, $action);}....
可以看到路由的註冊統一都是由router類的addRoute方法來處理的:
//註冊路由到RouteCollectionprotected function addRoute($methods, $uri, $action){ return $this->routes->add($this->createRoute($methods, $uri, $action));}//建立路由protected function createRoute($methods, $uri, $action){ if ($this->actionReferencesController($action)) { //controller@action類型的路由在這裡要進行轉換 $action = $this->convertToControllerAction($action); } $route = $this->newRoute( $methods, $this->prefix($uri), $action ); if ($this->hasGroupStack()) { $this->mergeGroupAttributesIntoRoute($route); } $this->addWhereClausesToRoute($route); return $route;}protected function convertToControllerAction($action){ if (is_string($action)) { $action = ['uses' => $action]; } if (! empty($this->groupStack)) { $action['uses'] = $this->prependGroupNamespace($action['uses']); } $action['controller'] = $action['uses']; return $action;}
註冊路由時傳遞給addRoute的第三個參數action可以閉包、字串或者數組,數組就是類似['uses' => 'Controller@action', 'middleware' => '...']這種形式的。如果action是Controller@action
類型的路由將被轉換為action數組, convertToControllerAction執行完後action的內容為:
[ 'uses' => 'App\Http\Controllers\SomeController@someAction', 'controller' => 'App\Http\Controllers\SomeController@someAction']
可以看到把命名空間補充到了控制器的名稱前組成了完整的控制器類名,action數組構建完成接下裡就是建立路由了,建立路由即用指定的HTTP要求方法、URI字串和action數組來建立\Illuminate\Routing\Route
類的執行個體:
protected function newRoute($methods, $uri, $action){ return (new Route($methods, $uri, $action)) ->setRouter($this) ->setContainer($this->container);}
路由建立完成後將Route添加到RouteCollection中去:
protected function addRoute($methods, $uri, $action){ return $this->routes->add($this->createRoute($methods, $uri, $action));}
router的$routes屬性就是一個RouteCollection對象,添加路由到RouteCollection對象時會更新RouteCollection對象的routes、allRoutes、nameList和actionList屬性
class RouteCollection implements Countable, IteratorAggregate{ public function add(Route $route) { $this->addToCollections($route); $this->addLookups($route); return $route; } protected function addToCollections($route) { $domainAndUri = $route->getDomain().$route->uri(); foreach ($route->methods() as $method) { $this->routes[$method][$domainAndUri] = $route; } $this->allRoutes[$method.$domainAndUri] = $route; } protected function addLookups($route) { $action = $route->getAction(); if (isset($action['as'])) { //如果時命名路由,將route對象映射到以路由名為key的數組值中方便尋找 $this->nameList[$action['as']] = $route; } if (isset($action['controller'])) { $this->addToActionList($action, $route); } }}
RouteCollection的四個屬性
routes中存放了HTTP要求方法與路由對象的映射:
[ 'GET' => [ $routeUri1 => $routeObj1 ... ] ...]
allRoutes屬性裡存放的內容時將routes屬性裡的二位元組編程一位元組後的內容:
[ 'GET' . $routeUri1 => $routeObj1 'GET' . $routeUri2 => $routeObj2 ...]
nameList是路由名稱與路由對象的一個映射表
[ $routeName1 => $routeObj1 ...]
actionList是路由控制器方法字串與路由對象的映射表
[ 'App\Http\Controllers\ControllerOne@ActionOne' => $routeObj1]
這樣就算註冊好路由了。
路由定址
中介軟體的文章裡我們說過HTTP請求在經過Pipeline通道上的中介軟體的前置操作後到達目的地:
//Illuminate\Foundation\Http\Kernelclass Kernel implements KernelContract{ 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()); } protected function dispatchToRouter() { return function ($request) { $this->app->instance('request', $request); return $this->router->dispatch($request); }; } }
上面代碼可以看到Pipeline的destination就是dispatchToRouter函數返回的閉包:
$destination = function ($request) { $this->app->instance('request', $request); return $this->router->dispatch($request);};
在閉包裡調用了router的dispatch方法,路由定址就發生在dispatch的第一個階段findRoute裡:
class Router implements RegistrarContract, BindingRegistrar{ public function dispatch(Request $request) { $this->currentRequest = $request; return $this->dispatchToRoute($request); } public function dispatchToRoute(Request $request) { return $this->runRoute($request, $this->findRoute($request)); } protected function findRoute($request) { $this->current = $route = $this->routes->match($request); $this->container->instance(Route::class, $route); return $route; } }
尋找路由的任務由 RouteCollection 負責,這個函數負責匹配路由,並且把 request 的 url 參數綁定到路由中:
class RouteCollection implements Countable, IteratorAggregate{ public function match(Request $request) { $routes = $this->get($request->getMethod()); $route = $this->matchAgainstRoutes($routes, $request); if (! is_null($route)) { //找到匹配的路由後,將URI裡的路徑參數綁定賦值給路由(如果有的話) return $route->bind($request); } $others = $this->checkForAlternateVerbs($request); if (count($others) > 0) { return $this->getRouteForMethods($request, $others); } throw new NotFoundHttpException; } protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true) { return Arr::first($routes, function ($value) use ($request, $includingMethod) { return $value->matches($request, $includingMethod); }); }}class Route{ 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; }}
$routes = $this->get($request->getMethod());
會先載入註冊路由階段在RouteCollection裡產生的routes屬性裡的值,routes中存放了HTTP要求方法與路由對象的映射。
然後依次調用這堆路由裡路由對象的matches方法, matches方法, matches方法裡會對HTTP請求對象進行一些驗證,驗證對應的Validator是:UriValidator、MethodValidator、SchemeValidator、HostValidator。
在驗證之前在$this->compileRoute()
裡會將路由的規則轉換成Regex。
UriValidator主要是看請求對象的URI是否與路由的正則規則匹配能匹配上:
class UriValidator implements ValidatorInterface{ public function matches(Route $route, Request $request) { $path = $request->path() == '/' ? '/' : '/'.$request->path(); return preg_match($route->getCompiled()->getRegex(), rawurldecode($path)); }}
MethodValidator驗證要求方法, SchemeValidator驗證協議是否正確(http|https), HostValidator驗證網域名稱, 如果路由中不設定host屬性,那麼這個驗證不會進行。
一旦某個路由通過了全部的認證就將會被返回,接下來就要將請求對象URI裡的路徑參數綁定賦值給路由參數:
路由參數綁定
class Route{ public function bind(Request $request) { $this->compileRoute(); $this->parameters = (new RouteParameterBinder($this)) ->parameters($request); return $this; }}class RouteParameterBinder{ public function parameters($request) { $parameters = $this->bindPathParameters($request); if (! is_null($this->route->compiled->getHostRegex())) { $parameters = $this->bindHostParameters( $request, $parameters ); } return $this->replaceDefaults($parameters); } protected function bindPathParameters($request) { preg_match($this->route->compiled->getRegex(), '/'.$request->decodedPath(), $matches); return $this->matchToKeys(array_slice($matches, 1)); } protected function matchToKeys(array $matches) { if (empty($parameterNames = $this->route->parameterNames())) { return []; } $parameters = array_intersect_key($matches, array_flip($parameterNames)); return array_filter($parameters, function ($value) { return is_string($value) && strlen($value) > 0; }); }}
賦值路由參數完成後路由定址的過程就結束了,結下來就該運行通過匹配路由中對應的控制器方法返迴響應對象了。
class Router implements RegistrarContract, BindingRegistrar{ public function dispatch(Request $request) { $this->currentRequest = $request; return $this->dispatchToRoute($request); } public function dispatchToRoute(Request $request) { return $this->runRoute($request, $this->findRoute($request)); } 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) ); } 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() ); }); } }namespace Illuminate\Routing;class Route{ 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(); } }}
這裡我們主要介紹路由相關的內容,runRoute的過程通過上面的源碼可以看到其實也很複雜, 會收集路由和控制器裡的中介軟體,將請求通過中介軟體過濾才會最終到達目的地路由,執行目的路由地run()
方法,裡面會判斷路由對應的是一個控制器方法還是閉包然後進行相應地調用,最後把執行結果封裝成Response對象返回給用戶端。這個過程還會涉及到我們以前介紹過的中介軟體過濾、服務解析、依賴注入方面的資訊,如果在看源碼時有不懂的地方可以翻看我之前寫的文章。
依賴注入
服務容器
中介軟體
以上就是本文的全部內容,希望對大家的學習有所協助,更多相關內容請關注topic.alibabacloud.com!