Embedded Form: CollectionType
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNow that we've added the yearsStudied
field to each GenusScientist
, I'm not too sure that checkboxes make sense anymore. I mean, if I want to show that a User
studies a Genus
, I need to select a User
, but I also need to tell the system how many years they have studied. How should this form look now?
Here's an idea, and one that works really well the form system: embed a collection of GenusScientist
subforms at the bottom, one for each user that studies this Genus
. Each subform will have a User
drop-down and a "Years Studied" text box. We'll even add the ability to add or delete subforms via JavaScript, so that we can add or delete GenusScientist
rows.
Creating the Embedded Sub-Form
Step one: we need to build a form class that represents just that little embedded GenusScientist
form. Inside your Form
directory, I'll press Command+N
- but you can also right-click and go to "New" - and select "Form". Call it GenusScientistEmbeddedForm
. Bah, remove that getName()
method - that's not needed in modern versions of Symfony:
// ... lines 1 - 2 | |
namespace AppBundle\Form; | |
// ... lines 4 - 8 | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\Form\FormBuilderInterface; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
class GenusScientistEmbeddedForm extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
// ... lines 17 - 26 | |
} | |
public function configureOptions(OptionsResolver $resolver) | |
{ | |
// ... lines 31 - 33 | |
} | |
} |
Yay!
In configureOptions()
, add $resolver->setDefaults()
with the classic data_class
set to GenusScientist::class
:
// ... lines 1 - 4 | |
use AppBundle\Entity\GenusScientist; | |
// ... lines 6 - 12 | |
class GenusScientistEmbeddedForm extends AbstractType | |
{ | |
// ... lines 15 - 28 | |
public function configureOptions(OptionsResolver $resolver) | |
{ | |
$resolver->setDefaults([ | |
'data_class' => GenusScientist::class | |
]); | |
} | |
// ... lines 35 - 36 | |
} |
We will ultimately embed this form into our main genus form... but at this point... you can't tell: this form looks exactly like any other. And it will ultimately give us a GenusScientist
object.
For the fields, we need two: user
and yearsStudied
:
// ... lines 1 - 12 | |
class GenusScientistEmbeddedForm extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
->add('user', EntityType::class, [ | |
// ... lines 19 - 23 | |
]) | |
->add('yearsStudied') | |
; | |
} | |
// ... lines 28 - 36 | |
} |
We do not need a genus
dropdown field: instead, we'll automatically set that property to whatever Genus
we're editing right now.
The user
field should be an EntityType
dropdown. In fact, let's go to GenusFormType
and steal the options from the genusScientists
field - it'll be almost identical. Set this to EntityType::class
and then paste the options:
// ... lines 1 - 5 | |
use AppBundle\Entity\User; | |
use AppBundle\Repository\UserRepository; | |
// ... lines 8 - 12 | |
class GenusScientistEmbeddedForm extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
->add('user', EntityType::class, [ | |
'class' => User::class, | |
'choice_label' => 'email', | |
'query_builder' => function(UserRepository $repo) { | |
return $repo->createIsScientistQueryBuilder(); | |
} | |
]) | |
// ... line 25 | |
; | |
} | |
// ... lines 28 - 36 | |
} |
And make sure you re-type the last r in User
and auto-complete it to get the use
statement on top. Do the same for UserRepository
. The only thing that's different is that this will be a drop-down for just one User
, so remove the multiple
and expanded
options.
Embedding Using CollectionType
This form is now perfect. Time to embed! Remember, our goal is still to modify the genusScientists
property on Genus
, so our form field will still be called genusScientists
. But clear out all of the options and set the type to CollectionType::class
. Set its entry_type
option to GenusScientistEmbeddedForm::class
:
// ... lines 1 - 11 | |
use Symfony\Component\Form\Extension\Core\Type\CollectionType; | |
// ... lines 13 - 18 | |
class GenusFormType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
// ... lines 24 - 46 | |
->add('genusScientists', CollectionType::class, [ | |
'entry_type' => GenusScientistEmbeddedForm::class | |
]) | |
; | |
} | |
// ... lines 52 - 58 | |
} |
Before we talk about this, let's see what it looks like! Refresh!
Woh! This Genus
is related to four GenusScientists... which you can see because it built an embedded form for each one! Awesome! Well, it's mostly ugly right now, but it works, and it's free!
Try updating one, like 26 to 27 and hit Save. It even saves!
Rendering the Collection... Better
But let's clean this up - because the form looks awful... even by my standards.
Open the template: app/Resources/views/admin/genus/_form.html.twig
:
{{ form_start(genusForm) }} | |
// ... lines 2 - 21 | |
{{ form_row(genusForm.genusScientists) }} | |
// ... lines 23 - 24 | |
{{ form_end(genusForm) }} |
This genusScientists
field is not and actual field anymore: it's an array of fields. In fact, each of those field is itself composed of more sub-fields. What we have is a fairly complex form tree, which is something we talked about in our Form Theming Tutorial.
To render this in a more controlled way, delete the form_row
. Then, add an h3
called "Scientists", a Bootstrap row, and then loop over the fields with for genusScientistForm in genusForm.genusScientists
:
{{ form_start(genusForm) }} | |
// ... lines 2 - 22 | |
<h3>Scientists</h3> | |
<div class="row"> | |
{% for genusScientistForm in genusForm.genusScientists %} | |
// ... lines 26 - 28 | |
{% endfor %} | |
</div> | |
// ... lines 31 - 32 | |
{{ form_end(genusForm) }} |
Yep, we're looping over each of those four embedded forms.
Add a column, and then call form_row(genusScientistForm)
to print both the user
and yearsStudied
fields at once:
{{ form_start(genusForm) }} | |
// ... lines 2 - 22 | |
<h3>Scientists</h3> | |
<div class="row"> | |
{% for genusScientistForm in genusForm.genusScientists %} | |
<div class="col-xs-4"> | |
{{ form_row(genusScientistForm) }} | |
</div> | |
{% endfor %} | |
</div> | |
// ... lines 31 - 32 | |
{{ form_end(genusForm) }} |
So this should render the same thing as before, but with a bit more styling. Refresh! Ok, it's better... but what's up with those zero, one, two, three labels?
This genusScientistForm
is actually an entire form full of several fields. So, it prints out a label for the entire form... which is zero, one, two, three, and four. That's not helpful!
Instead, print each field by hand. Start with form_errors(genusScientistForm)
, just in case there are any validation errors that are attached at this form level:
{{ form_start(genusForm) }} | |
// ... lines 2 - 22 | |
<h3>Scientists</h3> | |
<div class="row"> | |
{% for genusScientistForm in genusForm.genusScientists %} | |
<div class="col-xs-4"> | |
{{ form_errors(genusScientistForm) }} | |
// ... lines 28 - 29 | |
</div> | |
{% endfor %} | |
</div> | |
// ... lines 33 - 34 | |
{{ form_end(genusForm) }} |
It's not common, but possible. Then, simply print form_row(genusScientistForm.user)
and form_row(genusScientistForm.yearsStudied)
:
{{ form_start(genusForm) }} | |
// ... lines 2 - 22 | |
<h3>Scientists</h3> | |
<div class="row"> | |
{% for genusScientistForm in genusForm.genusScientists %} | |
<div class="col-xs-4"> | |
{{ form_errors(genusScientistForm) }} | |
{{ form_row(genusScientistForm.user) }} | |
{{ form_row(genusScientistForm.yearsStudied) }} | |
</div> | |
{% endfor %} | |
</div> | |
// ... lines 33 - 34 | |
{{ form_end(genusForm) }} |
Try it! Much better!
But you know what we can't do yet? We can't actually remove - or add - new scientists. all we can do is edit the existing ones. That's silly! So let's fix it!
Even if this video is fairly old now, I have no idea where else to post my question:
(I'm using Symfony 6)
My use case
TLDR;
I have two entities with a ManytoMany relation. I want to persist two new objects at the same time with one single form. To do so, I created two FromTypes with one embedding the other.
A bit more...
The goal is to provide users with a form to make an inquiry for an event. The Event entity consists of properties like starttime, endtime e.g. that are simple properties of Event aswell as a location (Location entity with a OneToMany relation, one Event has one Location, one Location can have many Events) and a contactperson (Contact entity with a ManyToMany relation, one Event can have multiple Contacts, one Contact can have multiple Events). For the particular form in question it is enough (and a deliberate choice) for the user to provide only one Contact as that is the bare minimum needed and enough for a start.
To build reusable forms, there are two simple forms with LocationFormType and ContactFormType and a more complex EventFormType. More complex as it embedds both LocationFormType and ContactFormType to create an Event entity "in one go" so to speak. When I build the EventFormType with option A (see code below), the form renders correct and the way it is intended. Everything looks fine until the form is submitted. Then the problem starts...
Problem
On $form->handleRequest() the FormSystem throws an error because the embedded form is not providing a Traversable for the related object. Obviously the embedded FormType is providing a single object, while the property for the relation needs a Collection. When I use CollectionType for embedding, the form is not rendering anymore as CollectionType seemingly expects entities to be present already. But I want to create a new one. So there is no object I could pass.
My Code
Failed solutions
'allow_add' => true with prototype
I found solutions suggesting to set 'allow_add' => true on the CollectionType and render the form in Twig with <form>.<related object>.vars.prototype
Thats a hacky solution (so I think) in my use case. I don't want to add multiple forms. And without 'allow_add' there is no prototype in CollectionType, so the data to render the form is missing.
provide empty object to CollectionType
To omit 'allow_add' => true but have an object to render the form correctly, I tried passing an empty instance of Contact in my controller
That works on initial load, but creates issues when the form is submitted. Maybe I could make it work, but my gut gives me 'hacky vibes' once again.
Actually I think I'm missing some basic point here as I think my use case is nothing edgy or in any way unusual. Can anyone give me a hint as where I'm going wrong with my approach?
P.S.: I'm unsure wether my issue was discussed (without a solution) over on Github.
Edit: I posted the issue over on Stackoverflow. The post and discussion there contain additional info.