17. 控制器执行及其解析器controller_resolver
在drupal的HttpKernel核心中使用控制器解析器来取得要执行的控制器,以及解析出控制器需要的参数。除此之外也在多个地方用到它,比如渲染数组的回调解析,是一个重点内容。
它的服务ID为:controller_resolver,接受以下两个参数:
psr7.http_message_factory:用于创建psr7描述的请求对象。(关于这个请看:http://www.php-fig.org/psr/psr-7/)
class_resolver:从容器里面取服务。
工作原理
它需要实现ControllerResolverInterface接口,里面有两个方法定义:
public function getController(Request $request); //返回一个回调作为控制器
public function getArguments(Request $request, $controller); //求出控制器的参数,并以索引数组的方式返回
这个是Symfony的接口定义,此外drupal加了一个方法:
public function getControllerFromDefinition($controller); //以路由中对控制器的定义来获取控制器,同样返回回调
所以drupal的控制器解析器一共要实现接口的以上三个方法。
先看一看它怎么解析出控制器:
这必须依赖于路由过程中在请求对象属性包上设置的_controller参数,代码如下:
$request->attributes->get('_controller'))
解析就是基于_controller的内容。
所有经过路由处理的请求对象上一定会有_controller变量,它来自路由定义的defaults字段
你可能奇怪在有些路由定义中没有定义_controller呢?
没有定义_controller的路由会在路由增强器里面定义,比如处理表单的路由里面定义了_form,而没有定义_controller,
但在增强器FormRouteEnhancer中就定义了
$defaults['_controller'] = 'controller.form:getContentResult';
其中:controller.form表示服务ID,实体也是类似情况,这个用法见下文,当流程到达这里时,总之一定有_controller这个变量设置在请求属性包中。
_controller的定义:它可以是函数名或者具备__invoke方法的对象,也可以只是一个类,但它必须包含__invoke方法。
类名必须是全限定的名字空间类名,控制器设置不能使用路径名,系统会通过类加载器找到控制器。大多数情况是用:或者::分割的字符串,它表示某个类的某方法,在这里:和::没有区别。这个分隔符还有个特殊的用法,前面部分不一定是类名也可以是容器的服务名,所以可以执行容器的服务方法。
这个特殊用法是在class_resolver服务中实现的,上文已经说了它作为参数传递给控制器解析器。
类定义在\core\lib\Drupal\Core\DependencyInjection\ClassResolver.php
这里需要注意的是在:或者::之前的部分如果是一个类,并且它实现了以下接口:
Drupal\Core\DependencyInjection\ContainerInjectionInterface
Symfony\Component\DependencyInjection\ContainerAwareInterface
那么它会被实例化并自动注入服务容器,在drupal中几乎全部服务都在容器中,
ControllerBase实现了ContainerInjectionInterface,它就是此时自动注入容器,所以能提供那些常用的服务
明白为什么控制器继承Drupal\Core\Controller\ControllerBase就会有容器了吧!
以上就是获得控制器的过程,经过控制器解析器处理返回了一个回调。
紧接着核心在得到回调后会派发kernel.controller事件,系统默认提供了两个侦听器:
服务id:path_subscriber 方法:onKernelController 作用是让别名管理器设置缓存键
服务id:early_rendering_controller_wrapper_subscriber 方法:onController 作用如下:
这个侦听器特别重要,但也让人感觉特别别扭,在控制器执行的时候要渲染页面本应该返回渲染数组。
但可能也会在控制器中直接调用drupal_render()来渲染,这样过早的渲染称为:"early rendering"。
这样做会让系统侦测不到渲染上下文,丢失一些元数据,影响到缓存等等,所以drupal8使用这个侦听器来解决这个问题
(过早渲染在drupa9中会被移除,禁止使用),那么它是怎么解决的呢?
其实所有的控制器都被这个侦听器包装在了一个闭包里面,在闭包中解决,Symfony HttpKernel得到的控制器全是经过包装的。
真正的参数解析工作也在这里进行,在\Symfony\Component\HttpKernel\HttpKernel::handleRaw()中进行的参数解析纯属浪费资源,但它毕竟是第三方库,代码不受drupal控制,白白浪费计算资源了,读者可以试试在handleRaw()中参数解析后面重置参数为空数组,系统运行照样是没有任何问题的,这里建议drupal8的控制器解析器要设置缓存机制,避免这种浪费,参数解析的反射操作是很耗资源的。
由于这个侦听器的特殊作用,当我们在注册kernel.controller事件侦听器时一定要注意优先级,此侦听器优先级为默认的0
下面来看一看控制器解析器是怎么获取参数的,一句话总结:使用php的反射机制。
许多drupal的初学者应该都好奇过控制器的参数问题吧,核心代码如辅助内容区:
参数:$parameters是一个ReflectionParameter对象组成的数组,见:http://php.net/manual/zh/class.reflectionparameter.php
它来自以下方法中:
public function getArguments(Request $request, $controller)
{
if (is_array($controller)) {
$r = new \ReflectionMethod($controller[0], $controller[1]);
} elseif (is_object($controller) && !$controller instanceof \Closure) {
$r = new \ReflectionObject($controller);
$r = $r->getMethod('__invoke');
} else {
$r = new \ReflectionFunction($controller);
}
return $this->doGetArguments($request, $controller, $r->getParameters());
}
从以上解析过程中可以知道控制器参数采用的顺序、命名规则、类型暗示:
先从$request->attributes找同名参数,这也是为什么路由定义中额外参数能作为控制器参数的原因,路由定义的占位符名要和控制器参数名一致。
再从$request->attributes中的_raw_variables找同名参数,它是未经过转换的原始变量。
再看参数是不是有类型暗示为请求对象,如果是则将请求作为参数。
再看是不是psr7描述的请求类型,它实现ServerRequestInterface接口,如果是则转化请求并传递。
再看类型暗示是不是路由匹配器(RouteMatchInterface或其子类),如果是则传递路由匹配器。
最后传递参数的默认值,如果不能找到参数将报错。
protected function doGetArguments(Request $request, $controller, array $parameters) {
$attributes = $request->attributes->all();
$raw_parameters = $request->attributes->has('_raw_variables') ? $request->attributes->get('_raw_variables') : [];
$arguments = array();
foreach ($parameters as $param) {
if (array_key_exists($param->name, $attributes)) {
$arguments[] = $attributes[$param->name];
}
elseif (array_key_exists($param->name, $raw_parameters)) {
$arguments[] = $attributes[$param->name]; //在8.2.3及之前的版本中,这里是一个bug,此处应该是$raw_parameters[$param->name],笔者已经提交了bug报告
}
elseif ($param->getClass() && $param->getClass()->isInstance($request)) {
$arguments[] = $request;
}
elseif ($param->getClass() && $param->getClass()->name === ServerRequestInterface::class) {
$arguments[] = $this->httpMessageFactory->createRequest($request);
}
elseif ($param->getClass() && ($param->getClass()->name == RouteMatchInterface::class || is_subclass_of($param->getClass()->name, RouteMatchInterface::class))) {
$arguments[] = RouteMatch::createFromRequest($request);
}
elseif ($param->isDefaultValueAvailable()) {
$arguments[] = $param->getDefaultValue();
}
else {
if (is_array($controller)) {
$repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]);
}
elseif (is_object($controller)) {
$repr = get_class($controller);
}
else {
$repr = $controller;
}
throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $repr, $param->name));
}
}
return $arguments;
}
以上就是控制器解析器的全部内容了,以下是一些补充:
1:在drupal8中所有控制器是放在闭包里面执行的,这样做是避免过早渲染带来的问题,此特性在D9中移除;
2:控制器最好定义默认值,否则当找不到值时会报错;
3:控制器最好继承Drupal\Core\Controller\ControllerBase,这样比较方便使用;
4:请求对象及路由匹配器可以在参数中获得也可以使用容器获得。