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.