Compound & Embedded Forms

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

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:

... lines 1 - 15
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... 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(genusForm.firstDiscoveredAt.day) }}
... 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.

Leave a comment!

This tutorial is built on Symfony 3 but form theming hasn't changed much in Symfony 4 and Symfony 5. Other than some path differences - this tutorial should work fine.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}