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

2.8 版本
维护中的版本

Symfony提供了一群核心字段类型可供构建表单。然而在有些情况下,您可能想要创建一个自定义表单字段类型实现一个特定的目标。假设你需要定义人的性别的字段,基于现存的choice字段。本章将解释如何定义这个字段,你将如何自定义它的布局以及最后你将如何将它注册到你的应用程序中来使用。

定义字段类型 

为了创建这个自定义的字段类型,首先你需要去创建这个代表字段的类。在这种情况下,持有这个字段类型的类被称为GenderType并且文件将被存储在表单字段的默认位置,<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
// src/AppBundle/Form/Type/GenderType.php
namespace AppBundle\Form\Type;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 
class GenderType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'choices' => array(
                'm' => 'Male',
                'f' => 'Female',
            )
        ));
    }
 
    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。此外,如果你想要修改你的父类型中产生的子类型的任何“视图”,就使用 finishView() 方法。

这个字段的目的是继承choice类型使其能够选择性别。可以通过choices去配置性别列表。

为字段创建一个模板 

每一个字段都会被一个模板片段渲染,一定程度上取决于你的类型的类名。更多信息请查看什么是表单主题?

Note

前缀(例如gender)的第一部分来自类名(GenderType -> gender)。这可以通过重写GenderTypegetBlockPrefix()来控制 。

在这种情况下,由于父字段是ChoiceType,你并不需要做任何工作这是由于定制的字段会自动被当做ChoiceType来渲染。但是为了这个例子考虑,假设当你的字段是“expanded”(例如 radio button 或者checkboxes,而不是选择字段),你想要一直在 ul 元素中渲染它。在你的表单主题模板中(详见上面的链接),创建一个 gender_widget 来处理它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{# app/Resources/views/form/fields.html.twig #}
{% block gender_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 #}
            {{ block('choice_widget') }}
        {% endif %}
    {% endspaceless %}
{% endblock %}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- app/Resources/views/form/gender_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

为了确保正确的控件前缀被使用。这个例子的名字应该是gender_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\GenderType;
 
class AuthorType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('gender_code', GenderType::class, array(
            'placeholder' => 'Choose a gender',
        ));
    }
}

但是,这只能是因为GenderType很简单。如果这个性别代码存储在配置或数据库中该如何?下一节介绍更复杂的字段类型是如何解决这个问题。

访问服务及配置 

到目前为止,这个教程已假设你有一个非常简单的自定义字段类型。但是,如果你需要访问配置,数据库连接,或者一些其他的服务,那么你要注册您的自定义类型为服务。例如,你将性别参数存储在配置中:

1
2
3
4
5
# app/config/config.yml
parameters:
    genders:
        m: Male
        f: Female
1
2
3
4
5
6
7
<!-- app/config/config.xml -->
<parameters>
    <parameter key="genders" type="collection">
        <parameter key="m">Male</parameter>
        <parameter key="f">Female</parameter>
    </parameter>
</parameters>
1
2
3
// app/config/config.php
$container->setParameter('genders.m', 'Male');
$container->setParameter('genders.f', 'Female');

去使用这个参数,定义自定义字段类型作为一种服务,注入genders 参数值作为第一个参数传入到你将要创建的 __construct中去:

1
2
3
4
5
6
7
8
# src/AppBundle/Resources/config/services.yml
services:
    app.form.type.gender:
        class: AppBundle\Form\Type\GenderType
        arguments:
            - '%genders%'
        tags:
            - { name: form.type }
1
2
3
4
5
<!-- src/AppBundle/Resources/config/services.xml -->
<service id="app.form.type.gender" class="AppBundle\Form\Type\GenderType">
    <argument>%genders%</argument>
    <tag name="form.type" />
</service>
1
2
3
4
5
6
7
8
9
10
// src/AppBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
 
$container
    ->setDefinition('app.form.type.gender', new Definition(
        'AppBundle\Form\Type\GenderType',
        array('%genders%')
    ))
    ->addTag('form.type')
;

Tip

确保服务文件被导入,请参见 用imports指令导入配置信息

首先,添加一个__construct方法到GenderType,接收性别配置:

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
// src/AppBundle/Form/Type/GenderType.php
namespace AppBundle\Form\Type;
 
use Symfony\Component\OptionsResolver\OptionsResolver;
 
// ...
 
// ...
class GenderType extends AbstractType
{
    private $genderChoices;
 
    public function __construct(array $genderChoices)
    {
        $this->genderChoices = $genderChoices;
    }
 
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'choices' => $this->genderChoices,
        ));
    }
 
    // ...
}

非常好!GenderType 现在已经拥有配置参数并且注册成为了服务。这是因为你在配置文件中使用了form.type别名,你的服务被使用,而不是创建了一个新的GenderType。换句话说,你的控制器并不需要改变,它仍然是这样的:

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\GenderType;
 
class AuthorType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('gender_code', GenderType::class, array(
            'placeholder' => 'Choose a gender',
        ));
    }
}

祝你玩的开心!

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

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