Router (路由)
Laravel Router设计非常优雅和美妙, 完美适配了Restfull API 设计规则, 模块职能划分非常清晰,
简直是领域驱动设计(DDD)经典的案例。
坦白来说,如果Laravel Router 代码非常难以理解,主要原因是对【路由模型】的核心机制不够理解
所以,阅读Router API 感觉到无所适从。
我们首先来梳理【路由模型】
路由的核心需求有:
Register Route
(路由注册)Dispatch Request To Route
(路由分发)
根据需求我们划分出3个模型:
Route
: 路由Entity(实体), 定义Route Entity 的属性和操作方法RouteCollection
: 路由Entity的集合, 职责是 保存 registered Route 和 匹配 RouteRouter
: Route Service,Router的职责是提供 register | find | dispatch 等API
Router
依赖 Route
和 RouterCollection
理解了这三个模型,就理解了路由了。
Route
Route
通俗来讲是【响应一次特定的URI请求】, 所以Route
包括【请求】和响应请求的【Action】两部分信息.
在Route中【请求】由两部分数据组成: 【HTTP Method】 和 【URI】 例如:
# Method URI
GET /v1/users/{user_id}
GET /v2/users/{user_id}/comments/{comment_id}
GET /v3/search
POST /v1/users
DELETE /v1/users/{user_id}
在Route中,【Action】有两类: -.【调用控制器的方法 -.【调用匿名函数】
【Action】还可以设置【别名】, 可以先忽略【别名】的知识点,下文会有更详细的说明
Route
把【Action】的信息保存在一个数组中, 格式如下:
# action是控制器的某个方法
action => Array(
as => profile, # 路由的别名
uses => ExampleController@index # 调用ExampleController 的index方法
)
# action 是匿名函数
action => Array(
as => profile
uses => Closure Object(), # 执行一个闭包函数, 这里留一个问题,闭包函数支持传递哪些参数
)
如果把【请求】和【Action】组合在一起,一个路由包含的信息大概是这样:
Route Object:
methods : array(GET) # 请求方法
uri : /v1/profiles/{id: \d+} # URI
action : Array (
[as] => profile
[uses] => ExampleController@index # 或者是一个闭包函数 Closure Object
# controller和users完全一样
[controller] => ExampleController@index # 或者是一个闭包函数 Closure Object
)
Route
的操作就是围绕Method
, URI
, Action
属性展开的。
先来看一下,如何初始化一个Route
:
# 请注意 Route 是怎么保存 method, uri, action的
public function __construct($methods, $uri, $action)
{
$this->uri = $uri;
$this->methods = (array) $methods;
# parseAction 返回的结构 ['as' => $as, 'uses' => $uses, 'controller' => $uses];
$this->action = $this->parseAction($action);
if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
$this->methods[] = 'HEAD';
}
if (isset($this->action['prefix'])) {
$this->prefix($this->action['prefix']);
}
}
Method
Method
是 HTTP 定义的 Method, 在Restfull设计原则中,一个资源的增删查改都对应标准的HTTP Method
需要注意的是,Method
和 Uri
是【多对一】的关系。
# 多个Method对应同一个URI, 这样做是因为有可能某些客户端不支持HEAD,只能传递GET方法
GET /usrs/{user_id}
HEAD /users/{user_id}
URI
URI是路由的重点也是难点。
第一个概念,URI有前缀 (prefix)
可以把URI的【版本信息】当作【前缀】 添加到 【URI】中
Route类里面有提供prefix
方法 可以为URI
添加前缀:
public function prefix($prefix)
{
$uri = rtrim($prefix, '/').'/'.ltrim($this->uri, '/');
$this->uri = trim($uri, '/');
return $this;
}
第二个概念, URI可以是一个【正则表达式】,可以设置正则表达式的规则。
# 匹配的url: /v2/users/1/comments/1
# 不能匹配 /v2/users/a/coments/b 因为user_id, comment_id必须是数字
/v2/users/{user_id}/comments/{comment_id}
$router = new Router()
# user_id必须是数字
$router->pattern("user_id", `\d+`);
# comment_id 必须是数字
$router->pattern("comment_id", `\d+`);
特别提一句,在lumen中,这个规则可以直接定义在uri里面:
/users/{user_id: \d+}
/v2/users/{user_id: \d+}/comments/{comment_id: \d+}
第三个概念:正则表达式匹配到的值会保存到parameters
数组中
uri: /v2/users/{user_id}/comments/{comment_id}
reqeust: /v2/users/1/comments/1 解析后 user_id = 1, comment_id = 1
parameters = [
"user_id": 1,
"comment_id": 3
]
那parameters
是什么时候被用到的呢,先留这个问题在这里。
【URI】的概念已经讲完了,怎么根据【request】匹配到对应的路由呢?
Route
提供 matches
实现了 【路由匹配】
# 核心代码中使用了preg_match_all 函数
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;
}
URI匹配成功后,就需要执行URI对应的action
Action
上文提到 Action
有两类,调用控制器的方法controller@method
和调用【闭包函数】。
[Action]是闭包函数, Route
提供 runCallable
# 比如定义这样的一个路由
App::get('/uses/{id}', function($id) {
return $id;
});
# 或者这样定义
App::get('/uses/{id}', array('as'=> 'users', function($id) {
return $id;
}));
# 这个路由被parse之后,生成的action
action = array(
'as' => 'users',
'uses' => Closure Object(
"parameters" => $id,
)
);
# 用到了上文提到了parameters 属性
protected function runCallable()
{
$callable = $this->action['uses'];
# parametersWithoutNulls 就是从parameters过滤了null的值
# 通过反射闭包函数,找到闭包函数的参数,然后从parametersWithoutNulls中拿去对应的值
return $callable(...array_values($this->resolveMethodDependencies(
$this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses'])
)));
}
【Action】是Controller@method
Route
提供 runController
实现了调用,
执行这类action的逻辑是这样的,找到controller并实例化controller执行controller对应的mehtod。
和闭包函数的参数一样,controller中method的参数通过反射自动从parametersWithoutNulls
获取。
# 假设定义这样的路由
App::get('/profiles/{id: \d+}', array("as" => "profile", "uses" => "ExampleController@index", ));
# 生成的action是
action = array(
'as' => 'profile',
'uses' => "ExampleController@index",
);
protected function runController()
{
return (new ControllerDispatcher($this->container))->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
Route 提供了一个run
方法,封装执行Action
的动作
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();
}
}
最后还有一个知识点,是action的as
有什么用。
as是为了支持路由重定向,类似于nginx的rewrite 是程序内部的跳转,不会修改到uri。
RouteCollection
有个数组nameList
会保存 as
到Action
的映射关系,于是,通过as
就可以直接找到Action
了
laravel 提供了一个route
函数可以对路由转发
比如这样:
# 在Collection中会保存这样的一个映射关系
$nameList['profile'] = 'ExampleController@index'
# 执行route(profile) 最终会执行ExampleController->index()
route('profile');
以上就是 Route
设计思路。搞清楚了Method
, Action
, URI
, parameters
就很自然能想到以下的方法:
methods: Get the HTTP verbs the route responds to
getAction: Get the action array for the route
setAction: Set the action array for the route.
getActionName: Get the action name for the route return 'ExampleController@index'
getActionMethod: Get the method name of the route action return 'index'
getController: Get the controller instance for the route return new ExampleController()
uri: Get the URI associated with the route
setUri: Set the URI that the route responds to.
prefix: Add a prefix to the route URI.
parameter: Get a given parameter from the route
parametersWithoutNulls: Get the key / value list of parameters without null values.
RouteCollection
RouteCollection的负责 【保存】 registered Route 和 【匹配】 Route
第一个问题,RouteCollection
怎么保存Route
。
RouteCollection
用到了4个数组 routes allRoutes nameList actionList
.
# routes是一个二维数组,使用Method作为第一维度key,URL作为第二维度key
$routes = [];
$domainAndUri = $route->domain().$route->uri();
foreach ($route->methods() as $method) {
$this->routes[$method][$domainAndUri] = $route;
}
# allRoutes是一维数组,key是Method 和URI拼接在一起
$allRoutes = []
$this->allRoutes[$method.$domainAndUri] = $route;
# 一维数组,key是Route的Action中设置别名
$nameList = []
$action = $route->getAction();
if (isset($action['as'])) {
$this->nameList[$action['as']] = $route;
}
# 一维数组, key是action中定义的controller,
# 在laravel中 $action['controller'] = $action['uses']
$actionList = []
if (isset($action['controller'])) {
$this->addToActionList($action, $route);
}
protected function addToActionList($action, $route)
{
$this->actionList[trim($action['controller'], '\\')] = $route;
}
RouteCollection
提供了add
方法 实现了【保存路由】。
第二个问题,怎么匹配到Route
.
RouteCollection
的处理逻辑是:
先根据request method 在routes 查找匹配到method映射的routes
遍历查找到的routes, 根据request uri 匹配最终的route
RouteCollection 提供了 match
方法 实现 【匹配】
public function match(Request $request)
{
# 先通过method 匹配
$routes = $this->get($request->getMethod());
# 遍历routes, 匹配request pathinfo
$route = $this->matchAgainstRoutes($routes, $request);
if ( ! is_null($route)) {
# 匹配到之后,把request绑定到route中,并会解析出parameters
return $route->bind($request);
}
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0)
{
return $this->getOtherMethodsRoute($request, $others);
}
throw new NotFoundHttpException;
}
# 根据method过滤
public function get($method = null)
{
return is_null($method) ? $this->getRoutes() : Arr::get($this->routes, $method, []);
}
# 遍历routes,调用了Route的matches函数匹配
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
return Arr::first($routes, function ($value) use ($request, $includingMethod) {
# 最终又调用了Route提供的matches方法完成匹配
return $value->matches($request, $includingMethod);
});
}
理解上面的逻辑之后,那下面的函数,我们也很好理解了:
# lookups
getRoutes: Get all of the routes in the collection. 【获取所有routes】
getByAction: Get a route instance by its controller action [Action查找]
getByName: Get a route instance by its name 【别名查找】
# 刷新
public function refreshNameLookups()
{
$this->nameList = [];
foreach ($this->allRoutes as $route) {
if ($route->getName()) {
$this->nameList[$route->getName()] = $route;
}
}
}
# 刷选
public function refreshActionLookups()
{
$this->actionList = [];
foreach ($this->allRoutes as $route) {
if (isset($route->getAction()['controller'])) {
$this->addToActionList($route->getAction(), $route);
}
}
}
# 继承了Countable, IteratorAggregate
# 可以count一个对象
public function count()
{
return count($this->getRoutes());
}
# 可以遍历RouteCollection对象
public function getIterator()
{
return ArrayIterator($this->getRoutes());
}
RouteCollection 核心概念讲完。
Router
Router 作为【领域服务】角色,主要是提供一系列API 完成 【路由模型】,主要操作有:
-. CreateRoute -. AddRoute -. Dispatch
首先,搞清楚如何【实例化】 Router
.
public function __construct(Dispatcher $events, Container $container = null)
{
# 处理事件
$this->events = $events;
# 实例化RouteCollection,添加和查找Route都需要依赖RouteCollection的函数
$this->routes = new RouteCollection;
# 容器
$this->container = $container ?: new Container;
}
CreateRoute
在Route
部分,我们讲到一个Route
涉及到三个属性:method
, URL
, Action
Router在实例化之前,会对Route的会对URL
, Action
进行一些特殊处理。
protected function createRoute($methods, $uri, $action)
{
# 如果action是ReferencesController,会对action做解析
if ($this->actionReferencesController($action)) {
$action = $this->convertToControllerAction($action);
}
# 会对uri加上前缀
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
);
}
# 对action主要的处理是添加controller的namespace
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;
}
# 对uri的处理主要是添加prefix
protected function prefix($uri)
{
return trim(trim($this->getLastGroupPrefix(), '/').'/'.trim($uri, '/'), '/') ?: '/';
}
Router可以通过group
函数对uri分组
Route::group(array('prefix' => '/v1', namespace="App/Http/Controller"), function () {
Route::group(array('prefix' => '/user'), function () {
Route::get('/password', 'UserProfileController@password');
}
}
# 解析后:
uri = /v1/user/password
action = App/Http/Controller/UserProfileController@password
group的实现方式是使用groupStack
数组保存了预设值属性
public function group(array $attributes, $routes)
{
# merge groupStack的属性
$this->updateGroupStack($attributes);
# 执行closure
$this->loadRoutes($routes);
# 回退groupStack
array_pop($this->groupStack);
}
protected function updateGroupStack(array $attributes)
{
if (! empty($this->groupStack)) {
# 返回的结构为 array("namespace" => $namespace, "prefix" => $prefix, "where" => $where, "as" => $as)
$attributes = RouteGroup::merge($attributes, end($this->groupStack));
}
$this->groupStack[] = $attributes;
}
protected function loadRoutes($routes)
{
if ($routes instanceof Closure) {
# 执行closure,传递router对象
$routes($this);
} else {
$router = $this;
require $routes;
}
}
addRoute
Router
提供了HTTP verbs有
$verbs = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
例如添加一个POST
:
# 添加了POST方法
public function post($uri, $action = null)
{
return $this->addRoute('POST', $uri, $action);
}
# routes是RouteCollection的实例化对象
protected function addRoute($methods, $uri, $action)
{
return $this->routes->add($this->createRoute($methods, $uri, $action));
}
laravel对注册【资源】的路由做了遍历处理,这里的【资源】对应【restfull】设计原则中的【资源】
在laravel中定义了资源处理的actions,也就是说资源的controller类中需要实现以下的方法, Router提供了resource
函数处理这块逻辑
$resourceDefaults = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
# 定义restaurants resource, only表示只需要实现index, show, destroy方法,除了only 还支持except
Route::resource('/favor/restaurants', 'RestaurantController', array('only' => array('index', 'show', 'destroy')));
# 实际上定义了如下路由:
GET /favor/restaurants UserFavorRestaurantController@index
GET /favor/restaurants/{id} UserFavorRestaurantController@show
DELETE /favor/restaurants/{id} UserFavorRestaurantController@destroy
Router
主要提供了get post delete put patch resource
方法实现了【注册路由】
Dispatch
dispatch Route 是相对好理解的模块, Router
提供【dispatch
】函数实现此功能。
public function dispatch(Request $request)
{
# 保存了request对象,在lavavel中通常会用一个属性保存依赖类实例化对象
$this->currentRequest = $request;
# disptch to route
return $this->dispatchToRoute($request);
}
public function dispatchToRoute(Request $request)
{
# Find the route matching a given request
$route = $this->findRoute($request);
# 在request对象中保存了依赖的route对象
$request->setRouteResolver(function () use ($route) {
return $route;
});
# fire event
$this->events->dispatch(new Events\RouteMatched($route, $request));
# Run the given Route within a Stack "onion"
# 涉及到中间件知识
$response = $this->runRouteWithinStack($route, $request);
# Create a response instance from the given value
return $this->prepareResponse($request, $response);
}
protected function findRoute($request)
{
# 依赖RouteCollection的match函数
$this->current = $route = $this->routes->match($request);
# resiger mathed Route to Container
$this->container->instance(Route::class, $route);
return $route;
}
Router
还涉及到【中间件】的知识,这部分内容下次会有专门的博文讲解,现在,你理解了Router了吗
group: 路由分组
get/post/delete/resource: 注册路由
prefix: 添加uri前缀
pattern: 添加uri正则表达式规则
dispatch: Dispatch the request to the application.
dispatchToRoute Dispatch the request to a route and return the response.
一次HTTP请求中完整流程
在Illuminate\Foundation\Http\Http\Kernel中会实例化Router
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
}
# 核心执行流程:
Http Kernel -> handle(request) -> sendRequestThroughRouter(request) -> dispatchToRouter(request) -> [router->dispatch(reqeust)]
【路由模型】真的很强大,设计很完美,你会灵活使用了吗
使用案例
Route::group(array('prefix' => '/v1'), function () {
Route::group(array('prefix' => '/user'), function () {
Route::resource('/', 'UserProfileController', ["only" => array("index", "show", "destroy")]);
Route::put('/password', 'UserProfileController@setPassword');
}
}
Route::group(array('prefix' => '/v2'), function () {
Route::delete('/orders/{order_id}', 'OrderController@destroy');
}
# 获取action
$this->container->make(Route::class)->getAction()
# 获取Controller
$this->container->make(Route::class)->getController()
# 获取注册的Routes
$this->container->make(Router::class)->getRoutes()->getRoutes()
# 获取注册的Routes的数量
$this->container->make(Router::class)->getRoutes()->count()
# 获取currentRequest
$this->container->make(Router::class)->getCurrentRequest()
# 获取parameters
$this->container->make(Route::class)->parametersWithoutNulls()
# 根据name获取parameter
$this->container->make(Route::class)->user_id
任何你想要的信息都可以拿到,这就是laravel的优雅所在!!!