15. 路由系统Route

drupal8使用的路由系统很强大,在系统中是一个比较大的子系统,它基于Symfony路由组件,所以请务必先学习Symfony的路由组件。

Symfony路由组件的官网文档地址是:http://symfony.com/components/Routing

路由是什么呢?就是一个请求到达后决定由谁去处理的系统,它决定着处理流程的去向,一些比较小的系统路由设计会针对网址特定的格式或元素判断谁来处理,比如常见的MVC,著名的CI框架就在网址中包含控制器、视图、模型信息来判断流程去向,然而drupal8使用的路由系统非常强大,远不止于此,它不但对url没有特征要求,还可以针对请求方法、浏览器代理标识符、上传内容类型、网址部分的正则格式等等来控制,这样能使用任意有表达力的字符串,与系统内部特征解耦,是一个全功能的路由系统。

drupal8路由系统目的很简单,就是将一个请求转换为一组变量,这些变量包含需要的控制器、默认参数、访问控制等等内容,系统将这些变量保存在请求对象的$request->attributes属性包中,以供后续参数解析、控制器解析流程使用。

在本系列的《云客Drupal8源码分析之核心处理流程HttpKerneldrupal8执行流程)》中已经说明系统执行的主流程,路由系统是在核心派发出 'kernel.request' 事件时参与执行的,注册的侦听器为router_listener,在其onKernelRequest方法中执行流程进入路由系统,整个路由系统以服务router作为api接口,drupal8权限检查在路由中进行,权限管理服务的服务id为:access_manager,由于权限控制是一块很大的内容,另外开一个主题讲解,本篇仅讲路由系统,也就是没有访问检查的路由系统,服务名为:router.no_access_checks,我们先从路由使用角度来看一看怎么定义一个路由:

在核心和模块中使用name.routing.ymlname为模块名)来定义自己相关的路由,在这个文件中可以定义多个路由,下面看一看单个路由是怎么定义的,这是一个全面的例子,见辅助内容区:

它们存储在yaml文件中,必须遵循yaml格式,否则yaml解析器会报错,下面来解释每个部分:

yunke.content:
  path: '/yunke/{book}/{page}'
  host: "{subdomain}.example.com"
  schemes:  [https]
  methods: [GET, HEAD]
  defaults:
    _controller: '\Drupal\yunke\Controller\book::show'
    _title: 'Hello World !'
    book:'drupal'
    page:1
    subdomain: m
  requirements:
    _permission: 'access content'
    page: \d+
    subdomain: m|mobile
    _format: 'json|html'
  options:
    _route_filters: 'yunke__route_filters'
    no_cache: TRUE

 

路由定义中各元素含义:

yunke.content:表示路由名,在系统内部用作机器名,代表这一条路由,一个yaml文件中可以定义多个路由,但drupal整个系统中路由名必须唯一,以下为它的第一级元素:

path:必须项,字符串值,且必须以/开始的一个路径,可变动的部分用花括号包起来,形成一个路径变量,里面的字符为占位符名,第一个路径段不能使用路径变量。

host:可选,字符串值,限定可匹配此路由的主机名,同样可以使用{}来包含可变部分形成一个路径变量,未指定则任意。

schemes:可选,数组值,可以使用的协议名,未指定则不限制。

methods:可选,数组值,可以使用的请求方法名,未指定则任意。

defaults:必须项,数组值,代表默认值,里面可以包含系统默认值如控制器、标题等,及路径变量默认值(用占位符名指定)。

requirements:必须项,数组值,约束选项,包含权限需求、路径变量须对应的正则格式、请求内容格式等等。

options:可选,数组值,本条路由的默认选项,用给路由系统本身提供额外信息,比如自动路由过滤器、缓存属性等等。

来看一看以上元素可能的值(第二级值)

defaults能包含以下值:

    _controller: 指定一个回调 (最常见的是classname::method,它不是路径,而是完全限定的名字空间类名,会被自动加载器加载)可以返回一个渲染数组,它将被模板渲染,也可以返回一个响应对象,它会直接发送给客户端

    _form: 指定一个Drupal\Core\Form\FormInterface的实现

    _entity_view: 指定entity_type.view_mode. 用给出的视图模式查找实体并渲染

    _entity_list: 指定entity_type

    _entity_form: 类似_form 但为实体提供一个编辑表

    _title (可选): 路由后对应页面的标题

    _title_context (可选): 额外标题文本的上下文信息,它传递给t()函数

    _title_callback (可选): 一个回调,用于产生_title

    此外可以指定路径变量的默认值,当请求路径不能提供路径变量的值时使用,也可以额外提供控制器需要的任意参数值,名字对应控制器签名中的参数名,顺序无关,内部使用反射机制处理,在这里所有不是以下划线开始的变量都会传给控制器。

requirements能包含以下值:

    _permission: 权限字符串 ( _permission: 'access content'). 多个权限可以用逗号 ','分隔,它们是AND关系,也可以用加号'+'分隔表示OR关系,有点反直觉但就是这样的,权限字符串定义在my_module.permissions.yml,详见https://www.drupal.org/node/2311427

    _role: 指定用户角色,如'administrator'. 能指定多个,同上,在站点间角色可能不一样,推荐使用_permission来控制权限

    _access: 如果设置为"'TRUE'" (注意单引号),那么在任意情况可访问

    _entity_access: 如果路由指向一个实体,能检查某访问级别( _entity_access: 'node.view')

    _custom_access: 自定义访问检查,详见访问控制

    _format: 指定请求类型(希望获取数据的类型),比如 _format: json将仅仅匹配Accept headerjson的请求

    _content_type_format: 指定请求的内容类型(上传数据的类型),比如 _content_type_format: json 将匹配请求Content-type headerjson的请求。

    _module_dependencies: 可选的使用某依赖的一个或多个模块,多个module names + 表示AND ,用逗号表示OR,注意这个和上面的不一样._module_dependencies: 'node + search'意味着nodesearch都需要, 如果在info.yml中已经指定依赖关系就不需要这个选项

    _csrf_token: 跨站攻击设置,'TRUE',详见路由访问检查

    此外可以包含对路径变量的格式限定,使用正则表达式为值,路径占位标识符为键

options能包含以下值:

    _access_mode: 值为:ANY / ALL 默认为ALL ,这个选项已经被移除了,现在总是ALL

    _admin_route: 是否使用管理主题

    _theme: 指定主题

    no_cache: 设置为'TRUE' 表示本路由得到的请求结果不可缓存

    parameters: 参数转换,此选项可以将路径占位变量的值转换为一个对象,见下文

 

路由的一些不规则用法

比如在requirements中指定_method 'GET|POST'来控制请求方法也是可以的,但不要这样用,它在Symfony2.2中就弃用了,在3.0版本中会完全移除,这里列出只是为你解惑,请遵从上面的例子来定义路由。

如何定义路由可以参看官方文档:https://www.drupal.org/docs/8/api/routing-system/structure-of-routes

以上就是路由系统普通用户使用层面的内容了,那么在系统中路由系统是如何实现的呢?继续

一个完整的路由系统应该是双向的,既可以使用请求得到路由信息,也可以用路由信息生成一个URL,在drupal中就是如此。

这样路由就有三个基本部分:

路由对象(保存路由信息)、匹配器(用以匹配请求到一个具体的路由对象)、url产生器(用路由对象生成一个url

Symfony的路由组件就是围绕这三个部分展开,drupal就基于这个组件,代码位于\vendor\symfony\routing

来看一看这个文件夹里面的文件:

Route.php就是路由对象,用于保存路由信息,我们在上面定义的路由信息最终会转化为这个对象

RouteCollection.php:路由集对象,系统中会有很多路由线路,多个路由对象需要它来统一管理,匹配器就在这个路由集里面寻找能匹配的路由对象,URL产生器也会根据路由名从里面取得一个路由。匹配器和产生器各位于一个文件夹,里面有不同用途的多个匹配器和产生器。

 

匹配器是怎么工作的呢?它其实是用路由集对象里面的每个路由对象去测试是否匹配请求。怎么测试?它将路由对象保存的路径信息和域名信息各生成对应的正则表达式,再用正则表达式去匹配请求的url看是否匹配。生成正则表达式这个工作由路由对象编译器进行,它需要考虑路径变量的正则约束,这就是RouteCompiler.php的工作。生成后的路由对象叫做编译后路由对象,也就是CompiledRoute.php

比如路径为:$path="/new/{cms}/yunke_{type}/show"; 约束条件是:$requirements=["cms"=>"\S+","type"=>"[a-zA-Z\d]+"];

生成(编译)的正则表达式为:#^/new/(?P<cms>\S+)/yunke_(?P<type>[a-zA-Z\d]+)/show$#s

主机为:$host="{sub}/baidu.com"; 约束条件是$requirements=["sub"=>"m|www"];

生成(编译)的正则表达式为:#^(?P<sub>m|www)/baidu\.com$#si

 

约束条件就是在定义中requirements项中指定的正则。不熟悉正则可能不理解(?P<cms>\S+)是什么意思,它表示在匹配结果将这个子组匹配到的内容赋值给一个叫做cms的变量。系统是使用preg_match函数匹配的,这也就是为什么能将utl中路径变量对应的值传给控制器的核心原因。除了正则匹配外还会检查请求方法、请求格式等等内容,为此有一个叫做请求上下文的对象来提供请求对象的这些信息。也就是RequestContext.php的工作,它往往用请求对象来建立,那为什么不直接使用请求对象呢?答案是可以在没有请求对象的情况下构造请求上下文,对他的修改也不会影响到请求对象。

总之匹配器会从路由集里面选取一条路由,如果有多条路由则使用优先级,url产生器比较简单这里不做讲解。在Symfony的路由组件中还提供了一个Router.php,它是做什么用的呢?它是这个组件的综合运用。

说到这里你可能会有一个疑问:正则匹配是非常耗资源和时间的,在drupal中有成千上万的路由,这样做系统不是会非常非常的慢吗?确实不错,所以drupal进而用到了Symfony框架(框架不是组件)的路由部分,它叫做动态路由,由它来解决这个问题,代码位于:\vendor\symfony-cmf\routing

symfony框架路由

在这里有一个DynamicRouter.php,称为动态路由,对应着Symfony路由组件中的Router.php,它是框架路由的综合运用

为了更加实用产生了一个叫做路由提供器的概念,它用接口:RouteProviderInterface.php规范,顾名思义它就是用来产生路由集对象的对象,上面说到了drupal中有许多的路由,为了减少潜在可能匹配的路由,提高性能,系统有两方面的考虑:优化路由提供器、使用路由过滤器,这个优化主要由路由提供器完成,来看一看drupal的路由提供器:

\core\lib\Drupal\Core\Routing\RouteProvider.php就是drupal的核心路由提供器,它根据请求对象从数据库中取出可能会匹配上的路由,它是怎么工作的呢?我们先看看路由表的设计:

路由数据库表

路由表为router,其字段结构及意义如下:

name:主键,路由的机器名   

pathURI路径   

pattern_outline:路由路径的大致模式,非具体模式    ,用于初步筛选可能会匹配的路径

fit:代表这个路径有多具体的一个数值,约等于路由的优先级   

route:路由对象Symfony\Component\Routing\Route的序列化表示

number_parts:路径的段数(对应着path字段能被/分割成的段数)

核心路由提供器根据请求计算出URL段长度(用/分割的段数)number_parts、及可能匹配的大概模式pattern_outline,通过这两项从数据库中查找可能的路由,核心代码如下,它位于getRoutesByPath方法中:


    $parts = preg_split('@/+@', $path, NULL, PREG_SPLIT_NO_EMPTY);//计算段数
    $ancestors = $this->getCandidateOutlines($parts); //计算大概模式
    $routes = $this->connection->query("SELECT name, route, fit FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE pattern_outline IN ( :patterns[] ) AND number_parts >= :count_parts", array(
        ':patterns[]' => $ancestors, ':count_parts' => count($parts),
      ))->fetchAll(\PDO::FETCH_ASSOC);  //查询路由

在这个方法中还会根据fit值对路由排序,fit值代表路由的详细程度,越详细优先级越高。计算工作是繁杂的,所以路由提供器会将查询结果存入缓存系统中,下次访问不必再计算,缓存表为cache_data。

drupal路由系统的调用过程

router_listener:从这里开始依次调用以下服务:

routerDrupal\Core\Routing\AccessAwareRouter具备访问检查的路由Router;

router.no_access_checksSymfony\Cmf\Component\Routing\ChainRouter纯粹的链式Router,它其中可以包含很多Router;

router.dynamicSymfony\Cmf\Component\Routing\DynamicRouter这就是Symfony框架路由的动态路由了;

router.matcherSymfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher框架路由的匹配器;

router.route_providerDrupal\Core\Routing\RouteProvider最终从数据库获取路由数据的核心路由提供器;

提供器将路由集对象传递给核心匹配器router.matcher,匹配器在最终执行匹配前会应用路由过滤器去进一步减少不可能的路由。

路由过滤器

系统默认提供了三个路由过滤器:

request_format_route_filterDrupal\Core\Routing\RequestFormatRouteFilter)检查格式是否支持;

method_filterDrupal\Core\Routing\MethodFilter)检查是否支持方法,默认的如果指定GET方法则隐含的支持了HEAD方法;

content_type_header_matcherDrupal\Core\Routing\ContentTypeHeaderMatcher)应用于所有路由,根据浏览器上传数据的内容类型来过滤路由,GET方法不会上传数据,所以不产生过滤;

过滤器除了过滤功能外,也会像提供器一样对路由排序,越详细的路由优先级越高;

用户可以定义额外的路由过滤器,实现Drupal\Core\Routing\RouteFilterInterface接口并给出服务标签route_filter即可,路由过滤器服务定义参考如下:

  method_filter:
    class: Drupal\Core\Routing\MethodFilter
    tags:
      - { name: route_filter, priority: 1 }

路由过滤器的收集工作由容器编译阶段完成。

编译器为\core\lib\Drupal\Core\DependencyInjection\Compiler\RegisterLazyRouteFilters.php

经过过滤后的路由集对象送入服务router.matcher.final_matcherDrupal\Core\Routing\UrlMatcher)执行最终的匹配。

执行最终的匹配后匹配器返回一个参数数组给router.dynamic,此时router.dynamic对参数数组应用路由增强器?

路由增强器

用于修改路由匹配后得到的参数,或增加或删除或修改,这样让路由系统得到更大的灵活性,系统默认提供了以下增强器,以容器服务方式使用,以下为服务ID

route_enhancer.param_conversion:Drupal\Core\Routing\Enhancer\ParamConversionEnhancer
route_enhancer.form:Drupal\Core\Routing\Enhancer\FormRouteEnhancer
route_enhancer.entity:Drupal\Core\Entity\Enhancer\EntityRouteEnhancer
route_enhancer.entity_revision:Drupal\Core\Routing\Enhancer\EntityRevisionRouteEnhancer
field_ui.route_enhancer:Drupal\field_ui\Routing\FieldUiRouteEnhancer

用户可以定义自己的增强器,参考如下:

  route_enhancer.entity:
    class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer
    tags:
      - { name: route_enhancer, priority: 20 }

需要实现增强器接口,并给出服务标签route_enhancer

服务容器编译器Drupal\Core\DependencyInjection\Compiler\RegisterLazyRouteEnhancers会将所有增强器收集并传递给服务route_enhancer.lazy_collector统一管理。

路由匹配后返回的参数经过增强器处理最后返回到服务routerAccessAwareRouter)中,它将其保存在$request->attributes中。router是一个具备访问检查的路由router,它会调用访问管理器(服务access_manager)执行访问检查,并将access_manager返回的结果一并保存在$request->attributes中,如果访问不能通过则抛出访问拒绝异常。如果通过则流程回到主干流程,路由系统运行完毕,保存的路由参数将会被后续流程使用(参数解析及控制器)。

以上就是drupal路由系统的全部了,由于篇幅有限,许多细节并未提到,但读者应该有了一个大概印象。

路由参数转换

drupal8有一个参数转换子系统可以将路径中的变量转换成对应的对象,比如将/node/{node}中的{node}的值转换成一个node对象,然后再传递给控制器,这是如何实现的呢?就是用了ParamConversionEnhancer这个路由增强器,使用上文提到的路由选项中的parameters值来做这件事情,用户可以在路由定义中决定参数是否转换,以及如何转换,下面看一下如何定义:

  options:
    parameters:
      var_name:
        tempstore: TRUE
        type: entity:view

以上定义中var_name必须是占位符名,它的子元素可以用来判断使用哪一个参数转换器,其中可以使用converter键名明确指定一个参数转换器,它的值是一个容器服务id,该服务必须实现Drupal\Core\ParamConverter\ParamConverterInterface,如果没有使用converter明确指定转换器,那么使用系统提供的所有转换器依次根据这些子元素来判断是否可以运用,如果可以则运用,转换器按优先级排序只运用一个。下面来看看参数转换系统的实现:

系统使用Drupal\Core\ParamConverter\ParamConverterManager来管理参数转换,容器id为:paramconverter_manager,在容器编译阶段将收集所有标签名为paramconverter的服务并注入到它里面,这些服务(也可以叫参数转换器)均实现了上文提到的参数转换接口,用户可以自定义参数转换器,需要给出paramconverter标签,可选给出优先级。

完成收集工作的编译器为:\core\lib\Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass.php(此处给管理器注入转换器时使用 addConverter(ParamConverterInterface $param_converter, $id),其中的$id参数就是转换器服务id,请看编译器的实现)

经过ParamConversionEnhancer这个路由增强器的处理,路由系统返回的参数已经是被处理过的了,但系统并不会丢失转换之前的值,他们将储存在_raw_variables中,这里需要注意的是原始变量并不包括定义在路由中除占位符变量之外的额外变量,这些额外变量也不会得到转换,请看辅助功能区:

其中otherVar并不会进行参数转换,也不会包含在_raw_variables中,因为它不是占位变量。

从请求得到原始变量请用:$request->attributes->get('_raw_variables')

yunke.c:
  path: '/a/b/{area}'
  defaults:
    _controller: '\Drupal\yunke\Controller\HelloController::showme'
    _title: 'yunkec'
    otherVar:  'yunke'

 

补充内容

一、如果你想看一看被编译后的路由对象是什么样子,可以替换drupal的首页文件index.php,运行代码如辅助功能区:

这可以帮助你更深入的理解匹配过程,记得看完将index.php换回去。

二、路由系统返回的参数,它们至少包含以下内容:

_route:一定会包含,表示匹配上的路由名;

_route_object:路由对象;

_access_result:访问检查结果对象Drupal\Core\Access\AccessResultAllowed

_route_params:路由参数,除了路由名和控制器外均已经设置在请求属性包里面;

_raw_variables:占位符变量的原始值,没有经过参数转换系统处理过;

三、是否提供路径变量的缺省值会影响路由表的pattern_outline字段值,有些莫名其妙的问题请检查是否错误的提供了缺省值。如:path: '/hill/{area}/name' 提供缺省值的话会引起路由不能匹配,所以此处不能提供area的缺省值,能匹配则它必有值,不会用到默认值。

四:路径/a/b/{area}//a/b/{area}等效,如果同时存在,以/a/b/{area}为准;

五:如果两个路由都能匹配,那么路由名对优先级会造成影响,以路由名的字母顺序排序;

六:路由/friend/{area}/{name}能匹配以下路径:friend/a/b/friend/a/b 如果没有提供默认值则不能匹配更短路径,如果提供默认值则还可以匹配以下路径:

friend/a/

friend/a

friend/

friend

七:在yaml文件中键值中间的冒号后面必须留有只少一个空格,否则引起yaml解析错误,导致网站提示不可预期的错误,这是很多初学者极容易犯的错误。

八:在drupal中大部分内容是以节点序号表示的,可以为其取别名,那么别名是怎么处理的呢?别名信息储存于url_alias表中,在进行路由判断时会先将别名转换为本来的路径,此工作在路由提供器的getRouteCollectionForRequest方法中进行,代码如下:

$path = $this->pathProcessor->processInbound($path, $request);

 

<?php
use Symfony\Component\Routing\Route;

$autoloader = require_once 'autoload.php';

$path="/new/{cms}/yunke_{type}/show";
$defaults=["cms"=>"drupal","type"=>"dev"];
$requirements=["cms"=>"\S+","type"=>"[a-zA-Z\d]+"];
$host="{sub}/baidu.com";
$r=new Route($path,$defaults,$requirements);
$r->setHost($host);
$r->setDefault("sub", "www");
$r->setRequirement("sub", "m|www");
$c=$r->compile();
print_r($c);
?>

 

本书共91小节:

评论 (写第一个评论)