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 和 匹配 Route

  • Router: Route Service,Router的职责是提供 register | find | dispatch 等API

Router 依赖 RouteRouterCollection

理解了这三个模型,就理解了路由了。

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

需要注意的是,MethodUri是【多对一】的关系。

# 多个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 会保存 asAction的映射关系,于是,通过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的优雅所在!!!

results matching ""

    No results matching ""