Buy Access to Course

Compound & Embedded Forms

Share this awesome video!


Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Right now, this is a pretty simple form. We have our top level form, and then each field below it is its own Form object. And we now know that when you pass this into the template, all of those Form objects become FormView objects.

But this will still just be 2 levels: the FormView object on top, and the children FormView object for each field. But, it can get a lot more complicated than that.

To show you, go into GenusFormType. For now, change the firstDiscoveredAt options: comment out widget and attr:

54 lines | src/AppBundle/Form/GenusFormType.php
// ... lines 1 - 15
class GenusFormType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
// ... lines 21 - 38
->add('firstDiscoveredAt', DateType::class, [
//'widget' => 'single_text',
//'attr' => ['class' => 'js-datepicker'],
'html5' => false,
// ... lines 46 - 52

Refresh this immediately. Ok, the widget option defaults to choice, which means that this renders as three select fields. I know, it's horribly ugly, hard to look at... but it's a perfect example! Click into the profiler for this form to see something really interesting. The firstDiscoveredAt has a "+" next to it... and three fields below it!

Compound Fields!

You see, firstDiscoveredAt is no longer a "simple" field: it's now a field that consists of 3 sub-fields: year, month, and day. Each of these is their own ChoiceType field. Oh, and if you select firstDiscoveredAt, under "View Variables", for the first time, the compound variable is set to true.

We saw this compound variable in a few places earlier. And now we know what it means! A field is compound if it's not really its own field, but is instead just a container for sub-fields.

In the _form.html.twig template, when we call form_row() on genusForm.firstDiscoveredAt, Symfony tries to render the parent field, notices that it's compound and so, calls form_row() on each of its three sub-fields:

{{ form_start(genusForm) }}
// ... lines 2 - 19
{{ form_row(genusForm.firstDiscoveredAt) }}
// ... lines 21 - 22
{{ form_end(genusForm) }}

The result is the nice output we're already seeing.

Rendering Sub-Fields

To get more control, you could instead call form_row on each individual field: for year, month and day:

{{ form_start(genusForm) }}
// ... lines 2 - 20
{{ form_row(genusForm.firstDiscoveredAt.year) }}
{{ form_row(genusForm.firstDiscoveredAt.month) }}
{{ form_row( }}
// ... lines 24 - 25
{{ form_end(genusForm) }}

But notice that if this field fails validation, the error is attached to the parent field. So you might want to keep rendering form_label(genusForm.firstDiscoveredAt) and you definitely want to keep rendering form_errors(genusForm.firstDiscoveredAt), so that the error shows up.

If you go back and refresh, you basically see the same thing as before. It's ugly, but you just learned how to take control of any level of a complex form tree.