支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
数据转换器(data transformers)用于把某个字段的数据给转换成“能够在表单中显示”的格式(并且在提交时反向进行)。很多内置的字段类型已经用上了它。例如,DateType 字段类型在input文本框中被渲染成 yyyy-MM-dd
格式。在内部,一个 data transformer 会把字段的 DateTime
初始值(PHP对象)给转换成 yyyy-MM-dd
字符串以完成表单输出,然后会在提交后恢复成一个 DateTime
对象。
Caution
当一个表单字段设置了 inherit_data
选项时,data transformer 将不会作用到这一字段。
假设你有一个 Task 表单,包含了一个 tags(标签)的 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 | // src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;
use AppBundle\Entity\Task;
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' => Task::class,
));
}
// ...
} |
tags
在内部被存储为一个数组,但是显示给用户的是一个简单的以逗号分隔的字符串,这令它们更易编辑。
这是一个对 tags
字段施以一个自定义的 data transformer 的 绝好 机会。使用 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 31 32 | // src/AppBundle/Form/Type/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()
方法接受 任何 实现了 0DataTransformerInterface
接口的对象 - 这样你能创建自己的类,而不是把所有的逻辑都放进表单类(见下一小节)。
通过略微改变格式,你也可以在刚刚添加字段时添加 data transformer:
1 2 3 4 5 6 7 | use Symfony\Component\Form\Extension\Core\Type\TextType;
$builder->add(
$builder
->create('tags', TextType::class)
->addModelTransformer(...)
); |
比如说你有一个 Task entity 和一个Issue entity 的 many-to-one (多对一) 映射关系(即每个 Task 针对它所关联 Issue 有一个可选的外键)。添加一个带有全部可能的 issues 的 listbox(框列表),最终会导致列表 很 长且加载费时。取而代之的是,你决定添加一个 textbox,用户可以简单键入 issue 的 number。
起点是,像通常那样设置一个 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 | // src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;
use AppBundle\Entity\Task;
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' => Task::class,
));
}
// ...
} |
很好的开始!如果你止步于此并提交表单,你的 Task 的 issue
属性会是一个字符串 (例如 55
)。怎样才能在提交时把它转换成一个 Issue
entity 呢?
你应该像之前那样使用 CallbackTransformer
。但是由于它有点复杂,创建一个全新 transformer 将会使得 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\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IssueToNumberTransformer implements DataTransformerInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Transforms an object (issue) to a string (number).
* 把一个对象(issue)转换成一个字符串(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).
* 把一个字符串(number)转换成一个对象(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->em
->getRepository(Issue::class)
// 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;
}
} |
就像第一个例子,transformer 有两个方法。transform()
负责将你代码中的数据转换为一个可在 form 表单中输出的格式(如:一个 Issue
entity对象转换为自身的 id
字符串)。reverseTransform()
方法正好相反:它把已提交的数据转换回你的代码所需要的数据(如:把一个 id
转换回 Issue
对象)。
若要引发一个验证错误,可抛出 TransformationFailedException
。但是你传入这个异常中的信息不会显示给用户。你要用 invalid_message
来设置这个消息(见下文)。
Note
当 null
被传递到 transform()
方法时,你的 transformer 应该返回一个和它要转换成的类型相等的值(例如:一个空字符串,整型0,或是浮点0.0)。
接下来,你需要在 TaskType
中使用 IssueToNumberTransformer
对象,并把它添加到 issue
字段。没问题!只要添加一个 __construct()
方法并对新类进行 type-hint 类型提示:
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/Type/TaskType.php
namespace AppBundle\Form\Type;
use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
class TaskType extends AbstractType
{
private $transformer;
public function __construct(IssueToNumberTransformer $transformer)
{
$this->transformer = $transformer;
}
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($this->transformer);
}
// ...
} |
这就可以了!因为你使用了 autowire 以及 autoconfigure,Symfony 将自动获知要为你的 TaskType
传入一个 IssueToNumberTransformer
实例。
Tip
更多关于把表单类型定义为服务的信息,参考 注册表单类型为服务。
现在,你可以轻松使用你 TaskType
:
1 2 3 4 | // e.g. in a controller somewhere / 如,在控制器中的某处
$form = $this->createForm(TaskType::class, $task);
// ... |
酷,你搞定了!用户将能够输入一个 issue 编号到文本字段中,而它会被转换回一个 Issue 对象。这意味着,在成功提交后,表单组件将会向 Task::setIssue()
传入一个真正的 Issue
对象而不是 issue number。
如果 issue 没有被找到的话,会创建该字段的一个表单error,可以用 invalid_message
字段选项来控制错误信息。
Caution
当你添加一个 transformer 时要特别小心。例如,下面代码是 错误 的,因为 transformer 将作用于整个表单而不是仅这个字段:
1 2 3 4 5 | // THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM
// 错误 - transformer 会被应用于整个表单
// see above example for correct code / 参考上例中的正确写法
$builder->add('issue', TextType::class)
->addModelTransformer($transformer); |
在上面的例子中,你对一个普通的 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 | // 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 $transformer;
public function __construct(IssueToNumberTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer($this->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()
)那样执行和输出,但它会自动拥有 data transformer 并且 给了 invalid_message
选项一个很好的默认值。
一旦你用了 autowire 以及 autoconfigure,便可以立即使用表单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // src/AppBundle/Form/Type/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)
;
}
// ...
} |
Tip
若你并未使用 autowire
和 autoconfigure
,参考 如何创建自定义的表单类型 以了解如何配置新的 IssueSelectorType
。
上面的例子中,transformer 是作为 “Model” 转换器来使用的。实际上,有两种不同类型的转换器,以及三种不同类型的底层数据。
在任何表单中,三种不同类型的数据是:
Issue
对象)。如果你调用 Form::getData()
或 Form::setData()
,你正在处理的就是 "model" data.Form::submit($data)
时,$data
处于 "view" data format 下。这两种不同类型的 transformer 帮助我们互相转换上述类型的数据:
transform()
: "model data" => "norm data"reverseTransform()
: "norm data" => "model data"transform()
: "norm data" => "view data"reverseTransform()
: "view data" => "norm data"使用那种转换器取决于你的实际情况。
如果你想使用 view transformer,调用 addViewTransformer
。
本例中,是一个text
字段类型,在 “norm” 和 “view” 格式中,文本字段总是预期一个简单的标量值。因此,最合适的转换器就是 model transformer(它在 norm 格式 - 字符型的 issue编号 - 转换为 model 格式 - issue 对象 之间进行转换)。
(不同类型)转换器之间的区别是微妙的,你应该始终思考某个字段的 ‘norm’ 数据应该是什么。举例来说,text
字段的“norm”数据是一个字符串,但是date
字段的则是一个 DataTime
对象。
Tip
一个通用规则是,规范化的数据(the normalized data)应当包含尽可能多的信息。
本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。