CollectionType字段

3.4 版本
维护中的版本

本字段类型,用于输出某些字段或表单的“集合(collection)”。从最简单的意义上说,它可以是一个 TextType 字段的数组,装载着 emails 值。在复杂场合下,你可以内嵌整个表单,这在创建one-to-many(一对多)关联的表单时(如,在一个产品表单中,你可以管理关联的多张产品图片),十分有用。

Rendered as
输出为
depends on the entry_type option
取决于 entry_type 选项
Options
选项
Inherited options
继承的选项
Parent type
父类型
FormType
Class
CollectionType

Note

如果你正在使用Doctrine entity的一个collection,要特别注意 allow_add, allow_deleteby_reference 选项。你可以在 如何嵌入表单(字段)集合 一文中看到完整示例。

基本用法 

当你在表单中需要管理一组相似元素时要用到这个类型。例如,假设你有一个 emails 字段,对应着一组邮箱地址。在表单中,你希望把每一个邮箱地址展现成它自己的input文本框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
// ...
 
$builder->add('emails', CollectionType::class, array(
    // each entry in the array will be an "email" field
    // 数组中的每一个入口都是一个 "email" 字段类型
    'entry_type'   => EmailType::class,
    // these options are passed to each "email" type
    // 这些选项将被传入到每一个 "email" 字段类型之中
    'entry_options'  => array(
        'attr'      => array('class' => 'email-box')
    ),
));

最简单的办法是一次输出全部:

1
{{ form_row(form.emails) }}
1
<?php echo $view['form']->row($form['emails']) ?>

一个灵活得多的方法是下面这种:

1
2
3
4
5
6
7
8
9
10
11
{{ form_label(form.emails) }}
{{ form_errors(form.emails) }}
 
<ul>
{% for emailField in form.emails %}
    <li>
        {{ form_errors(emailField) }}
        {{ form_widget(emailField) }}
    </li>
{% endfor %}
</ul>
1
2
3
4
5
6
7
8
9
10
11
<?php echo $view['form']->label($form['emails']) ?>
<?php echo $view['form']->errors($form['emails']) ?>
 
<ul>
<?php foreach ($form['emails'] as $emailField): ?>
    <li>
        <?php echo $view['form']->errors($emailField) ?>
        <?php echo $view['form']->widget($emailField) ?>
    </li>
<?php endforeach ?>
</ul>

两种情况下,每个输入框都不会输出内容,除非你的 emails data数组已经包含有一些邮箱。

在这个简单的例子中,是不可能添加或删除已有的邮箱的。使用 prototype 选项) (参考下面的例子)。从 emails 数组中删除邮箱可以通过 allow_delete 选项来完成。

添加或删除元素  

如果 allow_add 被设置为 true,那么如果有任何未被识别的元素被提交,它们将被无缝添加到数组元素中。这在理论上来说很不错,但实践中要付出更多努力来令客户端JavaScript运行正确。

继续遵循上例,假设你在一开始的 emails 数组的值中有两个邮箱。这时,两个 input 字段会被输出,看上去像下面这种 (具体取决于你的表单的name):

1
2
<input type="email" id="form_emails_0" name="form[emails][0]" value="foo@foo.com" />
<input type="email" id="form_emails_1" name="form[emails][1]" value="bar@bar.com" />

要让你的用户能够添加另一个邮箱,只需设置 allow_addtrue,并且 - 通过 JavaScript - 以 form[emails][2] 命名来输出其他字段 (以此类推至更多)。

为了让事情简单,把 prototype 选项设为 true 可以让你输出一个 "template" field(模板字段),你可以在后面的 JavaScript 中来使用,以便帮助你动态地创建这些新字段。一个已输出的 prototype field 可能是下面这样的:

1
2
3
4
5
<input type="email"
    id="form_emails___name__"
    name="form[emails][__name__]"
    value=""
/>

通过用一些唯一的值 (如 2) 来替换 __name__,你可以构建并插入新的HTML字段到表单中。

使用 jQuery 时,一个简单的示例可能是下面这样。你可以一次输出全部的集合字段 (即 form_row(form.emails)),后面的事情会因为 data-prototype 属性为你带来的自动输出 (小有不同 - 见下面) 而变得愈发简单,你要做的全部事情就是一些 JavaScript 而已:

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
43
44
45
46
47
48
49
{{ form_start(form) }}
    {# ... #}
 
    {# store the prototype on the data-prototype attribute #}
    {# 在 data-prototype 属性中存入 prototype #}
    <ul id="email-fields-list"
        data-prototype="{{ form_widget(form.emails.vars.prototype)|e }}">
    {% for emailField in form.emails %}
        <li>
            {{ form_errors(emailField) }}
            {{ form_widget(emailField) }}
        </li>
    {% endfor %}
    </ul>
 
    <a href="#" id="add-another-email">Add another email</a>
 
    {# ... #}
{{ form_end(form) }}
 
<script type="text/javascript">
    // keep track of how many email fields have been rendered
    // 跟踪已经渲染出来的email字段之数量
    var emailCount = '{{ form.emails|length }}';
 
    jQuery(document).ready(function() {
        jQuery('#add-another-email').click(function(e) {
            e.preventDefault();
 
            var emailList = jQuery('#email-fields-list');
 
            // grab the prototype template
            // 抓取 prototype 模板
            var newWidget = emailList.attr('data-prototype');
            // replace the "__name__" used in the id and name of the prototype
            // with a number that's unique to your emails
            // end name attribute looks like name="contact[emails][2]"
            // 把 prototype 中的 "__name__" 用一个相对于邮箱是“唯一”的数字来替换
            // 最终的 name 属性看上去像是 name="contact[emails][2]" 这种
            newWidget = newWidget.replace(/__name__/g, emailCount);
            emailCount++;
 
            // create a new list element and add it to the list
            // 创建一个新的 li 元素,并把它添加到列表中
            var newLi = jQuery('<li></li>').html(newWidget);
            newLi.appendTo(emailList);
        });
    })
</script>

Tip

如果你要一次输出整个collection,那么 prototype 在包裹着 collection 的元素 (如 divtable) 中的 data-prototype 属性上是自动可用的。唯一的区别是,整个 "form row"(表单行)因你而输出,意味着你不必把它封装到类似上例中的任何一个“容器元素(container element)”之中。

字段选项 

allow_add 

值类型: boolean 默认值: false

如果设为 true,则如果有未识别的元素被提交到集合的话,它们将被添加到新元素中。末级数组中将会包含这些已存在的元素以及这些一并提交过来的新元素。参考上例以了解细节。

prototype 选项可以用于辅助输出prototype元素,用于 - 和 JavaScript 一起 - 在客户端中动态地创建新元素。更多信息请参考上例,以及 使用“Prototype” 来允许“新”标签

Caution

如果你内嵌的整个表单对应的是一个 one-to-many 一对多的数据库映射,你需要手动确保这些新对象的外键被正确设置。若你使用的是Doctrine,这一切不会自动发生。参考上述链接以了解细节。(译注:有机会的话,我们会把这里讲清楚)。

allow_delete 

值类型: boolean 默认值: false

如果设为 true,则如果已经存在的元素没有被包括在已提交的数据中的话,它们将正确地在最终的数组元素中“被缺席”。这意味着通过 JavaScript 你可以实现一个“delete”删除键,从DOM中简单的删除一个表单元素。当用户提交表单时,从已提交的数据中“缺席”,代表它将在最终的数组中被删除掉。

参考 允许删除Tag 以了解更多。

Caution

当你内嵌了一组对象集合时,使用此选项要小心。本例中,如果任何内嵌的表单被删除,它们 将会 正确地从最终的对象数组中“被消失”。只是,根据你的程序逻辑,当某个对象被移除时,你可能还希望删除它或至少从主力对象中删掉它的那个外键。所有这些统统不会自动完成。更多信息,参考 允许删除Tag

delete_empty 

值类型: boolean 默认值: false

如果你希望显式地从表单中移除集合中的全部空字段,需要设置此选项。但是,已经存在的集合字段 ,只会在你开启了 allow_delete 选项的时候才会被删除。 否则,空值将会保留。

Caution

The delete_empty 选项只会删除normalized(标准化了的)值是 null 的元素。如果嵌套的 entry_type 是一个 compound 表单类型,你只能要么设置 required 选项为 false,要么设置 empty_data 选项为 null。这两个选项可以在 entry_options 中进行设置。参考 FormType 的 empty_data 选项 以了解为何这样做是必要的。

entry_options 

值类型: array 默认值: array()

这个数组要传到在 entry_type 中所指定的表单类型中。例如,如果你用的把 ChoiceType 用作 entry_type 选项 (如,用于一个下拉菜单的collection),那么你至少要把 choices 选项传入到底层类型中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
// ...
 
$builder->add('favorite_cities', CollectionType::class, array(
    'entry_type'   => ChoiceType::class,
    'entry_options'  => array(
        'choices'  => array(
            'Nashville' => 'nashville',
            'Paris'     => 'paris',
            'Berlin'    => 'berlin',
            'London'    => 'london',
        ),
    ),
));

entry_type 

值类型: string or FormTypeInterface 必填项

此即集合中的每一个元素的字段类型 (如 TextType, ChoiceType, 等)。例如,如果你有一个邮箱地址的数组,你可使用 EmailType。如果你希望内嵌一组其他类型的字段,创建一个新的form type实例,再把它传入到这个选项中。

prototype 

值类型: boolean 默认值: true

本选项在使用了 allow_add 选项时有用。如果设为 true (同时 allow_add 也被设为 true),就能够使用一个特殊的 "prototype" 属性,以便你能在页上中输出一个 "template" 模板样例,令其呈现出一个新元素的样子。提供给该元素的 name 属性是 __name__。这令你能够通过 JavaScript 去添加一个 "add another" 按钮, 它会读取 prototype,并把 __name__ 用一些唯一的名称或数字给替换掉,然后在你的表单里输出。当提交时,它会被添加到你的由 allow_add 选项所决定的底层数组中。

prototype 字段可通过 collection 字段中的 prototype 进行输出:

1
{{ form_row(form.emails.vars.prototype) }}
1
<?php echo $view['form']->row($form['emails']->vars['prototype']) ?>

注意

Tip

如果你要一次输出全部的collection集合字段,那么 prototype from row(表单行)将在包裹着 collection 的元素 (如 divtable) 中的 data-prototype 属性上自动可用。

真正使用此选项时的细节,除上例之外,亦可参考 使用“Prototype” 来允许“新”标签

prototype_data 

值类型: mixed 默认值: null

让你为 prototype 定义特定的值。每一个新行在初始化时将包含设置在此选项中的值。默认时,通过 entry_options 选项配置给所有行(all entries)的数据,将被使用。

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
 
$builder->add('tags', CollectionType::class, array(
    'entry_type' => TextType::class,
    'allow_add' => true,
    'prototype' => true,
    'prototype_data' => 'New Tag Placeholder',
));

prototype_name 

值类型: boolean 默认值: true

如果你在表单中拥有若干个collection,甚至是更糟糕的 nested collection(内嵌集合),你可能希望改变占位符,以便不相关的占位符不会被同一个值所替代。

继承的选项 

以下选项继承自 FormType 字段类型。并未列出全部选项 - 仅是最适用于 CollectionType 的部分:

by_reference 

值类型: boolean 默认值: false

多数情况下,如果你有一个 author 字段,你会预期 setAuthor() 在底层对象(underlying object)中被调用。但在某些时候,setAuthor() 可能 不会 被调用。将 by_reference 设置为 false 以确保 setter 在所有情况下皆被调用。

为说明此一功能,这里有一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
// ...
 
$builder = $this->createFormBuilder($article);
$builder
    ->add('title', TextType::class)
    ->add(
        $builder->create('author', FormType::class, array('by_reference' => ?))
            ->add('name', TextType::class)
            ->add('email', EmailType::class)
    )

如果 by_reference 是 true,当你对表单调用 submit() (或 handleRequest()) 时,在程序背后,会发生如下情形:

1
2
3
$article->setTitle('...');
$article->getAuthor()->setName('...');
$article->getAuthor()->setEmail('...');

注意 setAuthor() 未被调用。author 是根据引用而修改的(modified by reference)。

如果你设置 by_reference 为 false,提交之后会像下面这样:

1
2
3
4
5
$article->setTitle('...');
$author = clone $article->getAuthor();
$author->setName('...');
$author->setEmail('...');
$article->setAuthor($author);

所以,by_reference=false 所真正做的全部事情,就是迫使框架调用父级对象上的 setter。

类似的,若你正使用 CollectionType 字段,底层集合中的数据(collection data)是一个对象 (一如使用了Doctrine的 ArrayCollection),那么 by_reference 必须设置为 false,如果你需要 adder 和 remover 被调用的话 (即 addAuthor()removeAuthor())。

empty_data 

值类型: mixed

默认值是 array() (空数组)。

本选项决定集合字段将要 返回 什么值,若提交的值是空的 (或丢失) 的时候。当表单在view层输出时,如果什么也没提供的话,本选项不去设置一个初始值。

这可以帮你处理用表单提交空字段。例如,当没有值被选中时,如果你希望 name 字段能够被显式地设置为 John Doe,你可以这么做:

1
2
3
4
$builder->add('name', null, array(
    'required'   => false,
    'empty_data' => 'John Doe',
));

这仍将输出一个空文本框,但是随着提交,John Doe 这个值将被设置。使用 dataplaceholder 选项,在输出的表单中显示这个初始值。

如果表单是 compound 的,你可以把 empty_data 设为数组、对象或 closure。参考 如何为一个表单类配置空数据 以了解关于此选项的更多细节。

Note

如果你希望为你的整个表单类设置 empty_data 选项,参考 如何为一个表单类配置空数据 一文。

Tip

Form data transformers(表单的数据转换器)仍将适用于 empty_data 值。这意味着一个空字符串将被抛为 null。可使用一个自定义的data transformer,如果你明确地需要返回一个空字符串的话。

error_bubbling 

值类型: boolean 默认值: true

如果是 true,该字段的任何错误信息将被传到父级字段或表单中。例如,如果对一个常规字段设置为 true,该字段的任何错误将被附着到主表单上,而不是该字段本身。

error_mapping 

值类型: array 默认值: array()

本选项让你修改一条验证错误信息(validation error)的目标。

设想你有一个自定义的方法,名为 matchingCityAndZipCode(),它用于验证城市与邮编是是否匹配。不幸的是,你的表单中并没有一个 "matchingCityAndZipCode" 字段,所以Symfony能做的全部事情也就是在表单顶部显示错误而已。

有了自定义的错误信息映射,你可以做得更好:把错误映射到city字段,以便在那上面来显示错误:

1
2
3
4
5
6
7
8
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

下面是映射关系中的左侧和右侧之规则:

  • 左侧包含属性的路径;
  • 若是对类的方法和属性生成了错误信息,其路径可以是 propertyName;
  • 若是对一个arrayArrayAccess 对象的入口点生成了错误信息,则 property path 就是 [indexName];
  • 你可以构建嵌套的属性路径,通过把它们拼接起来即可,利用(.)点把属性分割开来。例如: addresses[work].matchingCityAndZipCode;
  • 右侧就是表单字段的名称。

默认时,任何没有被映射的属性,其错误信息将被顶到父级表单。你可以在左侧使用“点” (.) 来映射 city 字段的全部错误信息,使用:

1
2
3
4
5
$resolver->setDefaults(array(
    'error_mapping' => array(
        '.' => 'city',
    ),
));

label 

值类型: string 默认值: The label is "guessed" from the field name

设置用来输出字段的 label。设置为 false 将不显示 label。label 也可直接在模板中设置:

1
{{ form_label(form.name, 'Your name') }}
1
2
3
4
echo $view['form']->label(
    $form['name'],
    'Your name'
);

label_attr 

值类型: array 默认值: array()

设置 <label> 元素的 HTML 属性,用来输出字段的 label。它可以是关联数组,以HTML属性为键。该属性可直接在模板中设置:

1
2
3
{{ form_label(form.name, 'Your name', {
       'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}
}) }}
1
2
3
4
5
echo $view['form']->label(
    $form['name'],
    'Your name',
    array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
);

label_format 

值类型: string 默认值: null

配置用于字段的 label 中的字符串,如果 label 选项未被设置的话。这在使用了 关键字翻译信息 时是有用的。

如果你使用了关键字翻译源(keyword message)作为 labels,你常常会因为在同一个label上有多个关键字翻译源而被迫终止 (如 profile_address_street, invoice_address_street)。这是因为 label 就是为每一个字段“路径”(path)而设的(This is because the label is build for each "path" to a field)。要避免重复的关键字翻译源,可以把label format(标签格式)设为一个静态值,如下:

1
2
3
4
5
6
7
8
// ...
$profileFormBuilder->add('address', new AddressType(), array(
    'label_format' => 'form.address.%name%',
));
 
$invoiceFormBuilder->add('invoice', new AddressType(), array(
    'label_format' => 'form.address.%name%',
));

本选项是子类型继承过来的。使用上面的代码,两个表单的 street 字段的 label 将使用 form.address.street 键翻译源。

在label format中有两个可用的变量:

%id%
字段的一个独立识别符,由字段的完整路径和字段名称所组成 (如 profile_address_street);
%name%
字段名称 (如 street).

默认值 (null) 导致了字段名称的一个 "humanized" 版本

Note

The label_format 选项在表单主题(form theme)中被解析。如果你 自定义表单主题 的话,确对模板进行更新。

mapped 

值类型: boolean 默认值: true

如果你希望字段在“对象进行读写时”被忽略,可以设置 mapped 选项为 false

required 

值类型: boolean 默认值: true

如果是 true,一个 HTML5 required attribute 会被输出。其对应的 label 也将随一个 required class 而输出。

这是浅表限制,且独立在validation验证系统之外。最好是,如果你让 Symfony 猜测字段类型的话,则本选项的值将从你的validation infomation(验证信息)中进行猜测。

Note

这个 required 也会影响每个字段的空值(empty data)将被如何处理。参考 empty_data 选项以了解细节。

字段变量 

Variable(变量) Type(类型) Usage(作用)
allow_add boolean allow_add 选项的值。
allow_delete boolean allow_delete 选项的值。

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

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