路由是外界訪問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方法來處理的:
//注册路由到RouteCollection protected 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\Kernel class 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)) { 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()
里会将路由的规则转换成正则表达式。
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) ); } }
这里我们主要介绍路由相关的内容,在runRoute的过程通过上面的源码可以看到其实也很复杂, 会收集路由和控制器里的中间件,将请求通过中间件过滤才会最终调用控制器方法来生成响应对象,这个过程还会设计到我们以前介绍过的中间件过滤、服务解析、依赖注入方面的信息,如果在看源码时有不懂的地方可以翻看我之前写的文章。
依赖注入
服务容器
中间件
相关推荐:
以上是Laravel路由Route詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

PHP不是在消亡,而是在不斷適應和進化。 1)PHP從1994年起經歷多次版本迭代,適應新技術趨勢。 2)目前廣泛應用於電子商務、內容管理系統等領域。 3)PHP8引入JIT編譯器等功能,提升性能和現代化。 4)使用OPcache和遵循PSR-12標準可優化性能和代碼質量。

PHP的未來將通過適應新技術趨勢和引入創新特性來實現:1)適應云計算、容器化和微服務架構,支持Docker和Kubernetes;2)引入JIT編譯器和枚舉類型,提升性能和數據處理效率;3)持續優化性能和推廣最佳實踐。

在PHP中,trait適用於需要方法復用但不適合使用繼承的情況。 1)trait允許在類中復用方法,避免多重繼承複雜性。 2)使用trait時需注意方法衝突,可通過insteadof和as關鍵字解決。 3)應避免過度使用trait,保持其單一職責,以優化性能和提高代碼可維護性。

依賴注入容器(DIC)是一種管理和提供對象依賴關係的工具,用於PHP項目中。 DIC的主要好處包括:1.解耦,使組件獨立,代碼易維護和測試;2.靈活性,易替換或修改依賴關係;3.可測試性,方便注入mock對象進行單元測試。

SplFixedArray在PHP中是一種固定大小的數組,適用於需要高性能和低內存使用量的場景。 1)它在創建時需指定大小,避免動態調整帶來的開銷。 2)基於C語言數組,直接操作內存,訪問速度快。 3)適合大規模數據處理和內存敏感環境,但需謹慎使用,因其大小固定。

PHP通過$\_FILES變量處理文件上傳,確保安全性的方法包括:1.檢查上傳錯誤,2.驗證文件類型和大小,3.防止文件覆蓋,4.移動文件到永久存儲位置。

JavaScript中處理空值可以使用NullCoalescingOperator(??)和NullCoalescingAssignmentOperator(??=)。 1.??返回第一個非null或非undefined的操作數。 2.??=將變量賦值為右操作數的值,但前提是該變量為null或undefined。這些操作符簡化了代碼邏輯,提高了可讀性和性能。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

禪工作室 13.0.1
強大的PHP整合開發環境

Atom編輯器mac版下載
最受歡迎的的開源編輯器

Dreamweaver CS6
視覺化網頁開發工具

ZendStudio 13.5.1 Mac
強大的PHP整合開發環境

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能