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...