This article mainly introduces the interpretation of Laravel routing (Route), which has certain reference value. Now I share it with everyone. Friends in need can refer to it.
Route is the way for the outside world to access Laravel applications. In other words, routing defines the specific way in which Laravel's application provides services to the outside world: the handler defined by the routing can be correctly accessed through the specified URI, HTTP request method, and routing parameters (optional). Whether the handler corresponding to the URI is a simple closure or the controller method does not have a corresponding route, the outside world cannot access them. Today we will take a look at how Laravel designs and implements routing.
We usually define the route as follows in the routing file:
Route::get('/user', 'UsersController@index');
Through the above routing, we can know that the client requests the URI "/user" through HTTP GET. At this time, Laravel will finally dispatch the request to the index method of the UsersController class for processing, and then return the response to the client in the index method.
The Route class used when registering routes above is called Facade in Laravel. It provides a simple way to access the service router bound to the service container. The design concept and implementation of Facade I plan to write a separate blog post about the method in the future. Here we only need to know that the static methods of the Route facade that are called correspond to the methods of the router service in the service container, so you can also think of the above route as registering like this:
app()->make('router')->get('user', 'UsersController@index');
The router service is bound to the service container by registering the RoutingServiceProvider in the constructor when instantiating the application Application:
//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); }); }
Through the above code, we know the Route call The static methods all correspond to the methods in the \Illuminate\Routing\Router
class. The Router class contains methods related to route registration, addressing, and scheduling.
Let’s take a look at how this is implemented in laravel from the stages of routing registration, loading, and addressing.
You need to load the routing file before registering the route. The routing file is loaded in the boot method of App\Providers\RouteServiceProvider
This server provider :
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 first looks for the cache file of the route, and then loads the route if there is no cache file. The cache file is usually in the bootstrap/cache/routes.php file.
The loadRoutes method will call the map method to load the routes in the routing file. The map function is in the App\Providers\RouteServiceProvider
class. This class inherits from Illuminate\Foundation\Support\Providers\ RouteServiceProvider
. Through the map method, we can see that laravel divides routing into two large groups: api and web. The routes for these two parts are written in two files respectively: routes/web.php and routes/api.php.
In Laravel 5.5, routes are placed in several files. The previous version was in the app/Http/routes.php file. Putting it in multiple files can make it easier to manage API routing and WEB routing
We usually use the Route Facade to call the static method get, post, head, options, put, patch, delete...etc. to register routes. As we said above, these static methods actually call methods in the Router class:
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); } ....
You can see The registration of routes is uniformly handled by the addRoute method of the router class:
//注册路由到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; }
The third parameter action passed to addRoute when registering a route can be a closure, a string, or an array. The array is similar to ['uses ' => 'Controller@action', 'middleware' => '...'] in this form. If the action is a route of type Controller@action
, it will be converted into an action array. After convertToControllerAction is executed, the content of the action is:
[ 'uses' => 'App\Http\Controllers\SomeController@someAction', 'controller' => 'App\Http\Controllers\SomeController@someAction' ]
You can see that the namespace is added to the name of the controller. The complete controller class name is formed before. After the action array is constructed, the next step is to create the route. To create a route, use the specified HTTP request method, URI string and action array to create it\Illuminate\Routing\Route
Instance of the class:
protected function newRoute($methods, $uri, $action) { return (new Route($methods, $uri, $action)) ->setRouter($this) ->setContainer($this->container); }
After the route is created, add the Route to the RouteCollection:
protected function addRoute($methods, $uri, $action) { return $this->routes->add($this->createRoute($methods, $uri, $action)); }
The $routes attribute of the router is a RouteCollection object, which will be updated when adding a route to the RouteCollection object. The routes, allRoutes, nameList and actionList attributes of the RouteCollection object
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); } } }
The four attributes of RouteCollection
routes stores the mapping between HTTP request methods and routing objects:
[ 'GET' => [ $routeUri1 => $routeObj1 ... ] ... ]
allRoutes The content stored in the attribute is the content after programming the two-digit array in the routes attribute into a one-digit array:
[ 'GET' . $routeUri1 => $routeObj1 'GET' . $routeUri2 => $routeObj2 ... ]
nameList is a mapping table between route names and routing objects
[ $routeName1 => $routeObj1 ... ]
actionList is the route Mapping table of controller method string and routing object
[ 'App\Http\Controllers\ControllerOne@ActionOne' => $routeObj1 ]
In this way, the route is registered.
In the middleware article, we said that the HTTP request reaches the destination after going through the pre-operation of the middleware on the Pipeline channel:
//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); }; } }
Above You can see from the code that the destination of the Pipeline is the closure returned by the dispatchToRouter function:
$destination = function ($request) { $this->app->instance('request', $request); return $this->router->dispatch($request); };
The dispatch method of the router is called in the closure, and routing addressing occurs in findRoute, the first stage of dispatch:
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()
里会将路由的规则转换成正则表达式。
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对象返回给客户端。这个过程还会涉及到我们以前介绍过的中间件过滤、服务解析、依赖注入方面的信息,如果在看源码时有不懂的地方可以翻看我之前写的文章。
依赖注入
服务容器
中间件
以上就是本文的全部内容,希望对大家的学习有所帮助,更多相关内容请关注PHP中文网!
相关推荐:
The above is the detailed content of Interpretation of Laravel routing (Route). For more information, please follow other related articles on the PHP Chinese website!