Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

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

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

Login Subscribe

Now 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!

Leave a comment!

65
Login or Register to join the conversation
Eric Avatar
Eric Avatar Eric | posted 7 months ago | edited

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

#[ORM\Entity(repositoryClass: EventRepository::class)]
class Event
{
    ...

    #[ORM\ManyToMany(targetEntity: Contact::class, inversedBy: 'events')]
    ...
    private Collection $contact;
    
    ...

    public function __construct()
    {
        $this->contact = new ArrayCollection();
    }
    
    ...

    /**
     * @return Collection<int, Contact>
     */
    public function getContact(): Collection
    {
        return $this->contact;
    }

    public function addContact(Contact $contact): self
    {
        if (!$this->contact->contains($contact)) {
            $this->contact->add($contact);
        }

        return $this;
    }

    public function removeContact(Contact $contact): self
    {
        $this->contact->removeElement($contact);

        return $this;
    }
    
    ...
}
#[ORM\Entity(repositoryClass: ContactRepository::class)]
class Contact
{
    ...

    #[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'contact')]
    private Collection $events;

    public function __construct()
    {
        $this->events = new ArrayCollection();
    }
    
    ...

    /**
     * @return Collection<int, Event>
     */
    public function getEvents(): Collection
    {
        return $this->events;
    }

    public function addEvent(Event $event): self
    {
        if (!$this->events->contains($event)) {
            $this->events->add($event);
            $event->addContact($this);
        }

        return $this;
    }

    public function removeEvent(Event $event): self
    {
        if ($this->events->removeElement($event)) {
            $event->removeContact($this);
        }

        return $this;
    }
}
class EventFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ...
            // option A: embedding related FormType directly
            ->add('contact', ContactFormType::class, [
                ...
            ])
            // option B: embedding with CollectionType
            ->add('contact', CollectionType::class, [
                'entry_type' => ContactFormType::class
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Event::class,
        ]);
    }
}
class ContactFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add(
            ... // here I only add the fields for Contact entity, no special config
            )
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Contact::class,
        ]);
    }
}

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

$eventForm = $this->createForm(EventFormType::class);
if(!$eventForm->get('contact')) $eventForm->get('contact')->setData(array(new Contact()));

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.

Reply
Eric Avatar

Okay, so I solved the problem. For this scenario one has to make use of Data Mappers.

It is possible to map single form fields by using the 'getter' and 'setter' option keys (Docs). In this particular case the setter-option is enough:

    ->add('contact', ContactFormType::class, [
        ...
        'setter' => function (Event &$event, Contact $contact, FormInterface $form) {
            $event->addContact($contact);
        }
    ])

The addContact()-method is provided by Symfonys CLI when creating ManyToMany relations, but can be added manually aswell (Docs, SymfonyCast).

Reply

Hello,

Looks like you did a great investigation work. I'm very sorry that we missed your initial message, probably it was an issue with notification system, anyway it work good now!

Thank you very much for sharing solution, it may help someone!

Cheers!

1 Reply

Hello!

Hope everything is going well on your side.

I need some help on usage of collectionType.
I have got the following entities:

  • module : id / name / description)
  • training : id / name / modules (relation oneToMany)
  • event: id / module / date
  • cycle : id / training (relation ManytoOne) / events (relation oneToMany)

A cycle is composed of events for a specific training.
So when I create a new cycle, I need to select the training. When I know the training, I can get the list of modules related to it and "pre-filled" the events belonging to the new cycle. This is done using the event listener in CycleType form and javascript.

The function called after selecting the training:


		public function getModuleList(Request $request, TrainingRepository $training_repository) {

			$training = $training_repository->findOneBy( [
				'id'        => $request->query->get( 'training' ),
				'isDeleted' => 0,
			] );

			$cycle = new Cycle();
			$cycle->setTraining($training);
			foreach ($training->getModules() as $module) {
				$event = new Event();
				$event->setModule($module);
				$cycle->addEvent($event);
			}

			if (null === $training)
				return new JsonResponse(null, 204);

			$form = $this->createForm(CycleType::class, $cycle);

			$modulesList = $this->renderView('agenda/admin/_events_list.html.twig', [
				'form' => $form->createView()
			]);

			return new JsonResponse([
				'modules' => $modulesList
			], 200);
		}

In cylceType - the eventListener:


			$builder->addEventListener(
				FormEvents::PRE_SET_DATA,
				function(FormEvent $event)  {
					/** @var Cycle $cyle */
					$cycle = $event->getData();

					if (!empty($cycle->getEvents()))
						$event->getForm()->add('events', CollectionType::class, [
								'entry_type' => EventType::class,
								'entry_options' => [
									'label' => false,
								]
							]
						);
				}
			);

The EventType:


                      $builder
				->add('module', EntityType::class, [
					'class' => Module::class,
					'choice_label' => 'title',
					'query_builder' => function (EntityRepository $er) {
						return $er->createQueryBuilder('m')
						          ->orderBy('m.position', 'ASC');
					},
				])
				->add('date', DateType::class, [
					'label' => $this->translator->trans( 'form.field.date', [], 'global' ),
					'html5' => false,
					'widget' => 'single_text',
					'format' => 'dd/MM/yyyy',
				]);

When I create a new cycle, I select the training and let's say that for this training, I have 3 modules.
When displaying the events linked to the new cycle on my form, I only got 3 events (that's good!) but for each event, for the Module drop-down, I got the complete list of existing Modules, not only the ones linked to my 3 events.

I guess I would need to use the training id in the query_builder for 'module' in event type but I do not how to access this information. That's y first question :)

The second one is : I am displaying the events in a table manner. For each line the module is a fixed value. So instead of displaying a select for it, I would prefer to display a fix value (the module title). Is this possible?

Hope this is not too confused.

Take care of you!

Reply

Hi be_tnt !

Sorry for my VERY slow reply. Your question is hard... so it waited for me :).

Yikes! This is complex! I honestly wish that the form component made this sort of this easier - it's just tough. Let's see what we can do!

I guess I would need to use the training id in the query_builder for 'module' in event type but I do not how to access this information. That's y first question :)

Yes, for sure. But, doing this is the harder part :). Hmm. If EventType is bound to the Event entity, then you should be able to see the same PRE_SET_DATA trick to access that. I'm not totally clear on the full situation, so my apologies if some of these details aren't right. But basically, I believe that if you create a PRE_SET_DATA listener in EventType (like you've done in CycleType), you should be able to access the Event object (via $event->getData()) inside that listener. Then you would call $event->getCycle()->getTraining() to get the related Training object (you didn't mention a cycle field on Event, but I think there should be one - that's the ManyToOne side of the relation. You could then use this in the query_builder option when adding the module field to filter it.

I could be over-looking a detail - these things get complex, but let me know. OR, if I'm wrong (because it's possible that maybe the Module isn't set on the Event at this moment - especially for new Events, then another option would be to:

A) Add a custom option on EventType called "training".
B) When adding the "events" field in the event listener in CycleType, you can pass in the custom option:


if (!empty($cycle->getEvents())) {
    $event->getForm()->add('events', CollectionType::class, [
            'entry_type' => EventType::class,
            'entry_options' => [
                'label' => false,
            ],
            'entry_options' => [
                'training' => $cycle->getTraining()
            ]
        ]
    );
}

C) Then, in buildForm() in EventType, the $options argument would have a "training" key that you could use for the field.

The more I think about it, this second solution seems simpler anyways :P.

The second one is : I am displaying the events in a table manner. For each line the module is a fixed value. So instead of displaying a select for it, I would prefer to display a fix value (the module title). Is this possible?

My guess is that you still want to show the module "select" for a new event, but not for existing events, is that right? Let me know - I might be missing something :). But anyways, when you are rendering your embedded event form, you should be able to access the underlying Event object (and so, then also the related module) by using "vars". So, doing this is 2 steps:

1) Don't add a module field to EventType. If you just want to print a value, then you don't need it as af iedl


    {% for eventForm in cycleForm.events %}
        <tr>
            <td>
                {# just in case you have errors on this level #}
                {{ form_errors(eventForm) }}
                {{ form_row(eventForm.date) }}
            </td>
            <td>
                {{ eventForm.vars.data.module.title }}
            </td>
        </tr>
    {% endfor %}

Phew! Let me know if any of this helps!

Cheers!

Reply

Hello weaverryan ,

Thx for your response. It helps a lot. I applied the first one (using PRE_SET_DATA). But I got error when submitting the form.
I got the error: "This form can not have extra fields". Checking the profiler, I can see the events field is empty and the error is related to:


Symfony\Component\Validator\ConstraintViolation {#1731 â–¼
  root: Symfony\Component\Form\Form {#1076 …}
  path: "children[events]"
  value: [â–¼
    [â–¼
      "date" => "23/05/2020"
      "time" => [â–¼
        "hour" => "12"
        "minute" => "0"
      ]
      "duration" => "12"
    ]
  ]
}

In CycleType eventlistener, I have added the by_reference attribut (set to false).

Below the POST_SUBMIT event listener code:


$builder->get('training')->addEventListener(
				FormEvents::POST_SUBMIT,
				function (FormEvent $form_event) {
					/** @var Training $training */
					$training = $form_event->getForm()->getData();

					if (!empty($training->getModules())) {
						$form_event->getForm()->getParent()->add( 'events', CollectionType::class, [
								'entry_type'    => EventType::class,
								'entry_options' => [
									'label' => false,
								],
								'by_reference'  => false // force to call the add and remove methods from attribute
							]
						);
					}
				}
			);```
 

Any advice would be welcome :)

Have a nice day!
Reply

Hey be_tnt!

But I got error when submitting the form. I got the error: "This form can not have extra fields"

Hmm. This means that, in CycleType, at the moment that your form submits, Symfony thinks that there is NO "events" field at ALL. So it's not that the "events" are set to the wrong "collection" (or something similar), the field "events" field completely does not exist on your form. It makes me wonder if, for some reason, the if statement around your $event->getForm()->add('events', ...) code is preventing the field from being added. I don't know if it will make a difference, but I would probably always add this field.

Unfortunately, I think this is complex enough that I would need to see the full code setup and be able to play with it to see what's really going on. Any chance you can extract this code into a small Symfony app and push it to GitHub? If you could, I'd be happy to take a deeper look :).

Cheers!

Reply

Hello!

I am using symfony version 4. When my collectionType element is empty, I just displayed the message "There is no element". But because I do not "consume" the collectionType element, I got an extra block below my submit button (the one from the collectionType element).

So in my root form type, I do have:

->add('shippingDestinationsElements', CollectionType::class, [
'entry_type' => DestinationsElementsType::class,
'entry_options' => [
'label' => false
],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
])

In the twig template:


......
......
<div class="elements-list">
<div class="row destinations-elements-wrapper no-gutters" data-prototype="{{ form_widget(form.shippingDestinationsElements.vars.prototype)|e('html_attr') }}" data-index="{{ form.shippingDestinationsElements|length }}">
<div class="col">


{% trans %}destinations.addElement{% endtrans %}


{% if form.shippingDestinationsElements is not empty %}
<div class="row">
{% for elt in form.shippingDestinationsElements %}
<div class="col-2 js-destinations-element">
{{ form_row(elt.element, {
attr: {
readonly: true,
},
}) }}

</div>
{% endfor %}
</div>
{% else %}

{% trans %}destinations.noPostalCode{% endtrans %}


{% endif %}
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">{{ button_label|trans }}</button>
{{ form_end(form) }}

The extra block, I got when no collection type is empty:

<fieldset class="form-group">
<legend class="col-form-label required">Shipping destinations elements</legend>
<div id="destinations_shippingDestinationsElements"></div>
</fieldset>

Any idea?

Thx!

Reply

Hey be_tnt

That's because of {{ form_end(form) }} it will render all the missing fields automatically. What you can do is to manually set that field as rendered. Here is a code example of how to do it: https://symfonycasts.com/screencast/symfony-form-theming/rendering-fields-manually#codeblock-8c486d12dc

Hope this helps! Cheers!

Reply

It works perfectly. Thx!

1 Reply
Christopher Avatar
Christopher Avatar Christopher | posted 4 years ago

Is it possible to keep the checkboxes and have a years studied input field next to each Genus Scientist? Not that this would be a great design, but for education's sake, could you keep the checkboxes so you could add multiple scientists and also enter the years studied all on the same form with one submit?

Reply

Hey Cmarcus,

Yes, I think it's possible, you can create a custom form type with all the fields you need for each entity and use it for CollectionType's entry_type option like:


$builder->add('emails', CollectionType::class, [
    // each entry in the array will be an "email" field
    'entry_type' => YourCustomFormType::class,
]);

I hope this helps!

Cheers!

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | posted 4 years ago

Hi, I wonder if it is possible to filter the entities which are included in the CollectionType. In my project, I have a Team entity with a OneToMany relation on a Player entity. However, players might vary for each season: season is a property of the Player entity. Now, I would like to create a form to edit the team including the players of the current season.

The only solution I've found, is to create custom get(), add() and remove() methods on the Team class, to be abled to filter out players for other seasons. But since it is not possible to use a parameter in the form definition, this would result in many methods: getPlayers2019(), getPlayers2018(), etc. Not an option, of course.

While I can't find a good practice on this subject online, I also can't find any resource telling it is not possible. It seems to me as a quite common requirement, however a solution is not at hand. It would be really nice if somebody has a good suggestion to handle this situation.

Reply
Thijs-jan V. Avatar

It took a while and the solution appeared to be in a complete difficult direction: I had to implement the 'finishView' method in the form type (as suggested in https://stackoverflow.com/a...

Reply

Thanks for sharing Thijs-jan V. - that is indeed an interesting fix - using finishView() hadn't occurred to me!

Reply

Hey Thijs-jan,

Hm, you can take a look at this example: https://symfony.com/doc/cur... - "query_builder" option may help with filtering things but it works in EntityType only IIRC. If it does not fit for you - take a look at Doctrine Criteria, we even have an example about them:
https://symfonycasts.com/sc...
https://symfonycasts.com/sc...

I hope this helps!

Cheers!

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | Victor | posted 4 years ago

Thanks for looking into my situation. It took some time until I was able to investigate further, however unfortunately your suggestions do not cover my situation.

I'm using the first suggestion in many places in my application, but this is only suitable for EntityType and not for CollectionType. It would be perfect if a CollectionType could take a query builder, but that is not the case.

The second suggestion is a variant of my solution creating custom getters, however the given examples don't resolve the impossibility to use a parameter in such getters. Even when I use Criteria, I still have to make a getter for each season.

Reply

Hey Thijs-jan V.

Have you tried this option entry_option (https://symfony.com/doc/current/reference/forms/types/collection.html#entry-options ). You can fetch manually which players you want to show and pass them into your form.
Give it a try and let us know! :)

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | MolloKhan | posted 4 years ago

As I understand, that is not a place to change which items appear in the collection, but more of a place to change the options for the form which is rendered for each item.

Reply

Actually, you can modify all the options of the form type being specified. So, you can pass the query_builder option of your embedded form

Cheers!

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | MolloKhan | posted 4 years ago | edited

I'm afraid you don't understand my problem fully. Let me try to make it more clear with some (untested) code. In the setting below, normally all 8 players from the instance of Team will be displayed in the form, containing a season and a name for each player. But now, I would like to display only the 2019-players in the form. Outside a form, I could just put the 2019 parameter into my getPlayers method, however forms don't seem to have such option.

Your solution would help me if I had to add some extra options to the PlayerType, but that's not the case.

I've seen some 'solutions', which remove the unwanted players from the collection, with an event listener or subscriber. However this is problematic since Doctrine will remove them from the database too, when I do a flush() to save the modified names.

Team.php


class Team {
    private $players = new ArrayCollection([
        new Player(2018, 'Player 1'),
        new Player(2018, 'Player 2'),
        new Player(2018, 'Player 3'),
        new Player(2018, 'Player 4'),
        new Player(2019, 'Player 5'),
        new Player(2019, 'Player 6'),
        new Player(2019, 'Player 7'),
        new Player(2019, 'Player 8'),
    ]);
    
    public function getPlayers($season = null) {
        return $this->players()
            ->filter(function (Player $player) use ($season) {
                return $season === null || $player->getSeason() === $season;
            }
        );
    }
}

Player.php


class Player {
    public $season;
    public $name;
    
    public function __construct($season, $name) {
        $this->season = $season;
        $this->name   = $name;
    }
}

TeamType.php


class TeamType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add(
            'players',
            CollectionType::class,
            [
                'entity_type' => PlayerType::class
            ]
        );
    }
    
    public function configureOptions(OptionsResolver $resolver) {
        $resolver
            ->setDefaults(['data_class' => Team::class]);
    }
}

PlayerType.php


class PlayerType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
        ->add('season', NumberType::class)
        ->add('name', TextType::class);
    }
    
    public function configureOptions(OptionsResolver $resolver) {
        $resolver
            ->setDefaults(['data_class' => Player::class]);
    }
}
Reply

Ok, so, on the Team entity you have a OneToMany relationship to Players, so you can fetch players filtering by Team and season. In that case what you can do is to implement a custom repository method on your PlayerRepository to fetch players based on those 2 parameters. Then in your form you can do something like this:


class TeamType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add(
            'players',
            CollectionType::class,
            [
                'entity_type' => PlayerType::class,
                'query_builder' => function (PlayerRepository $repository) {
                    return $repository->getPlayersByTeamAndSeason($team, $season);
                }
            ]
        );
    }
    ...
}

And in your controller you can inject $team & $season variables into your form.

Or, another thing you can try is to use a model class and add the fields just like you need them. You can get an idea of how to implement a model class by watching this chapter https://symfonycasts.com/screencast/symfony-forms/form-dto

Cheers!

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | MolloKhan | posted 4 years ago

When CollectionType accepts a query_builder parameter, it should be possible. The documentation doesn't mention it, though (https://symfony.com/doc/cur.... But I'll give it a try.

Reply

TBH I haven't tried it out but by reading this from the docs


entry_option:
This is the array that's passed to the form type specified in the entry_type option. 
For example, if you used the ChoiceType as your entry_type option (e.g. for a collection of drop-down menus), 
then you'd need to at least pass the choices option to the underlying type

I understand that "entry_option" is the third parameter you pass to $builder->add('field name', 'FieldType', $arrayOptions)

give it a try and let me know if it works. Cheers!

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | MolloKhan | posted 4 years ago

entry_option is indeed available, but you don't use that in your (last) code example

Reply

Oh sorry, that's my bad I should have coded something like this


class TeamType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add(
            'players',
            CollectionType::class,
            [
                'entity_type' => PlayerType::class,
                'entry_point' => [
                    'query_builder' => function (PlayerRepository $repository) {
                        return $repository->getPlayersByTeamAndSeason($team, $season);
                    }
                ]
            ]
        );
    }
    ...
}
Reply
Haitam Avatar
Haitam Avatar Haitam | posted 4 years ago | edited

Hello, im working with a ManyToMany reationship entities, '<b>Book</b>' and '<b>Tag</b>', when i fill a form to create add a new book i want a Tags combobox wich will show me existing tags on the database as well as the option to create a new one, so i chose to work with <b>select2</b> plugin wich has an option <a href="https://select2.org/tagging#tagging-with-multi-value-select-boxes&quot;&gt;to do that</a>. The issue is that it returns an <i>ArrayCollection</i> and i get an "Invalid value".
i have the following code in my <b>BookType</b> builder :
<blockquote>`
$builder
/.../
->add('tags', EntityType::class, [

            'class' => Tag::class,
            'choice_label' => "name",
            'required' => false
        ]);

<br /></blockquote><br />The code i used to render the input:<br /><blockquote>
{{ form_row(livreForm.descripteurs,{

                'label': "Tags",
                'attr':{
                    'multiple': "multiple",
                    'class': "select-two-multiple"
                }
            }) }}

`</blockquote>
Is there any solution for this type of fields ?

Reply

Hey Haitam

You are almost there! You only need a DataTransformer so you can transform your data into the desired input. You can watch how Ryan implement one here: https://symfonycasts.com/sc...
or you can visit the docs: https://symfony.com/doc/cur...

Cheers!

Reply
Christopher Avatar
Christopher Avatar Christopher | posted 4 years ago

This has been extremely helpful, but I feel like I missing something regarding collections. How would one go about having a fixed number collections (e.g. allow_add = false, allow_delete = false). For example, what if you wanted four Scientists always on the form, you want their user name to be read only, but you want the years studied to writable?

Reply

Hey Cmarcus,

Then allow_add/allow_delete is not what you need :) Take a look at readonly HTML attribute, you can add this to fields you don't want to edit, i.e. usernames, something like this:


$builder->add('username', TextType::class, [
    'attr' => [
        'readonly' => true,
    ],
]);

Cheers!

Reply
Christopher Avatar
Christopher Avatar Christopher | Victor | posted 4 years ago

I understand about not using allow_add/allow_delete. The part I'm missing is how do I add (read: hard-code) multiple GenusScientist forms. Each would have their own unique username value (based on a unique query). So GenusFormType has a Collection of GenusScientist and I want to add exactly 4 GenusScientist to the form. The username is fixed (read-only - you showed how to do that) and the years studied would still allow edits. I know how to modify GenusScientistEmbeddedForm but I can't figure out how to add 4 scientists to GenusFormType based on 4 unique queries. Thanks for your help!

Reply

Hey Cmarcus,

Ah, ok, well, that's tricky :) It depends on if those 4 extra GenusScientist are in the DB or no, i.e. if they have relations to the Genus entity. If they do, then great, Symfony Form should show them for you in your form, and you only need to worry about making those fields readonly for them. How to do it? You can try to do it with JS script probably... or render form fields manually and manually pass readonly in the template to specific data.

But if you don't have those 4 extra GenusScientist in the DB, or don't have relations with Genus entity - then I'm not sure I understand the reason of this :)

Cheers!

1 Reply

Hi, i'm using symfony 4 and i've got problem with form errors in child form because I need errors per field not to child form at all.
I have UserType with collection (User OneToMany BankAccounts)


// UserType
        $builder->add(
            'bankAccounts',
            CollectionType::class,
            [
                'entry_type' => BankAccountType::class,
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false,
                'error_bubbling' => false,
                'entry_options' =>
                    [
                        'label' => false
                    ],
            ]
        );

I tried everything to display it correctly ;) error per field not to child form (BankAccountsType)
How to set it up correctly?

Reply

solved ^_^ entry_options => 'label' => false

1 Reply
Victor Avatar Victor | SFCASTS | bartek | posted 4 years ago | edited

Hey bartek ,

Glad you solved it yourself :) And thank you for sharing your solution with others!

Cheers!

Reply
Thomas L. Avatar

Hi Bartek, I've run into same issue and I am not sure what causing it.

'entry_options' => [ 'label' => false ],

is set and If I remove it and/or set is to true I experience same issue.

Reply

Hey Thomas,

Could you explain what exactly problem do you have? Do you see any error message you can share here with us? We'll try to help you figure it out.

Cheers!

Reply
Thomas L. Avatar

The validation on some form fields are done but the error is not bubbling on that field; even the error bubbling is false. So dumped the form_errors(field) and after some research I've found out, that my issue is caused my using the wrong variable names in the form.

So for example my entity Trees has a field "tree_age" in database, named as $treeAge in entity. In FormClassthat field is included as

->add('tree_age')

and this worked obviously because the form is completely shown and it's able to be submitted ... but with the error explained above. If I change the formfield definition to the correct name

->add('treeAge')

the error is solved.

No Idea why I was using the wrong name but I mention, that this one was not jumping in my face because in version 3 or early 4 the issue was not happening.

Whatever ... now its working. I will take a look into stackoverflow and put my finding into there too because in back of my mind I remember that there was an user with same issue but without answer.

Reply

Hey Thomas!

Thank you for sharing your solution! Yeah, it might be helpful for others. Yep, you need to specify field name the same fay as they are called in your class, not in your DB. I'm glad you solved this problem by yourself, well done!

Cheers!

Reply
Ernest H. Avatar
Ernest H. Avatar Ernest H. | posted 5 years ago

In your lesson for using CollectionType, you have changed from ManyToMany to matching OneToMany and ManyToMany with a new entity to manage the relationship. All makes sense. Your embedded form for multiple drop-down boxes uses that new entity to persist changes to the relationship in the db.

I would like to leave things as a ManyToMany relationship (like in your earlier lessons) but use drop-down boxes like you use in this lesson. However, it doesn't seem to work. I'm listing the entity of the inverse side of the relationship in the embedded form (so I can list choices in the drop-down select field, but when I save it modifies only this entity (inverse side of ManyToMany) rather than the relationship.

In other words, using examples from your entities: Owning entity; Genus. Inverse side: Scientist. How to embed multiple drop-down list of scientists that in the same way you do when there is a named entity joining the two, but instead with the ManyToMany relationship itself defining things? I've tried the equivalent of adding an embedded form of Scientists drop-down list and embedding that into the Genus edit page. I get multiple drop-down lists, but the correct items are not pre-selected, and changing/saving updates the Scientist list itself rather than the relationship.

Reply

Hi Ernest H.!

Sorry for my slow reply - I needed to wait until I have a few minutes to really think about this :). So, the goal would be to have a collection of embedded forms (with the ability to add a new one, remove existing ones, etc), but each embedded form will have only the single, Scientist drop down. Is that correct?

So, here's how to do it :). You will STILL have a CollectionType just like before. The only difference is that the "entry_type" (i.e. the form that's used in the collection) won't be an entirely different form class (e.g. GenusScientistEmbeddedForm), it will simply be the EntityType field. Something like this:


$builder
    ->add('genusScientists', CollectionType::class, [
        'entry_type' => EntityType::class,
        'entry_options' => [
            'class' => User::class,
            'choice_label' => 'email'
        ],
        'allow_delete' => true,
        'allow_add' => true,
    ])

... and that's it! In your template, rendering is simpler, something more like this:


{% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
        <a href="#" class="js-remove-scientist pull-right">
            <span class="fa fa-close"></span>
        </a>
        {# you don't have individual fields to render, you just have the ONE field to render #}
        {{ form_row(genusScientistForm) }}
    </div>
{% endfor %}

If you were doing this from the inverse side instead, then you would just need to make sure to follow the same steps we did here: https://knpuniversity.com/screencast/collections/by-reference-adder-remover

Let me know if that helps! And cheers!

Reply
Ernest H. Avatar

Genius! That works perfectly. Thanks very much.

Glad to hear you had to think about it for a while :)

Reply
Ernest H. Avatar
Ernest H. Avatar Ernest H. | weaverryan | posted 5 years ago | edited

I really really appreciate you helping work this out!!

It looks like the only problem with your proposed solution is that I don't seem to have access to the sub-parts of the added EntityType when rendering the form. Specifically if I try to get rid of the automatically generated labels ("0", "1", "2", etc) in the way you did it in the lesson, it fails, I assume because twig doesn't now how to get to it:

`
{% for genusScientistForm in genusForm.genusScientists %}

<div class="col-xs-4 js-genus-scientist-item">
    {{ form_errors(genusScientistForm) }}
    {{ form_row(genusScientistForm.user) }} <------------------- this line fails
</div>

{% endfor %}
`

Thoughts?

Reply

Yo Ernest H.!

I think it should look something like this:


{% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
        {# you don't have individual fields to render, you just have the ONE field to render #}
        {{ form_errors(genusScientistForm) }}
        {{ form_label(genusScientistForm, 'Genus Scientist') }}
        {{ form_widget(genusScientistForm) }}
    </div>
{% endfor %}

In the tutorial, genusScientistForm is really an embedded form, with sub-parts as you mentioned. But now, genusScientistForm is literally just a single field (EntityType)... without any sub-fields. So, you just need to render it like any other single field :). One simpler option might be something like this:


{% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
        {{ form_row(genusScientistForm, {
            label: 'Genus Scientist')
        } }}
    </div>
{% endfor %}

Not tested - but give it a try :).

Cheers!

Reply
Default user avatar
Default user avatar Dan Costinel | posted 5 years ago | edited

Hi. I want to be able to display such a form: https://imgur.com/a/NsDQISz .

In short, it's about a Quiz project, and at this point, I need to let the admin create a new Question, as well as the Answers for the question in the same template.
For that I have two tables, Question 1-N Answers related:
-- Question [id, question_text, explication, questionNo, answers]
-- Answer [ answer_text, isCorrect, question ].
One problem, is that the admin can define for a question, a variable number of answers. So it can define 'till six answers (in the image I've shown just 3 for simplicity).
A second problem, is that the answers needs to be a CKEditor, so that the admin can easily apply styles to the text.

I've followed the docs from http://symfony.com/doc/3.4/form/embedded.html.

I expect the html to be like: https://gist.github.com/dancostinel/9be2690ce2d3bb8561f83b03bfe145f0

And then maybe to get beck, after the form is submitted, something like (and let's say the admin adds 3 answers for a question):


QuestionNo: 1
Quiz: object
Question Text: whatever is entered in the editor
Answer 1 [
    answer_text: whatever is entered in the editor
    isCorrect: 1 or 0, depending if the checkbox next to Answer 1 label is checked or not
]
Answer 2 [
    answer_text: whatever is entered in the editor
    isCorrect: 1 or 0, depending if the checkbox next to Answer 2 label is checked or not
]
Answer 3 [
    answer_text: whatever is entered in the editor
    isCorrect: 1 or 0, depending if the checkbox next to Answer 3 label is checked or not
]

And then to be able to save:

  • in questions table, the QuestionNo, quiz_id, Question text, explication
  • in answers table, the answer_text, isCorrect, and question_id

Thanks a lot for any suggestion!

Reply

Hey Dan,

At the first sight, I think you can do it with Symfony Forms. But what is your question exactly? I don't see a question at all. Have you tried to implement it? Do you have any errors/problems?

Cheers!

Reply
Default user avatar
Default user avatar Dan Costinel | Victor | posted 5 years ago | edited

Hi Victor, thanks for replying.
Sorry that I've forgot to post an actual question. Anyways, I've solved that one. Indeed you were right, I was able to solve it using forms.

Now, I would like to be able to edit those kind of form fields (Answer fields inside Question form).
Here's an image of what I get when submitting the edit form (nothing special in that form, just an usual CRUD Controller-view generated code for editing). https://imgur.com/a/yoc8kE9
And this would be the question: how to edit a embedded form-collection field?

At first point, for editing, in the editAction(), I need to somehow get each Answer entity that was previously saved on the Question creation, and attach them into the Question object that is being edited. So if the question_id is 1, and the answers ids are: 1 , 2, 3, 4, then I need to somehow pass the data saved in the answers, into the first four Answer objects added into the Question Entity. I've added each of those 6 Answer objects into the Question Entity like this:


/**
 * @Assert\Type(type="AppBundle\Entity\Answer")
 */
private $answer1;

public function getAnswer1()
{
    return $this->answer1;
}

public function setAnswer1(Answer $answer = null)
{
    $this->answer1 = $answer;

    return $this;
}

I've managed to do that, even though the code is really ugly.

So, returning to the problem, if we look at the image, we have that QuestionController.php on line 235: there is the edited data for the Question fields + Answer fields,
Down under, at QuestionController.php on line 237, is the original Answers that were inserted when the Question was created.
I need to somehow, add the edited Answer fields to those original Answers.
Any solution? :)
Thanks, and sorry for this long post.

Reply

Hey Dan,

Glad you solved it by yourself! Do you solve it in a one complex form? Or do you create a several forms for it? Actually, if you need for editing the same form as you used for inserting - just reuse that form, but now you need to pass already existent data to the form, for example, in a controller:


$question = $this->getDoctrine()
    ->getEntityManager()
    ->getRepository(Question::class)
    ->find($id);
$form = $this->createForm(YourQuestionForm::class, $question);

// Then just pass form view to the template and render it.

As you can see, I pass data as a second argument to createForm(), and this way Form component will render the same form but with actual data, i.e. fill all the fields with current values in DB, so you will be able to edit them and send edited form. Then just handle submitted form as you already to with inserting data, i.e. if form is valid - call flush() and that's it.

Cheers!

Reply
Default user avatar
Default user avatar Dan Costinel | Victor | posted 5 years ago

Hi Victor,
Thanks again for replying.
Yes, I'm using one single Question form.
I'll try your code as soon as I get to my PC, and I'll let you know if it worked or not.
Cheers!

Reply

Hey Dan,

Great, then it should work I think. Or let me know if you have any issues with it ;)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice