跳到内容

如何使用数据转换器

编辑此页

数据转换器用于将字段的数据转换为可以在表单中显示的格式(以及在提交时转换回来)。它们已经在内部用于许多字段类型。例如,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,这要归功于 autowireautoconfigure。否则,将表单类注册为服务 并使用 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 选项的良好默认值。

只要您使用 autowireautoconfigure,您就可以立即开始使用该表单

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)
        ;
    }

    // ...
}

提示

如果您不使用 autowireautoconfigure,请参阅 如何创建自定义表单字段类型,了解如何配置新的 IssueSelectorType

关于模型和视图转换器

在上面的示例中,转换器用作“模型”转换器。实际上,有两种不同类型的转换器和三种不同类型的底层数据。

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

  1. 模型数据 - 这是应用程序中使用的格式的数据(例如,Issue 对象)。如果您调用 Form::getData()Form::setData(),则您正在处理“模型”数据。
  2. 规范数据 - 这是数据的规范化版本,通常与您的“模型”数据相同(尽管在我们的示例中不是)。它通常不直接使用。
  3. 视图数据 - 这是用于填充表单字段本身的格式。它也是用户将提交数据的格式。当您调用 Form::submit($data) 时,$data 采用“视图”数据格式。

两种不同类型的转换器有助于在每种数据类型之间进行转换

模型转换器:
  • transform():“模型数据” => “规范数据”
  • reverseTransform():“规范数据” => “模型数据”
视图转换器:
  • transform():“规范数据” => “视图数据”
  • reverseTransform():“视图数据” => “规范数据”

您需要哪种转换器取决于您的具体情况。

要使用视图转换器,请调用 addViewTransformer()

警告

使用模型转换器和 Collection 字段类型时要小心。Collection 的子项在 PRE_SET_DATA 时由其 ResizeFormListener 提前创建,并且它们的数据稍后从规范化数据中填充。因此,您的模型转换器不能减少 Collection 中的项目数(即,过滤掉某些项目),因为在这种情况下,集合最终会得到一些空的子项。

对于该限制的一种可能的解决方法是不直接使用底层对象,而是使用 DTO(数据传输对象),该对象实现了此类不兼容数据结构的转换。

那么为什么要使用模型转换器?

在此示例中,该字段是一个 text 字段,并且文本字段始终期望在“规范”和“视图”格式中采用简单的标量格式。因此,最合适的转换器是“模型”转换器(它在规范格式 - 字符串 issue 编号 - 到模型格式 - Issue 对象之间进行转换)。

转换器之间的区别很微妙,您应该始终考虑字段的“规范”数据真正应该是什么。例如,text 字段的“规范”数据是一个字符串,但 date 字段的“规范”数据是一个 DateTime 对象。

提示

作为一般规则,规范化数据应包含尽可能多的信息。

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本