1

I'm currently developing a small CRM for a hotel, and I'm hitting a problem with dynamic forms. On the RoomBooking form, the user is invited to select a start date and an end date for the booking. At this moment, I want to display a kind of subform corresponding to breakfast. I want one of those subforms for each of the nights within the range (ex: if the booking lasts three nights, I want three subforms).

I tried using dynamic forms, but I couldn't manage to display any of the forms...

Here is an extract of my RoomBookingsType.php

class RoomBookingsType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('booking_date', DateType::class, [
                'attr' => [
                    'id' => 'booking_date'
                ]
            ])
            ->add('start_date', DateType::class, [
                'attr' => [
                    'id' => 'start_date'
                ]
            ])
            ->add('end_date', DateType::class, [
                'attr' => [
                    'id' => 'end_date'
                ]
            ])

            ->add('client_name')
            ->add('client_email')
            ->add('client_phone')
            ->add('client_country', EntityType::class, [
                'class' => Countries::class,
                'placeholder' => 'Sélectionnez un pays', // optionnel
            ])

            ->add('nb_guest', IntegerType::class, [
                'attr' => [
                    'min' => 1,
                    'value' => 1
                ]
            ])
            ->add('agency', ChoiceType::class, [
                'choices' => [
                    'La Pèlerine' => 'La Pèlerine',
                    'La Balagère' => 'La Balagère',
                    'Follow the Camino' => 'Follow the Camino',
                    'La Rebène' => 'La Rebène',
                    'Via Compostella' => 'Via Compostella',
                    'Trekking Découverte' => 'Trekking Découverte',
                    'L\'Autre Chemin' => 'L\'Autre Chemin',
                    'Chemins de France' => 'Chemins de France',
                    'Sud Randos' => 'Sud Randos',
                    'S-Cape' => 'S-Cape',
                    'Grand Angle' => 'Grand Angle',
                    'Nature Occitane' => 'Nature Occitane',
                    'Autre' => 'Autre',
                ],
                'placeholder' => 'Sélectionnez une agence', // optionnel
            ])
            ->add('booking_by', ChoiceType::class, [
                'choices' => [
                    'Email' => 'Email',
                    'Téléphone' => 'Téléphone',
                    'Booking.com' => 'Booking.com',
                    'En personne' => 'En personne',
                    'Autre' => 'Autre',
                ],
                'placeholder' => 'Sélectionnez un moyen de réservation', // optionnel
            ])
            ->add('payment_method', ChoiceType::class, [
                'choices' => [
                    'Carte' => 'Carte',
                    'Espèces' => 'Espèces',
                    'Chèque' => 'Chèque',
                    'ANCV' => 'ANCV',
                    'Amex' => 'Amex',
                    'Virement' => 'Virement',
                    'Autre' => 'Autre',
                ],
                'placeholder' => 'Sélectionnez une moyen de paiement', // optionnel
            ])
            ->add('paid')
            ->add('comment')
            ->add('nb_dinner', IntegerType::class, [
                'attr' => [
                    'min' => 0,
                    'value' => 0
                ]
            ])
            ->add('nb_breakfast', IntegerType::class, [
                'attr' => [
                    'min' => 0,
                    'value' => 0
                ]
            ])
            ->add('children')
            ->add('dog')
            ->add('room', EntityType::class, [
                'class' => Rooms::class,
                'placeholder' => 'Sélectionnez une chambre', // optionnel
            ])
        ;

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event): void {
                $form = $event->getForm();

                // this would be your entity, i.e. SportMeetup
                $data = $event->getData();

                $startDate = $data->getStartDate();
                $endDate = $data->getEndDate();
                $duration = round(($endDate - $startDate) / (60 * 60 * 24));
                
                // ??? what do I do ???
            }
        );
    }

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

My BreakfastsType:

class BreakfastsType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Breakfasts::class,
        ]);
    }
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('date', DateType::class, [
                'widget' => 'single_text',
                'disabled' => true,
            ])
            ->add('isTaken', CheckboxType::class, [
                'required' => false,
                'mapped' => false,
            ])
            ->add('nbGuest', IntegerType::class, [
                'required' => true,
            ])
            ->add('comment', TextareaType::class, [
                'required' => false,
            ]);
    }
}

My route in RoomBookingsController:

#[Route('/ajouter', name: 'app_room_bookings_new', methods: ['GET', 'POST'])]
    public function new(Request $request, EntityManagerInterface $entityManager): Response
    {
        $roomBooking = new RoomBookings();
        $form = $this->createForm(RoomBookingsType::class, $roomBooking);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager->persist($roomBooking);
            $entityManager->flush();

            return $this->redirectToRoute('app_room_bookings_index', [], Response::HTTP_SEE_OTHER);
        }

        return $this->render('room_bookings/new.html.twig', [
            'room_booking' => $roomBooking,
            'form' => $form,
        ]);
    }

Here is my main form (RoomBookings)

{{ form_start(form, {'attr': {'class': 'w-full'}}) }}

<div class="form-input-container">
    <h3 class="col-span-2 text-lg font-bold border-b-[1px] border-b-orange-500">Chambre</h3>
    {# Chambre associée (select) #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Chambre
        </label>
        <div class="relative w-full">
            {{ form_widget(form.room, {
                'attr': {'class': 'appearance-none form-select-input'}
            }) }}
            <twig:ux:icon name="mynaui:chevron-up-down" class="form-select-input-icon" style="height: 24px;"/>
            {{ form_errors(form.room) }}
        </div>
    </div>

    {# Nombre de personnes #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Nombre de personnes
        </label>
        <div class="w-full">
            {{ form_widget(form.nb_guest, {
                'attr': {'class': 'appearance-none form-text-input', 'min': 1, 'max': 5}
            }) }}
            {{ form_errors(form.nb_guest) }}
        </div>
    </div>
</div>

<div class="form-input-container">
    <h3 class="col-span-2 text-lg font-bold border-b-[1px] border-b-orange-500">Dates</h3>
    {# Date d'arrivée #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Date d'arrivée*
        </label>
        <div class="relative w-full">
            {{ form_widget(form.start_date, {
                'attr': {'class': 'form-text-input'}
            }) }}
            {{ form_errors(form.start_date) }}
        </div>
    </div>

    {# Date de départ #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Date de départ*
        </label>
        <div class="w-full">
            {{ form_widget(form.end_date, {
                'attr': {'class': 'form-text-input'}
            }) }}
            {{ form_errors(form.end_date) }}
        </div>
    </div>
</div>

<div
    class="form-input-container">
    {# Date de réservation #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Date de réservation*
        </label>
        <div class="w-full">
            {{ form_widget(form.booking_date, {
                'attr': {'class': 'form-text-input'}
            }) }}
            {{ form_errors(form.booking_date) }}
        </div>
    </div>
</div>

<div class="form-input-container">
    <h3 class="col-span-2 text-lg font-bold border-b-[1px] border-b-orange-500">Informations client</h3>
    {# Nom du client #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Nom du client*
        </label>
        <div class="w-full">
            {{ form_widget(form.client_name, {
                'attr': {'class': 'form-text-input', 'placeholder':'ex: Jean Dupont'}
            }) }}
            {{ form_errors(form.client_name) }}
        </div>
    </div>

    {# Nationalité du client #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Nationalité*
        </label>
        <div class="relative w-full">
            {{ form_widget(form.client_country, {
                'attr': {'class': 'appearance-none form-select-input'}
            }) }}
            <twig:ux:icon name="mynaui:chevron-up-down" class="form-select-input-icon" style="height: 24px;"/>
            {{ form_errors(form.client_country) }}
        </div>
    </div>

    {# Telephone du client #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Téléphone*
        </label>
        <div class="w-full">
            {{ form_widget(form.client_phone, {
                'attr': {'class': 'form-text-input', 'placeholder':'ex: (+33) 06578329'}
            }) }}
            {{ form_errors(form.client_phone) }}
        </div>
    </div>

    {# Email du client #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Email*
        </label>
        <div class="w-full">
            {{ form_widget(form.client_email, {
                'attr': {'class': 'form-text-input', 'placeholder':'ex: [email protected]'}
            }) }}
            {{ form_errors(form.client_email) }}
        </div>
    </div>
</div>

<div class="form-input-container">
    <h3 class="col-span-2 text-lg font-bold border-b-[1px] border-b-orange-500">Détails réservation</h3>

    {# Agence #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Agence
        </label>
        <div class="relative w-full">
            {{ form_widget(form.agency, {
                'attr': {'class': 'appearance-none form-select-input'}
            }) }}
            <twig:ux:icon name="mynaui:chevron-up-down" class="form-select-input-icon" style="height: 24px;"/>
            {{ form_errors(form.agency) }}
        </div>
    </div>


    {# Réservé par #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Réservé par
        </label>
        <div class="relative w-full">
            {{ form_widget(form.booking_by, {
                'attr': {'class': 'appearance-none form-select-input'}
            }) }}
            <twig:ux:icon name="mynaui:chevron-up-down" class="form-select-input-icon" style="height: 24px;"/>
            {{ form_errors(form.booking_by) }}
        </div>
    </div>

    {# Méthode de paiement #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Méthode de paiement
        </label>
        <div class="relative w-full">
            {{ form_widget(form.payment_method, {
                'attr': {'class': 'appearance-none form-select-input'}
            }) }}
            <twig:ux:icon name="mynaui:chevron-up-down" class="form-select-input-icon" style="height: 24px;"/>
            {{ form_errors(form.payment_method) }}
        </div>
    </div>


    {# Payé (checkbox) #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <div class="flex items-center gap-4 w-full h-full row-start-2 row-end-3">
            <label class="form-label-checkbox">
                Payé
            </label>
            <div class="w-3/4 flex items-center gap-2">
                {{ form_widget(form.paid, {
            'attr': {'class': 'h-4 w-4 text-orange-600 border-gray-300 rounded'}
        }) }}
                {{ form_errors(form.paid) }}
            </div>
        </div>
    </div>

    {# Nombre de petits-déjeuners #}
    <div class="grid grid-rows-[auto_1fr] w-full h-full">
        <label class="form-label m-0">
            Nombre de petits-déjeuners prévus
        </label>
        <div class="relative w-full h-full flex items-center">
            {{ form_widget(form.nb_breakfast, {
                'attr': {'class': 'form-text-input', 'min': 0}
            }) }}
            {{ form_errors(form.nb_breakfast) }}
        </div>
    </div>

    {# Nombre de dîners prévu#}
    <div class="grid grid-rows-[auto_auto] w-full">
        <label class="form-label">
            Nombre de dîners prévus
        </label>
        <div class="relative w-full">
            {{ form_widget(form.nb_dinner, {
                'attr': {'class': 'form-text-input', 'min': 0}
            }) }}
            {{ form_errors(form.nb_dinner) }}
        </div>
    </div>

    {# Enfants (checkbox) #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <div class="flex items-center gap-4 w-full h-full row-start-2 row-end-3">
            <label class="form-label-checkbox">
                Enfant(s)
            </label>
            <div class="w-3/4 flex items-center gap-2">
                {{ form_widget(form.children, {
            'attr': {'class': 'h-4 w-4 text-orange-600 border-gray-300 rounded'}
        }) }}
                {{ form_errors(form.children) }}
            </div>
        </div>
    </div>

    {# Chien (checkbox) #}
    <div class="grid grid-rows-[auto_auto] w-full">
        <div class="flex items-center gap-4 w-full h-full row-start-2 row-end-3">
            <label class="form-label-checkbox">
                Chien(s)
            </label>
            <div class="w-3/4 flex items-center gap-2">
                {{ form_widget(form.dog, {
            'attr': {'class': 'h-4 w-4 text-orange-600 border-gray-300 rounded'}
        }) }}
                {{ form_errors(form.dog) }}
            </div>
        </div>
    </div>

    {# Champ Commentaire #}
    <div class="grid grid-rows-[auto_auto] w-full col-span-2">
        <label class="form-label">
            Commentaire
        </label>
        <div>
            {{ form_widget(form.comment, {
            'attr': {'class': 'form-description-input', 'placeholder':'Notes supplémentaires'}
        }) }}
            {{ form_errors(form.comment) }}
        </div>
    </div>
</div>

{# Boutons #}
<div class="form-buttons-container">
    <a href="{{ path('app_room_bookings_index') }}" class="edit-button gap-2 items-center">
        <twig:ux:icon name="nrk:back" style="height: 24px;"/>
        Retour à la liste
    </a>

    <button class="edit-button cursor-pointer gap-2 items-center">
        <twig:ux:icon name="material-symbols-light:save-outline-rounded" style="height: 24px;"/>
        {{ button_label|default('Enregistrer') }}
    </button>
</div>

{{ form_end(form) }}

and the breakfast form (suppose to duplicate for each night

<div id="breakfasts">
    {% for breakfastForm in form.breakfasts %}
        <div class="p-2 border rounded mb-2">
            {{ form_row(breakfastForm.date) }}
            {{ form_row(breakfastForm.isTaken) }}
            {{ form_row(breakfastForm.number) }}
        </div>
    {% endfor %}
</div>
<div id="breakfasts">
    {% for breakfastForm in form.breakfasts %}
        <div class="p-2 border rounded mb-2">
            {{ form_row(breakfastForm.date) }}
            {{ form_row(breakfastForm.isTaken) }}
            {{ form_row(breakfastForm.number) }}
        </div>
    {% endfor %}
</div>


I mostly tried the solution found in the different tutorials found on the internet, but nothing really matched my expectations...

2
  • 1
    Thanks for this - So what have you've actually tried? "want one of those subforms for each of the nights within the range" - I don't see a subtraction between the end and start date defining this range. Please clarify where you are actually stuck. Is this a multi-step form or are you trying to do this on one page? If it's the latter, then you'll need an ajax request to inject the new fields Commented Aug 27 at 6:40
  • Why don't use a simple loop in your twig ? Be careful, you can't create the same FormView, you should loop in your Controller to create X Form. Or, I would say better, create a new Form with a CollectionType of BreakFastType, easier for me to handle. Commented Aug 28 at 7:48

1 Answer 1

-2

I write an answer, but there is other ways to achieve it.

Create a new Form:

class BreakfastsCollectionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('breakfasts', CollectionType::class, [
            'entry_type' => BreakfastsType::class,
            'entry_options' => ['label' => false],
            'allow_add' => true,
            'allow_delete' => true,
            'by_reference' => false,
        ]);
    }
}

And in Controller:

    $breakfasts = [];
    // here, you loop over the dates you need, startDate / endDate from the RoomBookings for example
    foreach ([new \DateTime('today'), new \DateTime('tomorrow')] as $date) {
        $b = new Breakfasts();
        $b->setDate($date);
        $breakfasts[] = $b;
    }
    $form = $this->createForm(BreakfastsCollectionType::class, ['breakfasts' => $breakfasts]);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $data = $form->getData()['breakfasts']; // Array of Breakfasts entities
        // persist them all at once
    }

Then in Twig, you can loop as you did. I guess it's the easiest way.

Sign up to request clarification or add additional context in comments.

3 Comments

foreach ([new \DateTime('today'), new \DateTime('tomorrow')] as $date) { - How is this useful? OP is requesting this data in the first form and this is not reflected in this answer - "I want one of those subforms for each of the nights within the range (ex: if the booking lasts three nights, I want three subforms)."
It's just an example to indicate you have to loop over dates to build your Collection. I added a comment, but the question is about the form Collection, other things are not relevant
It's definitely important to know which strategy OP is intending to use here, hence my comment - Can't see how this answer in it's current state is useful as it doesn't explain how or where to use it

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.