04/14/2015
Dynamic field names in Twig with attribute() and ~
Twig's dot notation needs the property name written out. When the name is
built at runtime — field_name_0, field_name_1, and so on up to
whatever the form happened to produce — there is nothing to write, and
form.field_name_ ~ i does not parse as a lookup.
attribute() takes the name as a string, so the tilde can build it:
{% for i in 0..4 %}
{% if attribute(form, 'field_name_' ~ i) is defined %}
{% for field in attribute(form, 'field_name_' ~ i) %}
{{ form_row(field) }}
{% endfor %}
{% endif %}
{% endfor %}
Two pieces are doing work here.
The tilde is concatenation, not addition. Twig has no + for
strings; 'field_name_' ~ i gives field_name_0, whereas
'field_name_' + i tries to add and gives you a number or an error.
is defined is what keeps this from throwing. A dynamically built
form rarely has an unbroken run of fields — somebody removed row 2 in the
browser before submitting — and asking for a field that is not there is a
runtime error, not an empty string. The guard turns a gap into a skipped
iteration.
When the name is not a form field
attribute() is not form-specific. It resolves a property, a method or an
array key on any variable, which makes it the tool for anything keyed by
data rather than by name:
{% for locale in ['en', 'fr', 'de'] %}
{{ attribute(product, 'name_' ~ locale)|default('-') }}
{% endfor %}
The better fix, where you control the form
If the form is yours, this whole problem is avoidable. A Symfony
CollectionType gives you field_name[0], field_name[1] as a real
collection, which iterates directly:
{% for field in form.field_name %}
{{ form_row(field) }}
{% endfor %}
That is the version to reach for on new work. attribute() is for the
form you inherited, where the names were serialized into strings by
something you are not going to rewrite today.