How to Create a Custom Form Field Type

Symfony comes with tens of form types (called "form fields" in other projects) ready to use in your applications. However, it's common to create custom form types to solve specific purposes in your projects.

symfony には、アプリケーションですぐに使用できる数十のフォーム タイプ (他のプロジェクトでは「フォーム フィールド」と呼ばれます) が付属しています。ただし、プロジェクトの特定の目的を解決するために、カスタム フォーム タイプを作成するのが一般的です。

Creating Form Types Based on Symfony Built-in Types

The easiest way to create a form type is to base it on one of the existing form types. Imagine that your project displays a list of "shipping options" as a <select> HTML element. This can be implemented with a ChoiceType where the choices option is set to the list of available shipping options.

フォーム タイプを作成する最も簡単な方法は、既存のフォーム タイプのいずれかに基づいて作成することです。プロジェクトが「配送オプション」のリストを HTML 要素として表示するとします。これは、choices オプションが利用可能な配送オプションのリストに設定されている ChoiceType で実装できます。

However, if you use the same form type in several forms, repeating the list of choices every time you use it quickly becomes boring. In this example, a better solution is to create a custom form type based on ChoiceType. The custom type looks and behaves like a ChoiceType but the list of choices is already populated with the shipping options so you don't need to define them.

ただし、同じフォーム タイプを複数のフォームで使用する場合、使用するたびに選択肢のリストを繰り返すのはすぐに退屈になります。この例では、ChoiceType に基づいてカスタム フォーム タイプを作成することをお勧めします。 custom タイプは ChoiceType のように見え、動作しますが、選択肢のリストには配送オプションがすでに入力されているため、それらを定義する必要はありません。

Form types are PHP classes that implement FormTypeInterface, but you should instead extend from AbstractType, which already implements that interface and provides some utilities. By convention they are stored in the src/Form/Type/ directory:

フォーム タイプは FormTypeInterface を実装する PHP クラスですが、代わりに AbstractType から拡張する必要があります。AbstractType は既にそのインターフェイスを実装しており、いくつかのユーティリティを提供しています。慣例により、それらは src/Form/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/ShippingType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ShippingType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'choices' => [
                'Standard Shipping' => 'standard',
                'Expedited Shipping' => 'expedited',
                'Priority Shipping' => 'priority',
            ],
        ]);
    }

    public function getParent(): string
    {
        return ChoiceType::class;
    }
}

getParent() tells Symfony to take ChoiceType as a starting point, then configureOptions() overrides some of its options. (All methods of the FormTypeInterface are explained in detail later in this article.) The resulting form type is a choice field with predefined choices.

getParent() は Symfony に ChoiceType を開始点として使用するよう指示し、configureOptions() はそのオプションの一部をオーバーライドします。 (FormTypeInterface のすべてのメソッドについては、この記事の後半で詳しく説明します。) 結果のフォーム タイプは、定義済みの選択肢を持つ選択肢フィールドです。

Now you can add this form type when creating Symfony forms:

これで、Symfony フォームを作成するときにこのフォーム タイプを追加できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Form/Type/OrderType.php
namespace App\Form\Type;

use App\Form\Type\ShippingType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('shipping', ShippingType::class)
        ;
    }

    // ...
}

That's all. The shipping form field will be rendered correctly in any template because it reuses the templating logic defined by its parent type ChoiceType. If you prefer, you can also define a template for your custom types, as explained later in this article.

それで全部です。親の typeChoiceType で定義されたテンプレート ロジックを再利用するため、配送フォーム フィールドはどのテンプレートでも正しくレンダリングされます。必要に応じて、この記事の後半で説明するように、customtypes のテンプレートを定義することもできます。

Creating Form Types Created From Scratch

Some form types are so specific to your projects that they cannot be based on any existing form types because they are too different. Consider an application that wants to reuse in different forms the following set of fields as the "postal address":

一部のフォーム タイプはプロジェクトに固有のものであり、既存のフォーム タイプに基づいて作成することはできません。次の一連のフィールドをさまざまな形式で「住所」として再利用するアプリケーションを考えてみましょう。

As explained above, form types are PHP classes that implement FormTypeInterface, although it's more convenient to extend instead from AbstractType:

上で説明したように、フォーム タイプは FormTypeInterface を実装する PHP クラスですが、AbstractType から継承する方が便利です。
1
2
3
4
5
6
7
8
9
10
11
// src/Form/Type/PostalAddressType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostalAddressType extends AbstractType
{
    // ...
}

These are the most important methods that a form type class can define:

フォーム タイプ クラスで定義できる最も重要なメソッドは次のとおりです。
buildForm()
It adds and configures other types into this type. It's the same method used when creating Symfony form classes.
このタイプに他のタイプを追加して構成します。これは、Symfony フォーム クラスを作成するときに使用されるメソッドと同じです。
buildView()
It sets any extra variables you'll need when rendering the field in a template.
テンプレートでフィールドをレンダリングするときに必要な追加の変数を設定します。
finishView()
This method allows to modify the "view" of any rendered widget. This is useful if your form type consists of many fields, or contains a type that produces many HTML elements (e.g. ChoiceType). For any other use case, it's recommended to use buildView() instead.
このメソッドを使用すると、レンダリングされたウィジェットの「ビュー」を変更できます。これは、フォーム タイプが多くのフィールドで構成されている場合、または多くの HTML 要素を生成するタイプ (ChoiceType など) が含まれている場合に役立ちます。その他の使用例では、代わりに buildView() を使用することをお勧めします。
configureOptions()
It defines the options configurable when using the form type, which are also the options that can be used in buildForm() and buildView() methods. Options are inherited from parent types and parent type extensions, but you can create any custom option you need.
フォーム タイプを使用するときに構成可能なオプションを定義します。これは、buildForm() および buildView() メソッドで使用できるオプションでもあります。オプションは親タイプと親タイプ拡張から継承されますが、必要なカスタム オプションを作成できます。
getParent()

If your custom type is based on another type (i.e. they share some functionality), add this method to return the fully-qualified class name of that original type. Do not use PHP inheritance for this. Symfony will call all the form type methods (buildForm(), buildView(), etc.) and type extensions of the parent before calling the ones defined in your custom type.

カスタム型が別の型に基づいている (つまり、いくつかの機能を共有している) 場合は、このメソッドを追加して、元の型の完全修飾クラス名を返します。これには PHP 継承を使用しないでください。Symfony は、カスタム タイプで定義されたものを呼び出す前に、すべてのフォーム タイプ メソッド (buildForm()、buildView() など) と親のタイプ拡張を呼び出します。

Otherwise, if your custom type is build from scratch, you can omit getParent().

それ以外の場合、カスタム タイプが最初から作成されている場合は、getParent() を省略できます。

By default, the AbstractType class returns the generic FormType type, which is the root parent for all form types in the Form component.

デフォルトでは、AbstractType クラスは、Form コンポーネント内のすべてのフォーム タイプのルートの親である genericFormTypetype を返します。

Defining the Form Type

Start by adding the buildForm() method to configure all the types included in the postal address. For the moment, all fields are of type TextType:

buildForm() メソッドを追加して、住所に含まれるすべてのタイプを構成することから始めます。現時点では、すべてのフィールドは TextType 型です。
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/PostalAddressType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class PostalAddressType extends AbstractType
{
    // ...

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('addressLine1', TextType::class, [
                'help' => 'Street address, P.O. box, company name',
            ])
            ->add('addressLine2', TextType::class, [
                'help' => 'Apartment, suite, unit, building, floor',
            ])
            ->add('city', TextType::class)
            ->add('state', TextType::class, [
                'label' => 'State',
            ])
            ->add('zipCode', TextType::class, [
                'label' => 'ZIP Code',
            ])
        ;
    }
}

Tip

ヒント

Run the following command to verify that the form type was successfully registered in the application:

次のコマンドを実行して、フォーム タイプがアプリケーションに正常に登録されたことを確認します。
1
$ php bin/console debug:form

This form type is ready to use it inside other forms and all its fields will be correctly rendered in any template:

このフォーム タイプは他のフォーム内で使用する準備ができており、そのすべてのフィールドはどのテンプレートでも正しくレンダリングされます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Form/Type/OrderType.php
namespace App\Form\Type;

use App\Form\Type\PostalAddressType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('address', PostalAddressType::class)
        ;
    }

    // ...
}

However, the real power of custom form types is achieved with custom form options (to make them flexible) and with custom templates (to make them look better).

ただし、カスタム フォーム タイプの真の力は、カスタム フォーム オプション (柔軟にするため) とカスタム テンプレート (見栄えをよくするため) によって実現されます。

Adding Configuration Options for the Form Type

Imagine that your project requires to make the PostalAddressType configurable in two ways:

プロジェクトで PostalAddressType を次の 2 つの方法で構成可能にする必要があるとします。
  • In addition to "address line 1" and "address line 2", some addresses should be allowed to display an "address line 3" to store extended address information;
    「住所行 1」と「住所行 2」に加えて、拡張住所情報を格納するために「住所行 3」を表示できるようにする必要があります。
  • Instead of displaying a free text input, some addresses should be able to restrict the possible states to a given list.
    自由なテキスト入力を表示する代わりに、一部のアドレスは可能な状態を特定のリストに制限できる必要があります。

This is solved with "form type options", which allow to configure the behavior of the form types. The options are defined in the configureOptions() method and you can use all the OptionsResolver component features to define, validate and process their values:

これは、フォーム タイプの動作を設定できる「フォーム タイプ オプション」で解決されます。オプションは configureOptions() メソッドで定義され、すべての OptionsResolver コンポーネント機能を使用して、それらの値を定義、検証、および処理できます。
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
// src/Form/Type/PostalAddressType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostalAddressType extends AbstractType
{
    // ...

    public function configureOptions(OptionsResolver $resolver): void
    {
        // this defines the available options and their default values when
        // they are not configured explicitly when using the form type
        $resolver->setDefaults([
            'allowed_states' => null,
            'is_extended_address' => false,
        ]);

        // optionally you can also restrict the options type or types (to get
        // automatic type validation and useful error messages for end users)
        $resolver->setAllowedTypes('allowed_states', ['null', 'string', 'array']);
        $resolver->setAllowedTypes('is_extended_address', 'bool');

        // optionally you can transform the given values for the options to
        // simplify the further processing of those options
        $resolver->setNormalizer('allowed_states', static function (Options $options, $states) {
            if (null === $states) {
                return $states;
            }

            if (is_string($states)) {
                $states = (array) $states;
            }

            return array_combine(array_values($states), array_values($states));
        });
    }
}

Now you can configure these options when using the form type:

フォーム タイプを使用するときに、これらのオプションを設定できるようになりました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Form/Type/OrderType.php
namespace App\Form\Type;

// ...

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('address', PostalAddressType::class, [
                'is_extended_address' => true,
                'allowed_states' => ['CA', 'FL', 'TX'],
                // in this example, this config would also be valid:
                // 'allowed_states' => 'CA',
            ])
        ;
    }

    // ...
}

The last step is to use these options when building the form:

最後のステップは、フォームを作成するときにこれらのオプションを使用することです。
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/PostalAddressType.php
namespace App\Form\Type;

// ...

class PostalAddressType extends AbstractType
{
    // ...

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        // ...

        if (true === $options['is_extended_address']) {
            $builder->add('addressLine3', TextType::class, [
                'help' => 'Extended address info',
            ]);
        }

        if (null !== $options['allowed_states']) {
            $builder->add('state', ChoiceType::class, [
                'choices' => $options['allowed_states'],
            ]);
        } else {
            $builder->add('state', TextType::class, [
                'label' => 'State/Province/Region',
            ]);
        }
    }
}

Creating the Form Type Template

By default, custom form types will be rendered using the form themes configured in the application. However, for some types you may prefer to create a custom template in order to customize how they look or their HTML structure.

デフォルトでは、カスタム フォーム タイプは、アプリケーションで構成されたフォーム テーマを使用してレンダリングされます。ただし、一部のタイプでは、外観や HTML 構造をカスタマイズするために、カスタム テンプレートを作成することをお勧めします。

First, create a new Twig template anywhere in the application to store the fragments used to render the types:

最初に、アプリケーション内の任意の場所に新しい Twig テンプレートを作成して、型のレンダリングに使用されるフラグメントを保存します。
1
2
3
{# templates/form/custom_types.html.twig #}

{# ... here you will add the Twig code ... #}

Then, update the form_themes option to add this new template at the beginning of the list (the first one overrides the rest of files):

次に、form_themes オプションを更新して、この新しいテンプレートをリストの先頭に追加します (最初のテンプレートが残りのファイルを上書きします)。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
# config/packages/twig.yaml
twig:
    form_themes:
        - 'form/custom_types.html.twig'
        - '...'

The last step is to create the actual Twig template that will render the type. The template contents depend on which HTML, CSS and JavaScript frameworks and libraries are used in your application:

最後のステップは、型をレンダリングする実際の Twig テンプレートを作成することです。テンプレートの内容は、アプリケーションで使用されている HTML、CSS、および JavaScript フレームワークとライブラリによって異なります。
1
2
3
4
5
6
7
8
9
10
11
{# templates/form/custom_types.html.twig #}
{% block postal_address_row %}
    {% for child in form.children|filter(child => not child.rendered) %}
        <div class="form-group">
            {{ form_label(child) }}
            {{ form_widget(child) }}
            {{ form_help(child) }}
            {{ form_errors(child) }}
        </div>
    {% endfor %}
{% endblock %}

The first part of the Twig block name (e.g. postal_address) comes from the class name (PostalAddressType -> postal_address). This can be controlled by overriding the getBlockPrefix() method in PostalAddressType. The second part of the Twig block name (e.g. _row) defines which form type part is being rendered (row, widget, help, errors, etc.)

Twig ブロック名の最初の部分 (例: postal_address) は、クラス名 (PostalAddressType -> postal_address) に由来します。これは、PostalAddressType の getBlockPrefix() メソッドをオーバーライドすることで制御できます。 Twig ブロック名の 2 番目の部分 (例: _row) は、レンダリングされるフォーム タイプのパーツ (行、ウィジェット、ヘルプ、エラーなど) を定義します。

The article about form themes explains the form fragment naming rules in detail. The following diagram shows some of the Twig block names defined in this example:

フォーム テーマに関する記事では、フォーム フラグメントの命名規則について詳しく説明しています。次の図は、この例で定義されている Twig ブロック名の一部を示しています。

Caution

注意

When the name of your form class matches any of the built-in field types, your form might not be rendered correctly. A form type named App\Form\PasswordType will have the same block name as the built-in PasswordType and won't be rendered correctly. Override the getBlockPrefix() method to return a unique block prefix (e.g. app_password) to avoid collisions.

フォーム クラスの名前がいずれかの組み込みフィールド タイプと一致する場合、フォームが正しくレンダリングされない可能性があります。 App\Form\PasswordType という名前のフォーム タイプは、組み込みのPasswordType と同じブロック名を持つため、正しくレンダリングされません。競合を避けるために、getBlockPrefix() メソッドをオーバーライドして一意のブロック プレフィックス (app_password など) を返します。

Passing Variables to the Form Type Template

Symfony passes a series of variables to the template used to render the form type. You can also pass your own variables, which can be based on the options defined by the form or be completely independent:

symfony は一連の変数をフォームタイプのレンダリングに使用されるテンプレートに渡します。フォームで定義されたオプションに基づくか、完全に独立している独自の変数を渡すこともできます。
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/Type/PostalAddressType.php
namespace App\Form\Type;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
// ...

class PostalAddressType extends AbstractType
{
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    // ...

    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
        // pass the form type option directly to the template
        $view->vars['isExtendedAddress'] = $options['is_extended_address'];

        // make a database query to find possible notifications related to postal addresses (e.g. to
        // display dynamic messages such as 'Delivery to XX and YY states will be added next week!')
        $view->vars['notification'] = $this->entityManager->find('...');
    }
}

If you're using the default services.yaml configuration, this example will already work! Otherwise, create a service for this form class and tag it with form.type.

デフォルトの services.yaml 構成を使用している場合、この例は既に機能しています。それ以外の場合は、このフォーム クラスのサービスを作成し、form.type でタグ付けします。

The variables added in buildView() are available in the form type template as any other regular Twig variable:

buildView() に追加された変数は、他の通常の Twig 変数と同様に、フォーム タイプ テンプレートで使用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{# templates/form/custom_types.html.twig #}
{% block postal_address_row %}
    {# ... #}

    {% if isExtendedAddress %}
        {# ... #}
    {% endif %}

    {% if notification is not empty %}
        <div class="alert alert-primary" role="alert">
            {{ notification }}
        </div>
    {% endif %}
{% endblock %}