如何使用数据转换器(Data Transformers)

2.8 版本
维护中的版本

数据转换适用于将一个字段数据格式转换成表单里显示的数据格式(并且可以重复提交)。在symfony内部已经有了很多这样的字段类型。举例,DateType类型在input文本框中被渲染成yyyy-MM-dd格式。在内部,一个数据转换器将开始的DateTime字段的值转换成yyyy-MM-dd字符串渲染到表单,并在提交时返回DateTime对象。

Caution

当一个表单字段设置了inherit_data配置时,数据转换器将不会应用到这一字段。

---简单例子:转换用户输入的字符串标签为一个数组---

假设你有一个Task表单,有一个text类型的标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
 
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
 
// ...
class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('tags', TextType::class)
    }
 
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Task',
        ));
    }
 
    // ...
}

在内部tags被存储为一个数组,但是我们想显示给用户一个简单的逗号分隔的字符串,使它们更容易编辑。

这是一个将自定义数据转换到tags字段的好机会。使用CallbackTransformer这个方法很容易去做到:

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
// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
 
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
 
class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('tags', TextType::class);
 
        $builder->get('tags')
            ->addModelTransformer(new CallbackTransformer(
                function ($tagsAsArray) {
                    // transform the array to a string 转换数组为一个字符串
                    return implode(', ', $tagsAsArray);
                },
                function ($tagsAsString) {
                    // transform the string back to an array 转换字符串为一个数组
                    return explode(', ', $tagsAsString);
                }
            ))
        ;
    }
 
    // ...
}

CallbackTransformer类用两个回调函数作为参数。第一个函数将原始的值转化为一个能在表单中渲染的格式。第二个函数做了相反的事情:他将提交后获取的值转化为你代码中需要的格式。

Tip

这个addModelTransformer()方法接受任何实现DataTransformerInterface接口的对象- 这样你能够创建属于我们自己的类,而不是在表单中放入所有的逻辑(看下一章)。

当添加字段略微改变格式,你也可以添加转换器(transformer):

1
2
3
4
5
6
7
use Symfony\Component\Form\Extension\Core\Type\TextType;
 
$builder->add(
    $builder
        ->create('tags', TextType::class)
        ->addModelTransformer(...)
);

---复杂的例子:将Issue编号转化成Isuse实体---

比如说你有一个Task实体和一个Issue实体他们是 many-to-one(多对一)的映射关系(好像每一个任务都有一些关联的问题)。添加所有问题到一个listbox,他会变得很长,而且加载时间也变长了。你可以添加一个textbox让用户输入一些问题的编号来解决。

开始我们设置一个文本字段就和平时一样:

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
// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
 
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
 
// ...
class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('description', TextareaType::class)
            ->add('issue', TextType::class)
        ;
    }
 
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Task'
        ));
    }
 
    // ...
}

一个好的开始!如果你停止在这里,并提交表单,你的Task的issue属性就会是一个字符串(例如 55 )。你怎么把他变成一个Issue实体提交呢?

---创建转换器---1

你应该像之前一样使用CallbackTransformer。但是由于这个逻辑有些复杂,创建一个转换器将会使得TaskType表单类更加简单。

创建一个IssueToNumberTransformer类:他将会负责相互转化Issue编号和Issue实体:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// src/AppBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace AppBundle\Form\DataTransformer;
 
use AppBundle\Entity\Issue;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
 
class IssueToNumberTransformer implements DataTransformerInterface
{
    private $manager;
 
    public function __construct(ObjectManager $manager)
    {
        $this->manager = $manager;
    }
 
    /**
     * Transforms an object (issue) to a string (number).
     *
     * @param  Issue|null $issue
     * @return string
     */
    public function transform($issue)
    {
        if (null === $issue) {
            return '';
        }
 
        return $issue->getId();
    }
 
    /**
     * Transforms a string (number) to an object (issue).
     *
     * @param  string $issueNumber
     * @return Issue|null
     * @throws TransformationFailedException if object (issue) is not found.
     */
    public function reverseTransform($issueNumber)
    {
        // no issue number? It's optional, so that's ok
        if (!$issueNumber) {
            return;
        }
 
        $issue = $this->manager
            ->getRepository('AppBundle:Issue')
            // query for the issue with this id
            ->find($issueNumber)
        ;
 
        if (null === $issue) {
            // causes a validation error
            // this message is not shown to the user
            // see the invalid_message option
            throw new TransformationFailedException(sprintf(
                'An issue with number "%s" does not exist!',
                $issueNumber
            ));
        }
 
        return $issue;
    }
}

就像第一个例子,转换器有两个方法。这个transform()负责将你代码中的数据转换为一个form表单渲染的数据格式(如:一个Issue对象转换为一个id字符串)。这个reverseTransform()方法正好相反:他将提交的数据转换成你代码想要的数据(如:把一个id转换为Issue对象)。

如果验证发生错误,你可以抛出TransformationFailedException。但是这个异常信息就不要给你的用户看了。你使用invalid_message来设置消息(详见下面)。

Note

null被传递到transform()方法时,你的转换器应该返回一个和它类型相等的值(例如:一个空字符串,整型的0,或者是浮点数0.0)。

---使用这个转换器---2

下一步,你将在TaskType中实例化你的IssueToNumberTransformer类并添加他到issue字段。要做到这一点,你将需要一个实体管理(entity manager)(因为IssueToNumberTransformer需要他)。

没有问题!仅仅给TaskType添加一个__construct()函数把它注册为一个服务传入entity管理即可:

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
// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
 
use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
 
// ...
class TaskType extends AbstractType
{
    private $manager;
 
    public function __construct(ObjectManager $manager)
    {
        $this->manager = $manager;
    }
 
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('description', TextareaType::class)
            ->add('issue', TextType::class, array(
                // validation message if the data transformer fails
                'invalid_message' => 'That is not a valid issue number',
            ));
 
        // ...
 
        $builder->get('issue')
            ->addModelTransformer(new IssueToNumberTransformer($this->manager));
    }
 
    // ...
}

在你的配置文件中定义一个表单类型作为一个服务:

1
2
3
4
5
6
7
# src/AppBundle/Resources/config/services.yml
services:
    app.form.type.task:
        class: AppBundle\Form\Type\TaskType
        arguments: ["@doctrine.orm.entity_manager"]
        tags:
            - { name: form.type }
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- src/AppBundle/Resources/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.form.type.task" class="AppBundle\Form\Type\TaskType">
            <tag name="form.type" />
            <argument type="service" id="doctrine.orm.entity_manager"></argument>
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/AppBundle/Resources/config/services.php
use AppBundle\Form\Type\TaskType;
 
$definition = new Definition(TaskType::class, array(
    new Reference('doctrine.orm.entity_manager'),
));
$container
    ->setDefinition(
        'app.form.type.task',
        $definition
    )
    ->addTag('form.type')
;

Tip

更多表单类型注册为服务的信息,请阅读 注册一个表单类型为服务.

现在,你能够很容易的使用你的TaskType

1
2
3
4
// e.g. in a controller somewhere
$form = $this->createForm(TaskType::class, $task);
 
// ...

酷,你完成了!你的用户将能够在text字段输入一个issue编号来把他转换成一个Issue对象。这意味着,在成功的提交之后,表单组件将会向 Task::setIssue() 传递一个真正的 Issue 对象而不是问题数字。

如果issue没有被找到的话,一个表单字段错误将会产生,并且invalid_message这个字段能够控制错误信息。

Caution

当你添加一个转换器时你要小心。举例,下面代码是错误的,由于转换器将会被用于整个表单而不是仅仅这个字段:

1
2
3
4
// THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM
// see above example for correct code
$builder->add('issue', TextType::class)
    ->addModelTransformer($transformer);

---创建一个可以重复使用的 issue_selector 字段---

在上面的例子中,你转换了一个普通的text字段。但如果你要做很多这样的转换,最好是创建一个自定义的表单类型,他就可以自动完成。

首先,创建一个自定义的字段类型类:

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/Form/IssueSelectorType.php
namespace AppBundle\Form;
 
use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
 
class IssueSelectorType extends AbstractType
{
    private $manager;
 
    public function __construct(ObjectManager $manager)
    {
        $this->manager = $manager;
    }
 
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new IssueToNumberTransformer($this->manager);
        $builder->addModelTransformer($transformer);
    }
 
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'invalid_message' => 'The selected issue does not exist',
        ));
    }
 
    public function getParent()
    {
        return TextType::class;
    }
}

好!他将像一个text字段一样渲染(getParent()做了指定),但他自动有一个数据转换器并默认配置invalid_message

接下来,将你的类型注册为一个服务并标注form.type标签,这样他就被认定为是一个自定义的字段类型了:

1
2
3
4
5
6
7
# app/config/services.yml
services:
    app.type.issue_selector:
        class: AppBundle\Form\IssueSelectorType
        arguments: ['@doctrine.orm.entity_manager']
        tags:
            - { name: form.type }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 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.type.issue_selector"
            class="AppBundle\Form\IssueSelectorType">
            <argument type="service" id="doctrine.orm.entity_manager"/>
            <tag name="form.type" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
 
$container
    ->setDefinition('app.type.issue_selector', new Definition(
            'AppBundle\Form\IssueSelectorType'
        ),
        array(
            new Reference('doctrine.orm.entity_manager'),
        )
    )
    ->addTag('form.type')
;

现在,无论什么使用你需要使用你的特殊issue_selector字段类型,他都非常的容易:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
 
use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
// ...
 
class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('description', TextareaType::class)
            ->add('issue', IssueSelectorType::class)
        ;
    }
 
    // ...
}

---关于Model(模型)和View Transformers(视图转换器)---

上面的例子的转换器是一个“Model”转换器。实时上,共有两种类型的转换器,又有三种不同类型的基础数据。

DataTransformersTypes

在任何表单中,都有三种不同类型的数据:

  • 1. Model data (模型数据)- 这个数据在你的应用程序内部使用(例如一个`Issue`对象)。如果你调用`Form::getData()`或者`Form::setData(`),你就可以处理模型数据。
  • 2.Norm Data (普通数据) – 这是一个你的普通版本数据,并且这个数据和你的modle数据一样常见(尽管我们的例子中没有)。她通常不会被直接应用。
  • 3.View Data (视图数据) – 这是表单字段自动填充的数据格式。用户也很有可能提交这种格式的数据。当你调用 `Form::submit($data)`时,$data 就是“视图”格式的数据。

这两种不同类型的转换器可以帮助我们相互转换这些类型数据:

Model transformers:

-transform: “model data” => “norm data”

-reverseTransform: “norm data” => “model data”

View transformers:

-transform: “norm data” => “view data”

-reverseTransform: “view data” => “norm data”

你需要使用那种转换器取决于你的实际情况。

如果你想使用视图转换器(view transformer)就调用addViewTransformer

---为什么在这里要使用模型转换器?---

在这个例子中,字段类型是一个text,同时一个text字段总是比较简单,这个格式在“norm”和“view”中。因为在这里model转换器做适合转换(转换表单格式—-字符串issue编号—-模型格式—-Issue对象)。

转换器的区别是微妙的,你应该考虑‘norm’数据字段是什么样子。举例来说,text字段的普通数据就是一个字符串,但是一个date字段就是一个DataTime对象。

Tip

一个普遍的规律,规范化的数据应当包含尽可能多的信息。

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

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