Symfony comes with a bunch of core field types available for building forms. However there are situations where we want to create a custom form field type for a specific purpose. This recipe assumes we need a field definition that holds a person's gender, based on the existing choice field. This section explains how the field is defined, how we can customize its layout and finally, how we can register it for use in our application.
Symfony自带了一组用于构建表单的核心表单域类型。但在有些情况下,我们为了某个目的需要创建自定义表单域类型。本文假定我们需要基于现有的choice表单域定义一个表单域,用来保存用户性别。本文将阐述如何定义表单域、如何设定它的布局及最终显示、如何在应用程序中注册以便使用。

Defining the Field Type
定义域类型

In order to create the custom field type, first we have to create the class representing the field. In our situation the class holding the field type will be called GenderType and the file will be stored in the default location for form fields, which is <BundleName>\Form\Type. Make sure the field extends AbstractType:
为了创建自定义域类型,首先我们需要创建该域对应的类。就我们这种情况而言,我们可以将保持域类型的类定义为GenderType,并将该文件放置在表单域的缺省位置:<BundleName>\Form\Type 目录中,并确保该表单域从AbstractType类扩展:

  1. // src/Acme/DemoBundle/Form/Type/GenderType.php 
  2. namespace Acme\DemoBundle\Form\Type; 
  3.  
  4. use Symfony\Component\Form\AbstractType; 
  5. use Symfony\Component\OptionsResolver\OptionsResolverInterface; 
  6.  
  7. class GenderType extends AbstractType 
  8.     public function setDefaultOptions(OptionsResolverInterface $resolver
  9.     { 
  10.         $resolver->setDefaults(array
  11.             'choices' => array
  12.                 'm' => 'Male'
  13.                 'f' => 'Female'
  14.             ) 
  15.         )); 
  16.     } 
  17.  
  18.     public function getParent() 
  19.     { 
  20.         return 'choice'
  21.     } 
  22.  
  23.     public function getName() 
  24.     { 
  25.         return 'gender'
  26.     } 
The location of this file is not important - the Form\Type directory is just a convention.
该文件的位置并不重要,放在Form\Type目录只是根据惯例。

Here, the return value of the getParent function indicates that we're extending the choice field type. This means that, by default, we inherit all of the logic and rendering of that field type. To see some of the logic, check out the ChoiceType class. There are three methods that are particularly important:
在这里,getParent函数的返回值表明我们正在扩展choice域类型。这意味着,在缺省情况下,我们继承该域类型的全部逻辑和渲染。要查看该域类型的逻辑,请检出ChoiceType类。有三个方法是非常重要的:

  • buildForm() - Each field type has a buildForm method, which is where you configure and build any field(s). Notice that this is the same method you use to setup your forms, and it works the same here.
    buildForm() - 每个域类型都有一个buildForm方法,在该方法中您可以配置和构建任何表单域。注意这与您设置您表单的方法相同,同时它们的工作原理也相同。
  • buildView() - This method is used to set any extra variables you'll need when rendering your field in a template. For example, in ChoiceType, a multiple variable is set and used in the template to set (or not set) the multiple attribute on the select field. See Creating a Template for the Field for more details.
    buildView() - 该方法用于设置您在模板中渲染您表单域所需的任何额外变量。例如,在ChoiceType中multiple变量被设置用来在模板中设置(或不设置)select表单域的multiple属性。详情请参见为表单域创建模板
  • setDefaultOptions() - This defines options for your form type that can be used in buildForm() and buildView(). There are a lot of options common to all fields (see form Field Type), but you can create any others that you need here.
    setDefaultOptions() - 该方法定义您表单类型的选项,可用于buildForm()和buildView()方法。对于全部表单域来说已经有很多常用选项(参见表单域类型),但您也可以在这里创建您所需的其它选项。
If you're creating a field that consists of many fields, then be sure to set your "parent" type as form or something that extends form. Also, if you need to modify the "view" of any of your child types from your parent type, use the finishView() method.
如果您正在创建一个由许多表单域组成的表单域,然后您的“父”类型设为表单或者设为表单的扩展。此外,如果您需要修改您父类型下任何子类型的“视图”,请使用finishView()方法。

The getName() method returns an identifier which should be unique in your application. This is used in various places, such as when customizing how your form type will be rendered.
getName()方法返回一个在您应用程序中唯一的标识。它将被用于不同的地方,如定制您的表单类型要如何渲染。

The goal of our field was to extend the choice type to enable selection of a gender. This is achieved by fixing the choices to a list of possible genders.
我们表单域的目的是要扩展choice类型,以便能选择性别。这里通过固定的性别选择列表来实现。

Creating a Template for the Field
为表单域创建模板

Each field type is rendered by a template fragment, which is determined in part by the value of your getName() method. For more information, see What are Form Themes?.
每个域类型都是通过一个模板片断渲染的,而模板片断又部分取决于您getName()方法的值。更多信息请参见什么是表单主题?

In this case, since our parent field is choice, we don't need to do any work as our custom field type will automatically be rendered like a choice type. But for the sake of this example, let's suppose that when our field is "expanded" (i.e. radio buttons or checkboxes, instead of a select field), we want to always render it in a ul element. In your form theme template (see above link for details), create a gender_widget block to handle this:
在本例中,因为我们的父表单域是choice,所以我们不需要做任何工作,作为我们自定义的域类型会自动按choice类型模板渲染。但就在这个例子里,让我们假定我们的表单域是“扩大”的(如用单选按钮或复选框来代替select表单域),我们想总是在ul元素中渲染它。那么在您表单主题模板中(详情参见上面的链接),让我们创建一个gender_widget区块来处理上述假定:

  1. {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} 
  2. {% block gender_widget %} 
  3.     {% spaceless %} 
  4.         {% if expanded %} 
  5.             <ul {{ block('widget_container_attributes') }}> 
  6.             {% for child in form %} 
  7.                 <li> 
  8.                     {{ form_widget(child) }} 
  9.                     {{ form_label(child) }} 
  10.                 </li> 
  11.             {% endfor %} 
  12.             </ul> 
  13.         {% else %} 
  14.             {# just let the choice widget render the select tag #} 
  15.             {{ block('choice_widget') }} 
  16.         {% endif %} 
  17.     {% endspaceless %} 
  18. {% endblock %} 

Make sure the correct widget prefix is used. In this example the name should be gender_widget, according to the value returned by getName. Further, the main config file should point to the custom form template so that it's used when rendering all forms.
确保使用正确的小部件前缀。在这个例子中,根据getName返回的值,小部件的名称应该是gender_widget。此外,当自定义表单模板被用于渲染所有表单时,在主配置文件中应该指向该模板。

  1. # app/config/config.yml 
  2. twig: 
  3.     form: 
  4.         resources: 
  5.             - 'AcmeDemoBundle:Form:fields.html.twig' 

Using the Field Type
使用域类型

You can now use your custom field type immediately, simply by creating a new instance of the type in one of your forms:
您现在可以马上使用您的自定义域类型,只需要简单地在您表单中创建一个该类型的新实例:

  1. // src/Acme/DemoBundle/Form/Type/AuthorType.php 
  2. namespace Acme\DemoBundle\Form\Type; 
  3.  
  4. use Symfony\Component\Form\AbstractType; 
  5. use Symfony\Component\Form\FormBuilderInterface; 
  6.  
  7. class AuthorType extends AbstractType 
  8.     public function buildForm(FormBuilderInterface $builderarray $options
  9.     { 
  10.         $builder->add('gender_code'new GenderType(), array
  11.             'empty_value' => 'Choose a gender'
  12.         )); 
  13.     } 

But this only works because the GenderType() is very simple. What if the gender codes were stored in configuration or in a database? The next section explains how more complex field types solve this problem.
但这仅仅只是可以工作,因为GenderType()非常简单。如果gender相关代码是被保存在配置文件或数据库中呢?下一节将阐述更复杂的域类型将如何解决该问题。
 

Creating your Field Type as a Service
创建您的域类型服务

So far, this entry has assumed that you have a very simple custom field type. But if you need access to configuration, a database connection, or some other service, then you'll want to register your custom type as a service. For example, suppose that we're storing the gender parameters in configuration:
到目前为止,本文只是假定您有一个非常简单的自定义域类型。但如果您需要访问配置、数据库连接或其它服务,您需要将您自定义的类型注册成服务。举个例子,假设我们在配置中保存了gender的参数:

  1. # app/config/config.yml 
  2. parameters: 
  3.     genders: 
  4.         m: Male 
  5.         f: Female 

To use the parameter, we'll define our custom field type as a service, injecting the genders parameter value as the first argument to its to-be-created __construct function:
要使用该参数,我们需要将我们自定义的域类型定义成服务,并将genders参数值做为它的构造函数__construct的第一个参数注入:

  1. # src/Acme/DemoBundle/Resources/config/services.yml 
  2. services: 
  3.     form.type.gender: 
  4.         class: Acme\DemoBundle\Form\Type\GenderType 
  5.         arguments: 
  6.             - "%genders%" 
  7.         tags: 
  8.             - { name: form.type, alias: gender } 
Make sure the services file is being imported. See Importing Configuration with imports for details.
确保服务文件已经被导入。详情参见用imports导入配置

Be sure that the alias attribute of the tag corresponds with the value returned by the getName method defined earlier. We'll see the importance of this in a moment when we use the custom field type. But first, add a __construct argument to GenderType, which receives the gender configuration:
务必让tag的alias属性与前面定义的getName方法的返回值一致。当我们使用自定义表单域类型时这非常重要。但首先,请添加GenderType的__construct参数,用以接受gender的配置参数值:

  1. // src/Acme/DemoBundle/Form/Type/GenderType.php 
  2. namespace Acme\DemoBundle\Form\Type; 
  3.  
  4. use Symfony\Component\OptionsResolver\OptionsResolverInterface; 
  5.  
  6. // ... 
  7.  
  8. class GenderType extends AbstractType 
  9.     private $genderChoices
  10.  
  11.     public function __construct(array $genderChoices
  12.     { 
  13.         $this->genderChoices = $genderChoices
  14.     } 
  15.  
  16.     public function setDefaultOptions(OptionsResolverInterface $resolver
  17.     { 
  18.         $resolver->setDefaults(array
  19.             'choices' => $this->genderChoices, 
  20.         )); 
  21.     } 
  22.  
  23.     // ... 

Great! The GenderType is now fueled by the configuration parameters and registered as a service. And because we used the form.type alias in its configuration, using the field is now much easier:
太棒了!GenderType现在可以通过配置参数生效,并被注册成服务。因为我们在它的配置中使用了form.type别名,现在使用表单域更容易了:

  1. // src/Acme/DemoBundle/Form/Type/AuthorType.php 
  2. namespace Acme\DemoBundle\Form\Type; 
  3.  
  4. use Symfony\Component\Form\FormBuilderInterface; 
  5.  
  6. // ... 
  7.  
  8. class AuthorType extends AbstractType 
  9.     public function buildForm(FormBuilderInterface $builderarray $options
  10.     { 
  11.         $builder->add('gender_code''gender'array
  12.             'empty_value' => 'Choose a gender'
  13.         )); 
  14.     } 

Notice that instead of instantiating a new instance, we can just refer to it by the alias used in our service configuration, gender. Have fun!
注意,与实例化一个新的实例不同,我们只需要通过在我们的服务配置中使用别名gender,就可以引用它。玩得开心!