如何创建自定义表单字段类型
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 %}