扩展对Action参数的解析

3.4 版本
维护中的版本

3.1 ArgumentResolver和value resolvers在3.1中被引入。

在《起步》的控制器一文中,你了解到可以通过控制器参数来获取Request对象。这个参数必须是Request类的类型提示才能被识别。而具体落实则是通过ArgumentResolver参数解析器完成。通过创建和注册自定义的参数值解析器(augument value resolvers),你可以扩展此功能。

功能性来自HttpKernel 

在HttpKernel组件中Symfony推出了四个value resolver(值解析器):

RequestAttributeValueResolver
尝试找到一个匹配到参数名(argument name)的request属性。
RequestValueResolver
注入当前的Request对象——如果类型提示是Request,或者是一个扩展了Request的类的话。
DefaultValueResolver
若参数是可选的并且出现的话,将会被设置一个默认的参数值。
VariadicValueResolver
验证request数据是否是数组,是的话就把全部数据添加至参数列表中。当action被调用时,最后一个(variadic)参数,将包含这个数组中的全部值。

在Symfony 3.1之前,这部分逻辑是在ControllerResolver中被解析的。旧有功能被重写为前述的value resovlers(值解析器)。

添加一个自定义的Value Resolver 

要添加一个新的值解析器,需要创建一个类,以及它的服务定义。这可以通过实现ArgumentValueResolverInterface接口来完成。这个接口要求你实现两个方法:

supports()

这个方法用于检查value resolver是否支持给定的参数。resolve()仅在本方法返回true的时候被执行。


resolve()

用于解析(action)参数所对应的确切的值。一旦该值被解析,你必须yealdArgumentResolver

两个方法都得到Request对象,就是当前的请求,以及一个ArgumentMetadata实例。这个对象,包含了从“当前参数的方法签名”中所取出的全部信息(译注:签名即type hint)。

现在你知道了要做什么,你可以实现这个接口。要得到User对象,你需要当前的security token。这个token可以从token storage(服务)中取出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/AppBundle/ArgumentResolver/UserValueResolver.php
namespace AppBundle\ArgumentResolver;
 
use AppBundle\Entity\User;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
 
class UserValueResolver implements ArgumentValueResolverInterface
{
    private $tokenStorage;
 
    public function __construct(TokenStorageInterface $tokenStorage)
    {
        $this->tokenStorage = $tokenStorage;
    }
 
    public function supports(Request $request, ArgumentMetadata $argument)
    {
        if (User::class !== $argument->getType()) {
            return false;
        }
 
        $token = $this->tokenStorage->getToken();
 
        if (!$token instanceof TokenInterface) {
            return false;
        }
 
        return $token->getUser() instanceof User;
    }
 
    public function resolve(Request $request, ArgumentMetadata $argument)
    {
        yield $this->tokenStorage->getToken()->getUser();
    }
}

为了在参数中得到真正的User,给定的值必须满足以下条件:

  • 在你的方法签名中,参数必须经过User类型提示;

  • 必须有一个security token;

  • 给定的值必须是User的实例。

当所有这些条件被满足而且返回了true时,因为先调用了supports()方法,AugumentResolver再以相同的值来调用resolve()方法。

就是这样!现在你要把配置信息添加到服务容器。只要对服务打上controller.argument_resolver再添加一个优先级即可:

1
2
3
4
5
6
7
8
# app/config/services.yml
services:
    app.value_resolver.user:
        class: AppBundle\ArgumentResolver\UserValueResolver
        arguments:
            - '@security.token_storage'
        tags:
            - { name: controller.argument_value_resolver, priority: 50 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="'http://www.w3.org/2001/XMLSchema-Instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="app.value_resolver.user"
            class="AppBundle\ArgumentResolver\UserValueResolver"
        >
            <argument type="service" id="security.token_storage">
            <tag name="controller.argument_value_resolver" priority="50" />
        </service>
    </services>
 
</container>
1
2
3
4
5
6
7
8
9
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
 
$defintion = new Definition(
    'AppBundle\ArgumentResolver\UserValueResolver',
    array(new Reference('security.token_storage'))
);
$definition->addTag('controller.argument_value_resolver', array('priority' => 50));
$container->setDefinition('app.value_resolver.user', $definition);

虽然优先级是可选的,但还是推荐设置一个,以确保预期的value被注入。RequestAttributeValueResolver有一个100的优先级。因为它负责的是从Request中取出属性,建议把你的自定义value resolver设为一个更低的优先级。这可以确保参数解析器在属性现前时“不被激发”。例如,当一个用户伴随着子请求被传入时。

当你查看UserValueResolver::Supports()的代码时,user可能并不能用(比如,控制器并没有处在防火墙下时)。这时,解析器将不被执行。如果没有参数值被解析,会抛出一个异常。

为防止这种情况出现,你应该添在控制器中添加一个默认值(比如User $user = null)。DefaultValueResolver作为最后一个解析器被执行,如果没有值被解析,它会使用默认值。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)