如何使用数据转换器
数据转换器用于将字段的数据转换为可以在表单中显示的格式(以及在提交时转换回来)。它们已经在内部用于许多字段类型。例如,DateType 字段可以呈现为 yyyy-MM-dd
格式的输入文本框。在内部,数据转换器在呈现表单时将字段的 DateTime
值转换为 yyyy-MM-dd
格式的字符串,然后在提交时转换回 DateTime
对象。
警告
当表单字段的 inherit_data
选项设置为 true
时,数据转换器不会应用于该字段。
另请参阅
如果除了转换值的表示形式之外,您还需要将值映射到表单字段并返回,则应使用数据映射器。请查看 何时以及如何使用数据映射器。
示例 #1:将字符串表单数据标签从用户输入转换为数组
假设您有一个带有 tags text
类型的 Task 表单
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/Form/Type/TaskType.php
namespace App\Form\Type;
use App\Entity\Task;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('tags', TextType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
// ...
}
在内部,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/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('tags', TextType::class);
$builder->get('tags')
->addModelTransformer(new CallbackTransformer(
function ($tagsAsArray): string {
// transform the array to a string
return implode(', ', $tagsAsArray);
},
function ($tagsAsString): array {
// transform the string back to an array
return explode(', ', $tagsAsString);
}
))
;
}
// ...
}
CallbackTransformer
接受两个回调函数作为参数。第一个将原始值转换为将用于呈现字段的格式。第二个执行相反的操作:它将提交的值转换回您将在代码中使用的格式。
提示
addModelTransformer()
方法接受任何实现 DataTransformerInterface 的对象 - 因此您可以创建自己的类,而不是将所有逻辑都放在表单中(请参阅下一节)。
您还可以在添加字段时添加转换器,只需稍微更改格式即可
1 2 3 4 5 6 7
use Symfony\Component\Form\Extension\Core\Type\TextType;
$builder->add(
$builder
->create('tags', TextType::class)
->addModelTransformer(/* ... */)
);
示例 #2:将 Issue 编号转换为 Issue 实体
假设您有一个从 Task 实体到 Issue 实体的多对一关系(即,每个 Task 都有一个指向其相关 Issue 的可选外键)。添加包含所有可能 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
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use App\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): void
{
$builder
->add('description', TextareaType::class)
->add('issue', TextType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
// ...
}
好的开始!但是,如果您在此处停止并提交表单,则 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
// src/Form/DataTransformer/IssueToNumberTransformer.php
namespace App\Form\DataTransformer;
use App\Entity\Issue;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IssueToNumberTransformer implements DataTransformerInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
) {
}
/**
* Transforms an object (issue) to a string (number).
*
* @param Issue|null $issue
*/
public function transform($issue): string
{
if (null === $issue) {
return '';
}
return $issue->getId();
}
/**
* Transforms a string (number) to an object (issue).
*
* @param string $issueNumber
* @throws TransformationFailedException if object (issue) is not found.
*/
public function reverseTransform($issueNumber): ?Issue
{
// no issue number? It's optional, so that's ok
if (!$issueNumber) {
return null;
}
$issue = $this->entityManager
->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;
}
}
与第一个示例一样,转换器具有两个方向。transform()
方法负责将代码中使用的数据转换为可以在表单中呈现的格式(例如,将 Issue
对象转换为其 id
,一个字符串)。reverseTransform()
方法执行相反的操作:它将提交的值转换回您想要的格式(例如,将 id
转换回 Issue
对象)。
要导致验证错误,请抛出 TransformationFailedException。但是,您传递给此异常的消息不会显示给用户。您将使用 invalid_message
选项设置该消息(见下文)。
注意
当将 null
传递给 transform()
方法时,您的转换器应返回与其转换类型的等效值(例如,空字符串,整数为 0 或浮点数为 0.0)。
使用转换器
接下来,您需要在 TaskType
内部使用 IssueToNumberTransformer
对象,并将其添加到 issue
字段。没问题!添加一个 __construct()
方法并类型提示新类
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/Form/Type/TaskType.php
namespace App\Form\Type;
use App\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
class TaskType extends AbstractType
{
public function __construct(
private IssueToNumberTransformer $transformer,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('description', TextareaType::class)
->add('issue', TextType::class, [
// validation message if the data transformer fails
'invalid_message' => 'That is not a valid issue number',
]);
// ...
$builder->get('issue')
->addModelTransformer($this->transformer);
}
// ...
}
每当转换器抛出异常时,invalid_message
都会显示给用户。您可以设置 setInvalidMessage()
方法在数据转换器中设置最终用户的错误消息,而不是每次都显示相同的消息。它还允许您包含用户值
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
// src/Form/DataTransformer/IssueToNumberTransformer.php
namespace App\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IssueToNumberTransformer implements DataTransformerInterface
{
// ...
public function reverseTransform($issueNumber): ?Issue
{
// ...
if (null === $issue) {
$privateErrorMessage = sprintf('An issue with number "%s" does not exist!', $issueNumber);
$publicErrorMessage = 'The given "{{ value }}" value is not a valid issue number.';
$failure = new TransformationFailedException($privateErrorMessage);
$failure->setInvalidMessage($publicErrorMessage, [
'{{ value }}' => $issueNumber,
]);
throw $failure;
}
return $issue;
}
}
就是这样!如果您使用 默认的 services.yaml 配置,Symfony 将自动知道将 IssueToNumberTransformer
的实例传递给您的 TaskType
,这要归功于 autowire 和 autoconfigure。否则,将表单类注册为服务 并使用 form.type
标签 标记它。
现在,您可以使用您的 TaskType
1 2 3 4
// e.g. somewhere in a controller
$form = $this->createForm(TaskType::class, $task);
// ...
太棒了,您完成了!您的用户将能够在文本字段中输入 issue 编号,该编号将被转换回 Issue 对象。这意味着,在成功提交后,Form 组件会将真实的 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
// src/Form/IssueSelectorType.php
namespace App\Form;
use App\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class IssueSelectorType extends AbstractType
{
public function __construct(
private IssueToNumberTransformer $transformer,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer($this->transformer);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'invalid_message' => 'The selected issue does not exist',
]);
}
public function getParent(): string
{
return TextType::class;
}
}
太棒了!这将像文本字段 (getParent()
) 一样工作和呈现,但将自动具有数据转换器和 invalid_message
选项的良好默认值。
只要您使用 autowire 和 autoconfigure,您就可以立即开始使用该表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use App\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('description', TextareaType::class)
->add('issue', IssueSelectorType::class)
;
}
// ...
}
提示
如果您不使用 autowire
和 autoconfigure
,请参阅 如何创建自定义表单字段类型,了解如何配置新的 IssueSelectorType
。
关于模型和视图转换器
在上面的示例中,转换器用作“模型”转换器。实际上,有两种不同类型的转换器和三种不同类型的底层数据。
在任何表单中,三种不同类型的数据是
- 模型数据 - 这是应用程序中使用的格式的数据(例如,
Issue
对象)。如果您调用Form::getData()
或Form::setData()
,则您正在处理“模型”数据。 - 规范数据 - 这是数据的规范化版本,通常与您的“模型”数据相同(尽管在我们的示例中不是)。它通常不直接使用。
- 视图数据 - 这是用于填充表单字段本身的格式。它也是用户将提交数据的格式。当您调用
Form::submit($data)
时,$data
采用“视图”数据格式。
两种不同类型的转换器有助于在每种数据类型之间进行转换
- 模型转换器:
-
transform()
:“模型数据” => “规范数据”reverseTransform()
:“规范数据” => “模型数据”
- 视图转换器:
-
transform()
:“规范数据” => “视图数据”reverseTransform()
:“视图数据” => “规范数据”
您需要哪种转换器取决于您的具体情况。
要使用视图转换器,请调用 addViewTransformer()
。
警告
使用模型转换器和 Collection 字段类型时要小心。Collection 的子项在 PRE_SET_DATA
时由其 ResizeFormListener
提前创建,并且它们的数据稍后从规范化数据中填充。因此,您的模型转换器不能减少 Collection 中的项目数(即,过滤掉某些项目),因为在这种情况下,集合最终会得到一些空的子项。
对于该限制的一种可能的解决方法是不直接使用底层对象,而是使用 DTO(数据传输对象),该对象实现了此类不兼容数据结构的转换。
那么为什么要使用模型转换器?
在此示例中,该字段是一个 text
字段,并且文本字段始终期望在“规范”和“视图”格式中采用简单的标量格式。因此,最合适的转换器是“模型”转换器(它在规范格式 - 字符串 issue 编号 - 到模型格式 - Issue 对象之间进行转换)。
转换器之间的区别很微妙,您应该始终考虑字段的“规范”数据真正应该是什么。例如,text
字段的“规范”数据是一个字符串,但 date
字段的“规范”数据是一个 DateTime
对象。
提示
作为一般规则,规范化数据应包含尽可能多的信息。