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

3.4 版本
维护中的版本

Symfony提供了一组核心字段类型,用于构建表单。然而某些情况下,你可能想要创建一个自定义的表单字段类型去实现一个特定的目标。本文假设你需要一个字段来定义快递选项,基于已有的choice字段。下面将解释如何定义字段,你将如何自定义其布局,以及,最后如何将它注册到程序中来使用。

定义字段类型 

为了创建自定义的字段类型,首先要创建代表这个字段的类。在这种情况下,持有字段类型的类将被称为 ShippingType,并且类文件将被存储在表单字段专享的默认位置,即 <BundleName>\Form\Type。请确保字段继承了 AbstractType

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/AppBundle/Form/Type/ShippingType.php
namespace AppBundle\Form\Type;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 
class ShippingType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'choices' => array(
                'Standard Shipping' => 'standard',
                'Expedited Shipping' => 'expedited',
                'Priority Shipping' => 'priority',
            )
        ));
    }
 
    public function getParent()
    {
        return ChoiceType::class;
    }
}

Tip

此文件的位置并不重要,Form\Type 目录仅仅是一个约定(惯例)。

在这里,getParent() 函数的返回值指明了你正在扩展 ChoiceType 字段。这意味着,默认情况下,你继承了这个字段类型的全部逻辑和模板输出。你可以查看 ChoiceType 类来看到这些逻辑。它有三个极为重要的方法:

buildForm()
每个字段类型都有一个 buildForm() 方法,这里是你配置和构建任意(多个)字段的地方。注意这是和你用来设置 你的 表单的同一个方法,此处的用法(和设置表单)也是相同的。
buildView()
这个方法可用于设置任何额外的变量,你在模板中输出你的字段时需要用到。例如,在 ChoiceType 中,设置了一个 multiple 变量,它在模板中用于设置 (或不去设置) select 字段的 multiple 属性。参考 为字段创建一个模板 以了解更多。
configureOptions()
这个方法定义了你的表单字段的选项,它们可以用在 buildForm()buildView() 中。有大量选项常被用于所有的字段中 (参考 FormType Field),但你也可以在这里创建所需的任何其他的选项。

Tip

如果你创建一个包含有很多字段的字段,记得将你的 “父”类型 设置成 form 或者是继承了 form 的类型。同时,如果你需要从父类型中修改任何一个子类型的“view”,可使用 finishView() 方法。

字段的目的是继承choice类型来开启shipping type(“快递”表单类型)的选项。这可以通过把 choices 调整为一个可用的“快递选项”清单来实现。

为字段创建一个模板 

每个字段都是通过一个模板码段来输出的,这个“码段”是由你的类型之名称的某些部分来决定的。参考 什么是表单主题? 以了解更多。

Note

前缀中的第一个部分 (如shipping) 来自类名 (ShippingType -> shipping) 。这可以通过覆写 ShippingType 中的 getBlockPrefix() 方法来控制 。

Caution

当你的表单类的名字匹配到任何一个内置的字段类型时,你的表单可能不会正确输出。一个命名为 AppBundle\Form\PasswordType 的表单字段类型,将和内置的 PasswordType 拥有相同的码段区块名称,且将不会被正确地渲染输出。覆写 getBlockPrefix() 方法以返回一个唯一的区块前缀(如 app_password)来避免冲突。

本例中,由于父字段是 ChoiceType,你 毋须 做任何工作,因为这个自定义的字段会自动地作为 ChoiceType 被输出。出于演示的目的,假设你的字段是“expanded”(即是 radio 或是 checkboxes 之类,而非select下拉字段),你希望始终在 ul 元素中来渲染它。在你的表单主题模板中(详见上面的链接),创建一个 shipping_widget 区块来处理它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{# app/Resources/views/form/fields.html.twig #}
{% block shipping_widget %}
    {% spaceless %}
        {% if expanded %}
            <ul {{ block('widget_container_attributes') }}>
            {% for child in form %}
                <li>
                    {{ form_widget(child) }}
                    {{ form_label(child) }}
                </li>
            {% endfor %}
            </ul>
        {% else %}
            {# just let the choice widget render the select tag #}
            {# 只需让 choice widget 来渲染 select 标签 #}
            {{ block('choice_widget') }}
        {% endif %}
    {% endspaceless %}
{% endblock %}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- app/Resources/views/form/shipping_widget.html.php -->
<?php if ($expanded) : ?>
    <ul <?php $view['form']->block($form, 'widget_container_attributes') ?>>
    <?php foreach ($form as $child) : ?>
        <li>
            <?php echo $view['form']->widget($child) ?>
            <?php echo $view['form']->label($child) ?>
        </li>
    <?php endforeach ?>
    </ul>
<?php else : ?>
    <!-- just let the choice widget render the select tag -->
    <?php echo $view['form']->renderBlock('choice_widget') ?>
<?php endif ?>

Note

要确保正确的控件(widget)前缀被使用。本例中的名字应该是 shipping_widget(参考 什么是表单主题?)。进一步的,在主力配置文件指向这个自定义的表单模板,以便在渲染所有表单时能够使用它。

使用Twig时的配置如下:

1
2
3
4
# app/config/config.yml
twig:
    form_themes:
        - 'form/fields.html.twig'
1
2
3
4
<!-- app/config/config.xml -->
<twig:config>
    <twig:form-theme>form/fields.html.twig</twig:form-theme>
</twig:config>
1
2
3
4
5
6
// app/config/config.php
$container->loadFromExtension('twig', array(
    'form_themes' => array(
        'form/fields.html.twig',
    ),
));

对于php模板引擎,你的配置信息应该是这样的:

1
2
3
4
5
6
# app/config/config.yml
framework:
    templating:
        form:
            resources:
                - ':form:fields.html.php'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
    http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
 
    <framework:config>
        <framework:templating>
            <framework:form>
                <framework:resource>:form:fields.html.php</twig:resource>
            </framework:form>
        </framework:templating>
    </framework:config>
</container>
1
2
3
4
5
6
7
8
9
10
// app/config/config.php
$container->loadFromExtension('framework', array(
    'templating' => array(
        'form' => array(
            'resources' => array(
                ':form:fields.html.php',
            ),
        ),
    ),
));

使用字段类型 

现在,你可以立即使用你的自定义字段类型了,只需在你的表单中的某个字段里为该类型创建一个新实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/ src/AppBundle/Form/Type/AuthorType.php
namespace AppBundle\Form\Type;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use AppBundle\Form\Type\ShippingType;
 
class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('shipping_code', ShippingType::class, array(
            'placeholder' => 'Choose a delivery option',
        ));
    }
}

但这只是勉强能用而已,因为 ShippingType 太过简单。如果快递部分的相关代码存在了配置文件中或数据库中又该如何?下一节将介绍更为复杂的字段类型以解决此问题。

访问服务及配置 

如果你需要在表单类中访问 服务,请按通常的方式来添加一个 __construct() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/AppBundle/Form/Type/ShippingType.php
namespace AppBundle\Form\Type;
 
// ...
use Doctrine\ORM\EntityManagerInterface;
 
class ShippingType extends AbstractType
{
    private $em;
 
    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }
 
    // use $this->em down anywhere you want ...
    // 可以在下面的代码中随需使用 $this->em ...
}

如果你使用了默认的 services.yml 配置信息 (即 Form/ 下的文件会被当作服务来加载,而且开启了 autoconfigure 选项),这将始终能够运行!参考 在容器中创建/配置服务 以了解更多。

Tip

如果你未使用 自动配置,则应确保对你的服务 打上 form.type 标签。

玩得开心!

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)