How to Use Data Transformers

Data transformers are used to translate the data for a field into a format that can be displayed in a form (and back on submit). They're already used internally for many field types. For example, the DateType field can be rendered as a yyyy-MM-dd-formatted input text box. Internally, a data transformer converts the DateTime value of the field to a yyyy-MM-dd formatted string when rendering the form, and then back to a DateTime object on submit.

データ トランスフォーマーは、フィールドのデータをフォームに表示できる形式に変換するために使用されます (送信時に元に戻します)。これらは、すでに内部的にすべてのフィールド タイプで使用されています。たとえば、DateType フィールドは、yyyy-MM-dd 形式の入力テキスト ボックスとしてレンダリングできます。内部的には、データ トランスフォーマーは、フォームのレンダリング時にフィールドの DateTime 値を yyyy-MM-dd 形式の文字列に変換し、送信時に DateTime オブジェクトに戻します。

Caution

注意

When a form field has the inherit_data option set to true, data transformers are not applied to that field.

フォーム フィールドの inherit_data オプションが true に設定されている場合、データ トランスフォーマーはそのフィールドに適用されません。

See also

こちらもご覧ください

If, instead of transforming the representation of a value, you need to map values to a form field and back, you should use a data mapper. Check out When and How to Use Data Mappers.

値の表現を変換する代わりに、値をフォーム フィールドにマップして戻す必要がある場合は、データ マッパーを使用する必要があります。データマッパーをいつ、どのように使用するかを確認してください。

Example #1: Transforming Strings Form Data Tags from User Input to an Array

Suppose you have a Task form with a tags text type:

タグ テキスト タイプのタスク フォームがあるとします。
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,
        ]);
    }

    // ...
}

Internally the tags are stored as an array, but displayed to the user as a plain comma separated string to make them easier to edit.

内部的にタグは配列として保存されますが、編集しやすいようにカンマで区切られたプレーンな文字列としてユーザーに表示されます。

This is a perfect time to attach a custom data transformer to the tags field. The easiest way to do this is with the CallbackTransformer class:

これは、タグフィールドにカスタム データ トランスフォーマーをアタッチする絶好の機会です。これを行う最も簡単な方法は、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) {
                    // transform the array to a string
                    return implode(', ', $tagsAsArray);
                },
                function ($tagsAsString) {
                    // transform the string back to an array
                    return explode(', ', $tagsAsString);
                }
            ))
        ;
    }

    // ...
}

The CallbackTransformer takes two callback functions as arguments. The first transforms the original value into a format that'll be used to render the field. The second does the reverse: it transforms the submitted value back into the format you'll use in your code.

CallbackTransformer は、引数として 2 つのコールバック関数を受け取ります。 1 つ目は、フィールドのレンダリングに使用される形式に元の値を変換します。 2 番目は逆で、送信された値をコードで使用する形式に戻します。

Tip

ヒント

The addModelTransformer() method accepts any object that implements DataTransformerInterface - so you can create your own classes, instead of putting all the logic in the form (see the next section).

addModelTransformer() メソッドは、DataTransformerInterface を実装する任意のオブジェクトを受け入れるため、フォームにすべてのロジックを配置する代わりに、独自のクラスを作成できます (次のセクションを参照)。

You can also add the transformer, right when adding the field by changing the format slightly:

フォーマットを少し変更することでフィールドを追加するときに、トランスフォーマーを追加することもできます。
1
2
3
4
5
6
7
use Symfony\Component\Form\Extension\Core\Type\TextType;

$builder->add(
    $builder
        ->create('tags', TextType::class)
        ->addModelTransformer(/* ... */)
);

Example #2: Transforming an Issue Number into an Issue Entity

Say you have a many-to-one relation from the Task entity to an Issue entity (i.e. each Task has an optional foreign key to its related Issue). Adding a list box with all possible issues could eventually get really long and take a long time to load. Instead, you decide you want to add a text box, where the user can enter the issue number.

Task エンティティから Issue エンティティへの多対 1 の関係があるとします (つまり、各Task には関連する Issue へのオプションの外部キーがあります)。考えられるすべての問題を含むリスト ボックスを追加すると、最終的に非常に長くなり、読み込みに時間がかかる可能性があります。代わりに、ユーザーが問題番号を入力できるテキスト ボックスを追加することにしました。

Start by setting up the text field like normal:

通常のようにテキスト フィールドを設定することから始めます。
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,
        ]);
    }

    // ...
}

Good start! But if you stopped here and submitted the form, the Task's issue property would be a string (e.g. "55"). How can you transform this into an Issue entity on submit?

良いスタート!ただし、ここで停止してフォームを送信すると、タスクの問題プロパティは文字列になります (例: "55")。送信時にこれを Issueentity に変換するにはどうすればよいですか?

Creating the Transformer

You could use the CallbackTransformer like earlier. But since this is a bit more complex, creating a new transformer class will keep the TaskType form class simpler.

以前のように CallbackTransformer を使用できます。ただし、これは少し複雑なので、新しいトランスフォーマー クラスを作成すると、TaskType フォーム クラスがより単純になります。

Create an IssueToNumberTransformer class: it will be responsible for converting to and from the issue number and the Issue object:

IssueToNumberTransformer クラスを作成します。これは、課題番号と課題オブジェクトとの間の変換を担当します。
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
// 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;
    }
}

Like the first example, the transformer has two directions. The transform() method is responsible for converting the data used in your code to a format that can be rendered in your form (e.g. an Issue object to its id, a string). The reverseTransform() method does the reverse: it converts the submitted value back into the format you want (e.g. convert the id back to the Issue object).

最初の例と同様に、変圧器には 2 つの方向があります。 transform() メソッドは、コードで使用されているデータを、フォームでレンダリングできる形式 (Issue オブジェクトをその ID、文字列など) に変換する役割を果たします。reverseTransform() メソッドは逆の処理を行います。 valueback を必要な形式に戻します (例: ID を課題オブジェクトに変換します)。

To cause a validation error, throw a TransformationFailedException. But the message you pass to this exception won't be shown to the user. You'll set that message with the invalid_message option (see below).

検証エラーを発生させるには、TransformationFailedException をスローします。ただし、この例外に渡すメッセージはユーザーに表示されません。そのメッセージは、invalid_message オプションで設定します (以下を参照)。

Note

ノート

When null is passed to the transform() method, your transformer should return an equivalent value of the type it is transforming to (e.g. an empty string, 0 for integers or 0.0 for floats).

null が transform() メソッドに渡されると、トランスフォーマーは変換先の型と同等の値を返す必要があります (たとえば、空の文字列、整数の場合は 0、浮動小数点の場合は 0.0)。

Using the Transformer

Next, you need to use the IssueToNumberTransformer object inside of TaskType and add it to the issue field. No problem! Add a __construct() method and type-hint the new class:

次に、TaskType 内で IssueToNumberTransformer オブジェクトを使用し、課題フィールドに追加する必要があります。問題ない! __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
33
34
// 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
{
    private $transformer;

    public function __construct(IssueToNumberTransformer $transformer)
    {
        $this->transformer = $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);
    }

    // ...
}

Whenever the transformer throws an exception, the invalid_message is shown to the user. Instead of showing the same message every time, you can set the end-user error message in the data transformer using the setInvalidMessage() method. It also allows you to include user values:

トランスフォーマーが例外をスローするたびに、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;
    }
}

That's it! If you're using the default services.yaml configuration, Symfony will automatically know to pass your TaskType an instance of the IssueToNumberTransformer thanks to autowire and autoconfigure. Otherwise, register the form class as a service and tag it with the form.type tag.

それでおしまい!デフォルトの services.yaml 設定を使用している場合、autowire と autoconfigure のおかげで、Symfony は TaskType に theIssueToNumberTransformer のインスタンスを渡すことを自動的に認識します。それ以外の場合は、フォーム クラスをサービスとして登録し、form.type タグでタグ付けします。

Now, you can use your TaskType:

これで、TaskType を使用できます。
1
2
3
4
// e.g. somewhere in a controller
$form = $this->createForm(TaskType::class, $task);

// ...

Cool, you're done! Your user will be able to enter an issue number into the text field, which will be transformed back into an Issue object. This means that, after a successful submission, the Form component will pass a real Issue object to Task::setIssue() instead of the issue number.

クール、あなたは終わった!ユーザーは問題番号をテキスト フィールドに入力できます。これは問題オブジェクトに変換されます。これは、送信が成功した後、Form コンポーネントが課題番号の代わりに realIssue オブジェクトを Task::setIssue() に渡すことを意味します。

If the issue isn't found, a form error will be created for that field and its error message can be controlled with the invalid_message field option.

問題が見つからない場合、そのフィールドに対してフォーム エラーが作成され、そのエラー メッセージは、invalid_message フィールド オプションで制御できます。

Caution

注意

Be careful when adding your transformers. For example, the following is wrong, as the transformer would be applied to the entire form, instead of just this field:

変圧器を追加するときは注意してください。たとえば、トランスフォーマーがこのフィールドだけではなくフォーム全体に適用されるため、次の例は正しくありません。
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);

Creating a Reusable issue_selector Field

In the above example, you applied the transformer to a normal text field. But if you do this transformation a lot, it might be better to create a custom field type. that does this automatically.

上記の例では、トランスフォーマーを通常のテキスト フィールドに適用しました。ただし、この変換を頻繁に行う場合は、これを自動的に行うカスタム フィールド タイプを作成する方がよい場合があります。

First, create the custom field type class:

まず、カスタム フィールド タイプ クラスを作成します。
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/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
{
    private $transformer;

    public function __construct(IssueToNumberTransformer $transformer)
    {
        $this->transformer = $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;
    }
}

Great! This will act and render like a text field (getParent()), but will automatically have the data transformer and a nice default value for the invalid_message option.

すごい!これは、テキスト フィールド (getParent()) のように機能してレンダリングしますが、自動的にデータ トランスフォーマーと、invalid_message オプションの適切なデフォルト値を持ちます。

As long as you're using autowire and autoconfigure, you can start using the form immediately:

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

    // ...
}

Tip

ヒント

If you're not using autowire and autoconfigure, see How to Create a Custom Form Field Type for how to configure your new IssueSelectorType.

autowire と autoconfigure を使用していない場合は、新しい IssueSelectorType を構成する方法について、カスタム フォーム フィールド タイプを作成する方法を参照してください。

About Model and View Transformers

In the above example, the transformer was used as a "model" transformer. In fact, there are two different types of transformers and three different types of underlying data.

上記の例では、トランスフォーマーは「モデル」トランスフォーマーとして使用されています。実際には、2 つの異なるタイプのトランスフォーマーと 3 つの異なるタイプの基になるデータがあります。

In any form, the three different types of data are:

いずれの形式でも、3 つの異なるタイプのデータは次のとおりです。
  1. Model data - This is the data in the format used in your application (e.g. an Issue object). If you call Form::getData() or Form::setData(), you're dealing with the "model" data.
    モデル データ - アプリケーションで使用される形式のデータです (Issue オブジェクトなど)。 Form::getData() または Form::setData() を呼び出す場合、「モデル」データを扱っています。
  2. Norm Data - This is a normalized version of your data and is commonly the same as your "model" data (though not in our example). It's not commonly used directly.
    標準データ - これはデータの正規化されたバージョンであり、通常は「モデル」データと同じです (ただし、この例では異なります)。一般的に直接使用されることはありません。
  3. View Data - This is the format that's used to fill in the form fields themselves. It's also the format in which the user will submit the data. When you call Form::submit($data), the $data is in the "view" data format.
    データの表示 - これは、フォーム フィールド自体に入力するために使用される形式です。これは、ユーザーがデータを送信する形式でもあります。 Form::submit($data) を呼び出すと、$data は「ビュー」データ形式になります。

The two different types of transformers help convert to and from each of these types of data:

2 つの異なるタイプのトランスフォーマーは、次の各タイプのデータとの間の変換に役立ちます。
Model transformers:
  • transform(): "model data" => "norm data"
    transform(): "モデルデータ" => "ノルムデータ"
  • reverseTransform(): "norm data" => "model data"
    reverseTransform(): "ノルムデータ" => "モデルデータ"
transform(): "モデルデータ" => "ノルムデータ"reverseTransform(): "ノルムデータ" => "モデルデータ"
View transformers:
  • transform(): "norm data" => "view data"
    transform(): "標準データ" => "データの表示"
  • reverseTransform(): "view data" => "norm data"
    reverseTransform(): "ビューデータ" => "ノルムデータ"
transform(): "標準データ" => "データを見る"reverseTransform(): "データを見る" => "標準データ"

Which transformer you need depends on your situation.

どの変圧器が必要かは、状況によって異なります。

To use the view transformer, call addViewTransformer().

ビュー トランスフォーマーを使用するには、addViewTransformer() を呼び出します。

Caution

注意

Be careful with model transformers and Collection field types. Collection's children are created early at PRE_SET_DATA by its ResizeFormListener and their data is populated later from the normalized data. So your model transformer cannot reduce the number of items within the Collection (i.e. filtering out some items), as in that case the collection ends up with some empty children.

モデル トランスフォーマーとコレクション フィールド タイプには注意してください。コレクションの子は、その ResizeFormListener によって PRE_SET_DATA の早い段階で作成され、それらのデータは後で正規化されたデータから取り込まれます。そのため、モデル トランスフォーマーは Collection 内の項目の数を減らす (つまり、いくつかの項目を除外する) ことができません。その場合、コレクションはいくつかの空の子を持つことになります。

A possible workaround for that limitation could be not using the underlying object directly, but a DTO (Data Transfer Object) instead, that implements the transformation of such incompatible data structures.

この制限に対する可能な回避策は、基礎となるオブジェクトを直接使用するのではなく、そのような互換性のないデータ構造の変換を実装する DTO (データ転送オブジェクト) を代わりに使用することです。

So why Use the Model Transformer?

In this example, the field is a text field, and a text field is always expected to be a simple, scalar format in the "norm" and "view" formats. For this reason, the most appropriate transformer was the "model" transformer (which converts to/from the norm format - string issue number - to the model format - Issue object).

この例では、フィールドはテキスト フィールドであり、テキスト フィールドは常に、"norm" および "view" 形式の単純なスカラー形式であることが期待されます。このため、最も適切なトランスフォーマーは「モデル」トランスフォーマー (ノルム形式 - 文字列の問題番号 - モデル形式 - 問題オブジェクト) との間で変換されます) でした。

The difference between the transformers is subtle and you should always think about what the "norm" data for a field should really be. For example, the "norm" data for a text field is a string, but is a DateTime object for a date field.

トランスフォーマー間の違いは微妙であり、フィールドの「標準」データが実際にどうあるべきかについて常に考える必要があります。たとえば、テキスト フィールドの「ノルム」データは文字列ですが、日付フィールドの DateTime オブジェクトです。

Tip

ヒント

As a general rule, the normalized data should contain as much information as possible.

原則として、正規化されたデータにはできるだけ多くの情報が含まれている必要があります。