Home > Software engineering >  Rendering multiple Form fields for one Entity field
Rendering multiple Form fields for one Entity field

Time:09-24

I have a problem using Symfony 5 Forms.

I have two entities:

  • Reservation
  • Menus

They both have a ManyToMany relation. I want to create for each Menu object that is registered in database a numeric field input.

For example : there a 3 menus : A, B, C

I want the form to generate (among the other generated fields used for the reservation entity) 3 numeric fields and type in each of them the quantity i want --> ( 3 menus A, 2 menus B and 1 menu C)

My problems is that all theses 3 menus are registered in the Reservation entity as “menus” field.

I tried to iterate over Menu objects to add fields to my form but it seems that the form only takes the last Menu and to not renders the others.

Any idea to generate theses fields ?

Reservation.php

/**
 * @ORM\Entity(repositoryClass=ReservationRepository::class)
 */
class Reservation
{
...

    /**
     * @ORM\ManyToMany(targetEntity=Menu::class, mappedBy="reservation")
     */
    private $menus;
}

Menu.php

/**
 * @ORM\Entity(repositoryClass=MenuRepository::class)
 */
class Menu
{
...
    /**
     * @ORM\ManyToMany(targetEntity=Reservation::class, inversedBy="menus")
     */
    private $reservation;
...
}

ReservationType.php

class ReservationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('firstName', null, [
                'attr' => [
                    'class' => 'custom-input form-control-lg',
                    'placeholder' => 'First name'
                ],
                'label' => false
            ])
            ->add('lastName', null, [
                'attr' => [
                    'class' => 'custom-input form-control-lg',
                    'placeholder' => 'Last name'
                ],
                'label' => false
            ])
            ->add('phoneNumber', null, [
                'attr' => [
                    'class' => 'custom-input form-control-lg',
                    'placeholder' => 'Phone number'
                ],
                'label' => false
            ])
            ->add('paymentMethod', ChoiceType::class, [
                'attr' => [
                    'class' => 'form-control-lg'
                ],
                'placeholder' => 'Payment method',
                'choices' => [
                    "LYDIA" => true,
                    "CASH" => true
                ],
                'label' => false
            ])
        ;
    }

What I've tried with the form so far

AppController.php

<?php
#[Route('/', name: 'home')]
public function index(TableRepository $tableRepository, MenuRepository $menuRepository, Request $request): Response
{
    //...

    $form = $this->createForm(ReservationType::class, $reservation);
    $menus = $menuRepository->findAll();

    //...

    foreach ($menus as $menu) {
        $form->add('menus', TextType::class, [
            'attr' => [
                'placeholder' => 'Menu "' . $menu->getName() . '"',
                'class' => 'custom-input form-control-lg'
            ],
            'label' => false
        ]);
    }

    //...

    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        // $reservation = $form->getData();
        dump($reservation);
        return $this->redirectToRoute('home');
    }
}

What I get after the rendering (with 3 registered menus) : Result after rendering the form

After submitting the Form, I get this error (I understand that it is not the intented object but i thought i could create the Menu object after the submit) : Error

CodePudding user response:

I actually found how to fix my problem. It's as simple as an option called mapped. I just had to set it to false.

Here is the code to generate the fields :

        foreach ($menus as $menu) {
            $form->add('menuReservation_' . $menu->getName(), TextType::class, [
                'mapped' => false,
                'attr' => [
                    'placeholder' => 'Menu "' . $menu->getName() . '"',
                    'class' => 'custom-input form-control-lg'
                ],
                'label' => false
            ]);
        }

After form validation :


                foreach ($menus as $menu) {
                    $menuReservation = new MenuReservation();
                    $menuReservation
                        ->setMenu($menu)
                        ->setReservation($reservation)
                        ->setQuantity($form['menuReservation_' . $menu->getName()]->getData());
                    $entityManager->persist($menuReservation);
                    $reservation->addMenuReservation($menuReservation);
                }
                $entityManager->persist($reservation);

I also had to update the ReservationTypeForm to allow extra fields :


    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Reservation::class,
            //...
            'allow_extra_fields' => true
        ]);
    }

CodePudding user response:

After the comment thread and your answer i saw you modified your ManyToMany to two OneToMany/ManyToOne relationships, with that you can take advantage of the [CollectionType][1] class and let the form to deal with the field rendering and you only focus on the data, no need to add fields with a foreach.

I'm assuming you can also edit the reservation, so in your controller you need to check if the relationship is empty before creating the form.

if (empty($reservation->getMenuReservations())){
    $menus = $this->getDoctrine()->getRepository(Menu::class)->findAll(); //I'm assuming you only have those 3 menus
    foreach ($menus as $menu){
        $mr = new MenuReservation();
        $mr->setReservation($reservation)->setMenu($menu)->setQuantity(0);
    }
}
$form = $this->createForm(ReservationType::class, $reservation);

WIth this, if you're editing or the form isn't valid, you only pre-fill the menus if the reservation is "new", otherwise, the existing ones are used.

Now, for the CollectionType magic to work, the easiest way is to create a separate form only for the MenuReservationEntity with a single field with the quantity and the name of the menu for the label:

class MenuReservationType extends AbstractType { 
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('quantity', IntegerType::class, [
            'label' => $builder->getData()->getName(),
            'attr' => ['min' => 0] //cant' have negative menus
        ]);
    }
}

And let your CollectionType know about it:

...
->add('menuReservation', CollectionType::class, [
    'entry_type' => MenuReservatioType::class,
    //if you have problems whtn submitting, it may be because the next option is false by default, uncomment if necessary 
    //'allow_add' => true
])
...

And finally, if you don't have the entities to cascade persist, you need to persist them individually, also, if the quantity is 0, you don't need to persist it (no point in saving empty values to the DB).

  • Related