如何使用数据转换器
数据转换适用于将一个字段数据格式转换成表单里显示的数据格式(并且可以重复提交)。在symfony内部已经有了很多这样的字段类型。举例,DateType类型在input文本框中被渲染成yyyy-MM-dd
格式。在内部,一个数据转换器将开始的DateTime
字段的值转换成yyyy-MM-dd
字符串渲染到表单,并在提交时返回DateTime
对象。
当一个表单字段设置了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
类用两个回调函数作为参数。第一个函数将原始的值转化为一个能在表单中渲染的格式。第二个函数做了相反的事情:他将提交后获取的值转化为你代码中需要的格式。
这个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
实体提交呢?
创建转换器 ¶
你应该像之前一样使用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来设置消息(详见下面)。
当null
被传递到transform()
方法时,你的转换器应该返回一个和它类型相等的值(例如:一个空字符串,整型的0,或者是浮点数0.0)。
使用这个转换器 ¶
下一步,你将在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')
; |
更多表单类型注册为服务的信息,请阅读 注册一个表单类型为服务.
现在,你能够很容易的使用你的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
这个字段能够控制错误信息。
当你添加一个转换器时你要小心。举例,下面代码是错误的,由于转换器将会被用于整个表单而不是仅仅这个字段:
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”转换器。实时上,共有两种类型的转换器,又有三种不同类型的基础数据。
在任何表单中,都有三种不同类型的数据:
- 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
对象。
一个普遍的规律,规范化的数据应当包含尽可能多的信息。