CollectionType Field
Warning: You are browsing the documentation for Symfony 3.x, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
This field type is used to render a "collection" of some field or form.
In the easiest sense, it could be an array of TextType
fields that populate
an array emails
values. In more complex examples, you can embed entire
forms, which is useful when creating forms that expose one-to-many
relationships (e.g. a product from where you can manage many related product
photos).
Rendered as | depends on the entry_type option |
Options | |
Inherited options | |
Parent type | FormType |
Class | CollectionType |
Note
If you are working with a collection of Doctrine entities, pay special attention to the allow_add, allow_delete and by_reference options. You can also see a complete example in the How to Embed a Collection of Forms article.
Basic Usage
This type is used when you want to manage a collection of similar items
in a form. For example, suppose you have an emails
field that corresponds
to an array of email addresses. In the form, you want to expose each email
address as its own input text box:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
// ...
$builder->add('emails', CollectionType::class, [
// each entry in the array will be an "email" field
'entry_type' => EmailType::class,
// these options are passed to each "email" type
'entry_options' => [
'attr' => ['class' => 'email-box'],
],
]);
The simplest way to render this is all at once:
1
{{ form_row(form.emails) }}
A much more flexible method would look like this:
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>
In both cases, no input fields would render unless your emails
data
array already contained some emails.
In this simple example, it's still impossible to add new addresses or remove
existing addresses. Adding new addresses is possible by using the allow_add
option (and optionally the prototype option) (see example below). Removing
emails from the emails
array is possible with the allow_delete option.
Adding and Removing Items
If allow_add is set to true
, then if any unrecognized items are submitted,
they'll be added seamlessly to the array of items. This is great in theory,
but takes a little bit more effort in practice to get the client-side JavaScript
correct.
Following along with the previous example, suppose you start with two
emails in the emails
data array. In that case, two input fields will
be rendered that will look something like this (depending on the name of
your form):
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"/>
To allow your user to add another email, just set allow_add to true
and - via JavaScript - render another field with the name form[emails][2]
(and so on for more and more fields).
To help make this easier, setting the prototype option to true
allows
you to render a "template" field, which you can then use in your JavaScript
to help you dynamically create these new fields. A rendered prototype field
will look like this:
1 2 3 4 5
<input type="email"
id="form_emails___name__"
name="form[emails][__name__]"
value=""
/>
By replacing __name__
with some unique value (e.g. 2
),
you can build and insert new HTML fields into your form.
Using jQuery, a simple example might look like this. If you're rendering
your collection fields all at once (e.g. form_row(form.emails)
), then
things are even easier because the data-prototype
attribute is rendered
automatically for you (with a slight difference - see note below) and all
you need is this JavaScript code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// add-collection-widget.js
jQuery(document).ready(function () {
jQuery('.add-another-collection-widget').click(function (e) {
var list = jQuery(jQuery(this).attr('data-list-selector'));
// Try to find the counter of the list or use the length of the list
var counter = list.data('widget-counter') || list.children().length;
// grab the prototype template
var newWidget = list.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]"
newWidget = newWidget.replace(/__name__/g, counter);
// Increase the counter
counter++;
// And store it, the length cannot be used if deleting widgets is allowed
list.data('widget-counter', counter);
// create a new list element and add it to the list
var newElem = jQuery(list.attr('data-widget-tags')).html(newWidget);
newElem.appendTo(list);
});
});
And update the template as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
{{ form_start(form) }}
{# ... #}
{# store the prototype on the data-prototype attribute #}
<ul id="email-fields-list"
data-prototype="{{ form_widget(form.emails.vars.prototype)|e }}"
data-widget-tags="{{ '<li></li>'|e }}"
data-widget-counter="{{ form.emails|length }}">
{% for emailField in form.emails %}
<li>
{{ form_errors(emailField) }}
{{ form_widget(emailField) }}
</li>
{% endfor %}
</ul>
<button type="button"
class="add-another-collection-widget"
data-list-selector="#email-fields-list">Add another email</button>
{# ... #}
{{ form_end(form) }}
<script src="add-collection-widget.js"></script>
Tip
If you're rendering the entire collection at once, then the prototype
is automatically available on the data-prototype
attribute of the
element (e.g. div
or table
) that surrounds your collection.
The only difference is that the entire "form row" is rendered for you,
meaning you wouldn't have to wrap it in any container element as it
was done above.
Field Options
allow_add
type: boolean
default: false
If set to true
, then if unrecognized items are submitted to the collection,
they will be added as new items. The ending array will contain the existing
items as well as the new item that was in the submitted data. See the above
example for more details.
The prototype option can be used to help render a prototype item that can be used - with JavaScript - to create new form items dynamically on the client side. For more information, see the above example and How to Embed a Collection of Forms.
Caution
If you're embedding entire other forms to reflect a one-to-many database relationship, you may need to manually ensure that the foreign key of these new objects is set correctly. If you're using Doctrine, this won't happen automatically. See the above link for more details.
allow_delete
type: boolean
default: false
If set to true
, then if an existing item is not contained in the submitted
data, it will be correctly absent from the final array of items. This means
that you can implement a "delete" button via JavaScript which removes a form
element from the DOM. When the user submits the form, its absence from the
submitted data will mean that it's removed from the final array.
For more information, see How to Embed a Collection of Forms.
Caution
Be careful when using this option when you're embedding a collection of objects. In this case, if any embedded forms are removed, they will correctly be missing from the final array of objects. However, depending on your application logic, when one of those objects is removed, you may want to delete it or at least remove its foreign key reference to the main object. None of this is handled automatically. For more information, see How to Embed a Collection of Forms.
delete_empty
type: Boolean
or callable
default: false
If you want to explicitly remove entirely empty collection entries from your
form you have to set this option to true
. However, existing collection entries
will only be deleted if you have the allow_delete option enabled. Otherwise
the empty values will be kept.
Caution
The delete_empty
option only removes items when the normalized value is
null
. If the nested entry_type is a compound form type, you must
either set the required
option to false
or set the empty_data
option to null
. Both of these options can be set inside entry_options.
Read about the form's empty_data option
to learn why this is necessary.
A value is deleted from the collection only if the normalized value is null
.
However, you can also set the option value to a callable, which will be executed
for each value in the submitted collection. If the callable returns true
,
the value is removed from the collection. For example:
1 2 3 4 5 6 7 8 9
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
// ...
$builder->add('users', CollectionType::class, [
// ...
'delete_empty' => function (User $user = null) {
return null === $user || empty($user->getFirstName());
},
]);
Using a callable is particularly useful in case of compound form types, which may define complex conditions for considering them empty.
3.4
Support for using a callable for the delete_empty
option was introduced
in Symfony 3.4.
entry_options
type: array
default: []
This is the array that's passed to the form type specified in the entry_type
option. For example, if you used the ChoiceType
as your entry_type option (e.g. for a collection of drop-down menus),
then you'd need to at least pass the choices
option to the underlying
type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
// ...
$builder->add('favoriteCities', CollectionType::class, [
'entry_type' => ChoiceType::class,
'entry_options' => [
'choices' => [
'Nashville' => 'nashville',
'Paris' => 'paris',
'Berlin' => 'berlin',
'London' => 'london',
],
],
]);
entry_type
type: string
default: 'Symfony
This is the field type for each item in this collection (e.g. TextType
,
ChoiceType
, etc). For example, if you have an array of email addresses,
you'd use the EmailType. If you want
to embed a collection of some other form, pass the form type class as this
option (e.g. MyFormType::class
).
prototype
type: boolean
default: true
This option is useful when using the allow_add option. If true
(and
if allow_add is also true
), a special "prototype" attribute will
be available so that you can render a "template" example on your page of
what a new element should look like. The name
attribute given to this
element is __name__
. This allows you to add a "add another" button via
JavaScript which reads the prototype, replaces __name__
with some unique
name or number and render it inside your form. When submitted, it will
be added to your underlying array due to the allow_add option.
The prototype field can be rendered via the prototype
variable in the
collection field:
1
{{ form_row(form.emails.vars.prototype) }}
Note that all you really need is the "widget", but depending on how you're rendering your form, having the entire "form row" may be easier for you.
Tip
If you're rendering the entire collection field at once, then the prototype
form row is automatically available on the data-prototype
attribute
of the element (e.g. div
or table
) that surrounds your collection.
For details on how to actually use this option, see the above example as well as How to Embed a Collection of Forms.
prototype_data
type: mixed
default: null
Allows you to define specific data for the prototype. Each new row added will initially contain the data set by this option. By default, the data configured for all entries with the entry_options option will be used:
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, [
'entry_type' => TextType::class,
'allow_add' => true,
'prototype' => true,
'prototype_data' => 'New Tag Placeholder',
]);
prototype_name
type: string
default: __name__
If you have several collections in your form, or worse, nested collections you may want to change the placeholder so that unrelated placeholders are not replaced with the same value.
Inherited Options
These options inherit from the FormType. Not all options are listed here - only the most applicable to this type:
by_reference
type: boolean
default: true
In most cases, if you have an author
field, then you expect setAuthor()
to be called on the underlying object. In some cases, however, setAuthor()
may not be called. Setting by_reference
to false
ensures that the setter is
called in all cases.
To explain this further, here's a simple example:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
$builder = $this->createFormBuilder($article);
$builder
->add('title', TextType::class)
->add(
$builder->create('author', FormType::class, ['by_reference' => ?])
->add('name', TextType::class)
->add('email', EmailType::class)
)
If by_reference
is true, the following takes place behind the scenes
when you call submit()
(or handleRequest()
) on the form:
1 2 3
$article->setTitle('...');
$article->getAuthor()->setName('...');
$article->getAuthor()->setEmail('...');
Notice that setAuthor()
is not called. The author is modified by reference.
If you set by_reference
to false, submitting looks like this:
1 2 3 4 5
$article->setTitle('...');
$author = clone $article->getAuthor();
$author->setName('...');
$author->setEmail('...');
$article->setAuthor($author);
So, all that by_reference=false
really does is that it clones the object,
which enforces the framework to call the setter on the parent object.
Similarly, if you're using the CollectionType
field where your underlying collection data is an object (like with
Doctrine's ArrayCollection
), then by_reference
must be set to false
if you need the adder and remover (e.g. addAuthor()
and removeAuthor()
)
to be called.
empty_data
type: mixed
DEFAULT_PLACEHOLDER
This option determines what value the field will return when the submitted value is empty (or missing). It does not set an initial value if none is provided when the form is rendered in a view.
This means it helps you handling form submission with blank fields. For
example, if you want the name
field to be explicitly set to John Doe
when no value is selected, you can do it like this:
1 2 3 4
$builder->add('name', null, [
'required' => false,
'empty_data' => 'John Doe',
]);
This will still render an empty text box, but upon submission the John Doe
value will be set. Use the data
or placeholder
options to show this
initial value in the rendered form.
If a form is compound, you can set empty_data
as an array, object or
closure. See the How to Configure empty Data for a Form Class article for more details about
these options.
Note
If you want to set the empty_data
option for your entire form class,
see the How to Configure empty Data for a Form Class article.
Caution
Form data transformers will still be
applied to the empty_data
value. This means that an empty string will
be cast to null
. Use a custom data transformer if you explicitly want
to return the empty string.
:end-before: DEFAULT_PLACEHOLDER
The default value is []
(empty array).
empty_data
type: mixed
DEFAULT_PLACEHOLDER
This option determines what value the field will return when the submitted value is empty (or missing). It does not set an initial value if none is provided when the form is rendered in a view.
This means it helps you handling form submission with blank fields. For
example, if you want the name
field to be explicitly set to John Doe
when no value is selected, you can do it like this:
1 2 3 4
$builder->add('name', null, [
'required' => false,
'empty_data' => 'John Doe',
]);
This will still render an empty text box, but upon submission the John Doe
value will be set. Use the data
or placeholder
options to show this
initial value in the rendered form.
If a form is compound, you can set empty_data
as an array, object or
closure. See the How to Configure empty Data for a Form Class article for more details about
these options.
Note
If you want to set the empty_data
option for your entire form class,
see the How to Configure empty Data for a Form Class article.
Caution
Form data transformers will still be
applied to the empty_data
value. This means that an empty string will
be cast to null
. Use a custom data transformer if you explicitly want
to return the empty string.
:start-after: DEFAULT_PLACEHOLDER
error_bubbling
type: boolean
default: true
If true
, any errors for this field will be passed to the parent field
or form. For example, if set to true
on a normal field, any errors for
that field will be attached to the main form, not to the specific field.
error_mapping
type: array
default: []
This option allows you to modify the target of a validation error.
Imagine you have a custom method named matchingCityAndZipCode()
that validates
whether the city and zip code match. Unfortunately, there is no matchingCityAndZipCode
field in your form, so all that Symfony can do is display the error on top
of the form.
With customized error mapping, you can do better: map the error to the city field so that it displays above it:
1 2 3 4 5 6 7 8
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'error_mapping' => [
'matchingCityAndZipCode' => 'city',
],
]);
}
Here are the rules for the left and the right side of the mapping:
- The left side contains property paths;
- If the violation is generated on a property or method of a class, its
path is the
propertyName
; - If the violation is generated on an entry of an
array
orArrayAccess
object, the property path is[indexName]
; - You can construct nested property paths by concatenating them, separating
properties by dots. For example:
addresses[work].matchingCityAndZipCode
; - The right side contains the names of fields in the form.
By default, errors for any property that is not mapped will bubble up to the
parent form. You can use the dot (.
) on the left side to map errors of all
unmapped properties to a particular field. For instance, to map all these
errors to the city
field, use:
1 2 3 4 5
$resolver->setDefaults([
'error_mapping' => [
'.' => 'city',
],
]);
label
type: string
default: The label is "guessed" from the field name
Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:
1
{{ form_label(form.name, 'Your name') }}
1 2 3 4
echo $view['form']->label(
$form['name'],
'Your name'
);
label_attr
type: array
default: []
Sets the HTML attributes for the <label>
element, which will be used
when rendering the label for the field. It's an associative array with HTML
attribute as a key. This attributes can also be directly set inside the
template:
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',
['label_attr' => ['class' => 'CUSTOM_LABEL_CLASS']]
);
label_format
type: string
default: null
Configures the string used as the label of the field, in case the label
option was not set. This is useful when using
keyword translation messages.
If you're using keyword translation messages as labels, you often end up having
multiple keyword messages for the same label (e.g. profile_address_street
,
invoice_address_street
). This is because the label is built for each "path"
to a field. To avoid duplicated keyword messages, you can configure the label
format to a static value, like:
1 2 3 4 5 6 7 8
// ...
$profileFormBuilder->add('address', AddressType::class, [
'label_format' => 'form.address.%name%',
]);
$invoiceFormBuilder->add('invoice', AddressType::class, [
'label_format' => 'form.address.%name%',
]);
This option is inherited by the child types. With the code above, the label of
the street
field of both forms will use the form.address.street
keyword
message.
Two variables are available in the label format:
%id%
-
A unique identifier for the field, consisting of the complete path to the
field and the field name (e.g.
profile_address_street
); %name%
-
The field name (e.g.
street
).
The default value (null
) results in a
"humanized" version of the field name.
Note
The label_format
option is evaluated in the form theme. Make sure to
update your templates in case you
customized form theming.
mapped
type: boolean
default: true
If you wish the field to be ignored when reading or writing to the object,
you can set the mapped
option to false
.
required
type: boolean
default: true
If true, an HTML5 required attribute will be rendered. The corresponding
label
will also render with a required
class.
This is superficial and independent of validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.
Note
The required option also affects how empty data for each field is handled. For more details, see the empty_data option.
Field Variables
Variable | Type | Usage |
---|---|---|
allow_add |
boolean |
The value of the allow_add option. |
allow_delete |
boolean |
The value of the allow_delete option. |