> Symfony中文手册 > 如何去创建一个自定义的表单字段类型

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

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 标签。

玩得开心!