跳到内容

如何创建自定义表单字段类型

编辑此页

Symfony 自带 数十种表单类型(在其他项目中称为“表单字段”),可以在您的应用程序中直接使用。但是,创建自定义表单类型以解决项目中的特定目的也很常见。

基于 Symfony 内置类型创建表单类型

创建表单类型最简单的方法是基于 现有表单类型 之一。假设您的项目将“运输选项”列表显示为 <select> HTML 元素。这可以使用 ChoiceType 来实现,其中 choices 选项设置为可用运输选项的列表。

但是,如果您在多个表单中使用相同的表单类型,则每次使用时都重复 choices 列表会很快变得乏味。在本示例中,更好的解决方案是创建基于 ChoiceType 的自定义表单类型。自定义类型的外观和行为类似于 ChoiceType,但选项列表已预先填充了运输选项,因此您无需定义它们。

表单类型是实现 FormTypeInterface 的 PHP 类,但您应该改为从 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() 告诉 Symfony 以 ChoiceType 作为起点,然后 configureOptions() 覆盖其某些选项。(本文稍后将详细解释 FormTypeInterface 的所有方法。)生成的表单类型是具有预定义选项的选择字段。

现在,您可以在 创建 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)
        ;
    }

    // ...
}

就这样。 shipping 表单字段将在任何模板中正确呈现,因为它重用了其父类型 ChoiceType 定义的模板逻辑。如果您愿意,您还可以为自定义类型定义模板,如本文稍后所述。

从零开始创建表单类型

某些表单类型非常特定于您的项目,以至于它们不能基于任何 现有表单类型,因为它们差异太大。考虑一个应用程序,该应用程序希望在不同的表单中重用以下字段集作为“邮政地址”

如上所述,表单类型是实现 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
{
    // ...
}

以下是表单类型类可以定义的最重要的方法

getParent()

如果您的自定义类型基于另一种类型(即它们共享某些功能),请添加此方法以返回该原始类型的完全限定类名。不要为此使用 PHP 继承。在调用自定义类型中定义的方法之前,Symfony 将调用父类型和父类型扩展的所有表单类型方法(buildForm()buildView() 等)和类型扩展。

否则,如果您的自定义类型是从头开始构建的,则可以省略 getParent()

默认情况下,AbstractType 类返回通用的 FormType 类型,它是 Form 组件中所有表单类型的根父类型。

configureOptions()
它定义了使用表单类型时可配置的选项,这些选项也是可以在以下方法中使用的选项。选项从父类型和父类型扩展继承,但您可以创建所需的任何自定义选项。
buildForm()
它配置当前表单,并可能添加嵌套字段。它与 在类中创建 Symfony 表单 时使用的方法相同。
buildView()
它设置在表单主题模板中呈现字段时所需的任何额外变量。
finishView()
buildView() 相同。仅当您的表单类型由许多字段组成时(即由许多单选按钮或复选框组成的 ChoiceType)才有用,因为此方法将允许使用 $view['child_name'] 访问子视图。对于任何其他用例,建议改用 buildView()

定义表单类型

首先添加 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',
            ])
        ;
    }
}

提示

运行以下命令以验证表单类型是否已在应用程序中成功注册

1
$ php bin/console debug:form

此表单类型已准备好在其他表单中使用,并且其所有字段将在任何模板中正确呈现

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

    // ...
}

但是,自定义表单类型的真正威力是通过自定义表单选项(使其灵活)和自定义模板(使其外观更好)来实现的。

为表单类型添加配置选项

假设您的项目需要以两种方式配置 PostalAddressType

  • 除了“地址行 1”和“地址行 2”之外,某些地址应允许显示“地址行 3”以存储扩展的地址信息;
  • 某些地址不应显示自由文本输入,而应能够将可能的州/省/自治区/直辖市限制为给定的列表。

这可以通过“表单类型选项”来解决,该选项允许配置表单类型的行为。这些选项在 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
42
// 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): ?array
        {
            if (null === $states) {
                return $states;
            }

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

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

现在,您可以在使用表单类型时配置这些选项

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',
            ])
        ;
    }

    // ...
}

最后一步是在构建表单时使用这些选项

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',
            ]);
        }
    }
}

创建表单类型模板

默认情况下,自定义表单类型将使用应用程序中配置的 表单主题 进行呈现。但是,对于某些类型,您可能更喜欢创建自定义模板,以便自定义它们的外观或 HTML 结构。

首先,在应用程序中的任何位置创建一个新的 Twig 模板,以存储用于呈现类型的片段

1
2
3
{# templates/form/custom_types.html.twig #}

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

然后,更新 form_themes 选项,将此新模板添加到列表的末尾(每个主题都会覆盖所有以前的主题)

1
2
3
4
5
# config/packages/twig.yaml
twig:
    form_themes:
        - '...'
        - 'form/custom_types.html.twig'

最后一步是创建将呈现类型的实际 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 %}

Twig 代码块名称的第一部分(例如 postal_address)来自类名(PostalAddressType -> postal_address)。这可以通过覆盖 PostalAddressType 中的 getBlockPrefix() 方法来控制。Twig 代码块名称的第二部分(例如 _row)定义了正在呈现的表单类型部分(row、widget、help、errors 等)。

关于表单主题的文章详细解释了 表单片段命名规则。以下是邮政地址类型的 Twig 代码块名称的一些示例

postal_address_row
完整的表单类型代码块。
postal_address_addressLine1_help
第一个地址行下方的帮助消息代码块。
postal_address_state_widget
州/省/自治区/直辖市字段的文本输入小部件。
postal_address_zipCode_label
邮政编码字段的标签代码块。

警告

当您的表单类的名称与任何内置字段类型匹配时,您的表单可能无法正确呈现。名为 App\Form\PasswordType 的表单类型将具有与内置 PasswordType 相同的代码块名称,并且无法正确呈现。覆盖 getBlockPrefix() 方法以返回唯一的代码块前缀(例如 app_password)以避免冲突。

将变量传递到表单类型模板

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
// 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
{
    public function __construct(
        private EntityManagerInterface $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('...');
    }
}

如果您使用的是 默认的 services.yaml 配置,则此示例已可以正常工作!否则,请为此表单类 创建一个服务,并使用 form.type 标记它

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 %}
这项工作,包括代码示例,均根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本