跳转到主要内容
sina_门前小雨 提交于 14 April 2016

日程分发(Routing)

我们在前面的章节中对比了Drupal 8与Symfony框架的结构。能够了解Drupal 8是如何处理请求对我们的学习是有益的。首先需要了解启动流程(bootstrapping和总体控制流(flow of control。然后学习事件订阅(event subscribers),事件订阅的概念对学习请求处理是非常重要的。

控制流(Flow of control)

现在让我们了解一下当一个请求被送达至Drupal 8的时候会发生什么。

1. 启动流程配置

w 读取settings.php文件,动态生成一些其它设置,然后把它们存储在全局变量和Drupal\Component\Utility\Settings对象中。

w 启动类装载器(class loader),它负责装载类。

w 设置Drupal错误句柄(error handler)。

w 检测Drupal是否安装,如果Drupal没有安装则重定向至安装脚本。

2. 创建Drupal内核

3. 初始化服务容器(从缓存中或是彻底重建)

4. 为Drupal静态类添加容器。

5. 尝试从静态页面缓存中提取内容。

6. 加载所有变量(variable_get)。

7. 加载其它需要的或相关的文件。

8. 注册流封装(stream wrappers)(public://, private://,temp://和用户封装)

9. 使用Symfony的Http基础组件创建HTTP请求对象。

10. Drupal内核控制HTTP请求对象返回回应(response)。

11. 送出回应

12. 结束请求(模块可以从这个事件激活)。

 

对网络请求处理是最有趣的部分,不过在了解它之前你需要知道什么是事件订阅。

事件订阅(Event subscribers)“

先前我们了解了编译过程。其中“订阅事件的被标记服务(event_subscriber-tagged)”是标记服务的一种重要应用。这些服务实现了EventSubscriberInterface(事件订阅接口),是最基础的事件监听器。事件订阅使用getSubscribedEvents方法定义事件映射到哪些方法。并由优先级设置决定调用次序。

Drupal核心仅使用了几个事件:

w   kernel.request 请求发生的非常早期激活。

w   kernel.response 当请求的回应被创建时发生。

w   routing.route_dynamic 被触发以允许模块添加日程。

w   routing.route_alter 被日程集合触发以允许修改日程。核心使用它添加一些检查或参数转换。

所有的Drupal开发者都应当了解事件订阅。其中kernel.request特别重要,因为它取代了hook_init的角色。事件routing.route_dynamic也很重要,这个事件可以被引用来创建动态日程,而通常日程分发的配置是静态存储在YAML文件中的。原来这种功能由hook_menu完成,现在它只用于生成菜单项。举个栗子,block模块使用routing.route_dynamic事件为区块(末主题化)们的配置页面注册菜单日程。具体实现在\Drupal\block\Routing\RouteSubscriber。

你也许想知道为什么事件监听器不再用模块钩子实现了。原因是现在的实现更有效率,因为监听器可以被编译进服务容器配置中去,而服务容器的配置是被缓存的。同时也使Drupal核心尽可能面向对象。

从请求到回应

当一个请求送达Drupal,系统进入启动流程,Drupal内核启动。Drupal内核中的处理方法被调用,然后控制委派给Symfony2框架的HttpKernel,它将进一步处理请求。

HttpKernel激活'kernel.request'事件。一些订阅监听'kernel.request' 事件,它们以下面的顺序调用:

w   AuhtenticationSubscriber 加载 session 设置 global user。

w   LanguageRequestSubscriber 检测当前语言。

w   PathSubscriber 将url转换为系统路径(处理url别名)。

w   LegacyRequestSubscriber 允许设置用户主题并初始化它。

w   MaintenanceModeSubscriber 如果在维护模式显示维护页面。

w   RouteListener 获取一个完全加载的日程处理器对象。

w   AccessSubscriber 检查用户是否有日程处理器对象的访问权限。

 

日程分发就是在RouteListener当中执行的。它从Router服务处获得活动日程(active route)的属性。Drupal使用了与Symfony不同的日程处理器(Router)服务——DynamicRouter(由内容管理组扩展)。他们之间的主要区别为Druapl的DynamicRouter支持日程增强器(route enhancers),这将在后面详述。

w   DynamicRouter将发现活动日程(在Drupal 7中等同于current_path)的任务委托给NestedMatcher(嵌套匹配器)。

w   NestedMatcher将任务委托给RouteProvider(日程提供器)。

w   RouteProvider在日程表('router' table)中查找出所有符合的日程的集合。(日程处理器表缓存着内容管理系统中所有日程的列表,同时表里还缓存着日程创建器——route builder,这个稍后再讲。)查找到的日程按照路径长短排序,路径最长的被认为是最重要的。

w   NestedMatcher要求UrlMatcher(链接匹配器)从结果集中选出最明确的一个作为活动日程。

 

在实践中UrlMatcher就是把第一个——最长的结果选出,因为它包含着日程需要的协议(http/https)和提交(get/post)。在实践中通常结果集中只有一个结果,所以UrlMatcher并没那么重要。最终的返回结果是活动日程的id,像是node.add_page。

我觉得我应该再提一下route filters(日程过滤器),尽管你在实践中不太可能接触到它们。这些日程过滤器是可以被标记的服务。它们被NestedMatcher调用,能直接过滤RouteProvider返回的日程集合。Drupal内核只使用这个机制过滤日程使之不回应请求头中MIME类型的请求(MimeTypeMatcher),但是只在'_format'日程的需要被明确定义时。

找到活动日程之后,DynamicRouter调用日程增强器(这里就是不同点)。这将会最终完成日程处理器的属性以及转换日程参数(这个过程在下一节详述)。之后找到的日程处理器属性被返回RouterListener,由RouterListener填充到请求对象当中。接着AccessSubscriber(访问权限检查)被执行。最后控制器函数被以正确的实参调用,由控制器函数返回的回应被送回访客。

现在我们了解了请求的处理的过程,接着我们详细看一下日程

日程

在Drupal 7 中hook_menu使用标题、使用参和访问权限要求来注册页面回调。在Drupal 8中,使用Symfony的日程分发流程(routing process),它更灵活也更复杂!

在Drupal8中页面回调不再是独立的函数,而是controller类中的方法。可用的日程在模块根目录下的{module}.routing.yml文件中配置。举个栗子,用户登出日程的配置如下:

user.logout:

  path: '/user/logout'

  defaults:

    _controller: '\Drupal\user\Controller\UserController::logout'

  requirements:

    _user_is_logged_in: 'TRUE'

注意,每个日程都有一个id(user.logout)和路径。路径也许会包含参数,这点我们稍后再说。默认项(defaults)非常重要,它决定了当请求路径匹配时程序要做什么。需求项(requirements)决定请求是否需要被处理(没有登陆自然就不用登出了),其内容多数与访问检查有关。Symfony框架优化并取代了Drupal7的'access arguments' 和'access callback'。

默认标签(defaults)的键值(key)参考:

w   _controller 由特定日程参数调用的特定方法,其应当返回一个回应。

w   _content 如果使用,那么_controller基于请求的MIME类型的,回应的内容(content)由特定方法的结果填充(通常是字符串或格式化的数组)。

w   _form 如果使用,那么 _controller 被设置为 HtmlFormController::content,回应是特定的表单。_form的内容必须是一个完全合法的类的名字(或是服务id),这个类实现了FormInterfac接口并通常由FormBase类派生而来。当然,从这可以看出表单的构造也已经面向对象了。

w   _entity_form 如果使用,_controller 被设置为 HtmlFormController::content,回应为某个表单实体(entity form)((格式为 {entity_type}.{add|edit|delete}))。

w   _entity_list 如果使用,那么 _controller 被设置为 HtmlFormController::content,并且_content被设置为EntityListController::listing,这将会提交一个基于实体类型的列表控制器(the entity type's list controller)的实体列表。

w   _entity_view 如果使用,那么 _controller 被设置为 HtmlFormController::content,并且_content被设置为EntityViewController::view,这将会提交一个基于实体类型的视图控制器(the entity type's view controller)的实体。

w   _title 页面标题(string).

w   _title_callback 由函数返回的页面标题(method callback).

如你所见,日程关键字在日程分发过程中被依据其它日程关键字修改。因此可以很容易地输出实体,而不必设置_controller,_content或是其它关键字,这些都是自动完成的。这些特征(feature)是通过使用日程增强器实现的。日程增强器被标记为' route_enhancer',它们实现了RouteEnhancerInterface接口。Drupal内核中仅有几个重要的日程增强器。如果你想探索它们,去看ContentControllerEnhancer, FormEnhancer 和EntityRouteEnhancer的实现吧。它们已经够用了,所以看上去你不必自己再添加增强器了。

需求项(requirements)的键值参考:

w   _permission 当前访客必须拥有特定的权限。

w   _role 当前访客必须拥有特定的角色。

w   _method 允许的HTTP提交方法(GET, POST, etc)。

w   _scheme 设置为http或https。请求的协议必须与定义的_scheme相同。这个属性更多在生成url时使用。如果设置则url的协议固定。

w   _node_add_access 添加某类节点时自定义的权限检查器。

w   _entity_access 实体的权限查检器。

w   _format Mime (多媒体)类型格式化。

多数但不是所有的权限检查是由access checkers完成的,我们来简要地说一下它。在实践中,你需要创建一个自定义的权限检查器,因为Drupal7的access callback已经不再工作了。拿关键字_node_add_access举栗,它被NodeAddAccessCheck类监听,这个类是一个由节点模块添加的权限检查器。

访问检查由AccessManager(权限管理器)执行,权限管理器订阅了kernel.request事件。它调用了权限,检查器必须实现AccessInterface接口。就像之前看到的,检查器作为服务注册并且标记了access_check。检查器有一个access方法,这个方法被用来检查访客是否有权访问一个行为或日程。如果访问被拒绝,会抛出一个意外,系统会用显示访问拒绝页面的方式处理它。

 

路径参数(Path parameters)

1.	node.view:
2.	  path: '/node/{node}'
3.	  defaults:
4.	    _content: '\Drupal\node\Controller\NodeController::page'
5.	    _title_callback: '\Drupal\node\Controller\NodeController::pageTitle'
6.	  requirements:
7.	    _entity_access: 'node.view'

在实践中,页面回调需要某些参数(附加在url后面的小尾巴)。在下面的日程栗子中,路径包含一个名为node的形参。参数被用日程路径中花括号所包裹的部分定义。每个日程中确定的选项会被存储(存疑)。控制器方法会收到这些参数的实参。形参映射到使用相同名字的实参。因此NodeController的page方法有一个实参——$node。一个日程也许有多个参数,但参数的名称应该唯一。

被传递为实参的值默认为url中的字符串,但通常会被参数转换器(parameter converter)转换类型。上面的例子中,节点控制器不会得到节点的id,而会得到一个完成加载的节点实体(存疑?实例?)。参数转换是ParamConverterManager类执行的,这个类订阅了kernel.request事件。它还持有着所有注册的实现了ParamConverterInterface的服务(这些服务通过标记paramconverter注册)。当处理一个请求时,ParamConverterManager(它也是一个增强器)遍历活动日程的参数们调用参数转换器的convert方法转换参数。如果可能,一个字符串参数会被转换为一个完整的对象。注意,如果控制器方法的实参没有明确的类型指示(没有用实体名,见下段),那么会传递未经转换的参数。

实体转换器(EntityConverter)是Drupal内核中唯一的参数转换器,但它的使用非常的广泛。如果一个参数的名字与某个实体名一致,它将被转换为一个完整的实体对象。对象的值与实体id相关,如果转换失败(该id的实体不存在)会自动返回404错误。

你可以通过在默认项(defaults)中指定值的方式使一个参数变为可选参数。只有参数没有被转换才能这样做。并且可选参数只能出现在必需参数之后。

日程的建立过程(Route building)

很明显日程分发是在收到请求之后进行,但它所使用的日程处理器表('router' table)是早就创建好的。RouteBuilder(日程构造器)服务会在需要的时候(清空缓存时)填充这个表。在请求处理期间日程处理器表被用来找出活动请求然后完成它。之所以将日程处理器表的构造分开是因为性能方面的考虑。日程构造器收集所有静态配置文件中的日程然后创建容纳它们的集合。构造器还会触发route.route_dynamic事件,事件的订阅者会创建附加日程(前面block模块的例子)。之后还会触发routing.route_alter事件。这个事件有此订阅者,其中一些是重要的操作关系到权限检查和参数转换。

我们刚刚提到过权限检查器。当处理请求时,他们为特定的日程测试访客是否有访问权限。在实践中,并不是所有可用的权限检查器会被每个日程测试。反而在日程构造阶段,会决定每个日程使用哪个检查器。这项工作由AccessManager完成,它会监听router.route_alter事件。检查器有静态的也有动态的。接口StaticAccessCheckInterface有一个appliesTo方法,这个方法会返回一个适用键值的数组(此处描述存疑)。动态检查器使用applies方法测试每个日程。AccessManager为日程处理器表中的日程添加'_access_checks'项。这项信息将会在活动日程处理请求时用到。注意:日程至少有一项权限检查,不然日程无法被访问。

此前也提到过参数转换。一个参数被参数转换器转换。一个参数只对应一个参数转换器。就像权限检查器一样,对应的参数转换器会在日程构造阶段被ParamConverterManager找到。这个服务会检查所有的日程参数和所有参数转换器的applies方法,然后为日程处理器表中的日程添加项converter。当处理请求时活动日程需要参数转换这项信息就会被用到。

总结

这一章中我们学习了请求处理的阶段。同时你应该对Drupal 8日程分发的本质已经有所了解。事实上你了解了从请求到回应之间发生了什么。不过Drupal 8中还有许多你应该了解的有趣的概念,这些将在下章讲解。

 

Drupal 版本