wip: form refactoring

This commit is contained in:
Björn Fromme
2026-03-16 11:59:09 +01:00
parent 65a33922cf
commit d719b17aec
13 changed files with 158 additions and 100 deletions
+8 -17
View File
@@ -3,9 +3,8 @@
namespace App\Form;
use App\BusProNet\Form\CountryType;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\Model\ParticipantDto;
use App\Form\Service\ParticipantFieldOptionsProvider;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
@@ -85,14 +84,12 @@ class BookingCreateParticipantType extends AbstractType
return;
}
// Traverse up the form tree to get the root form's data.
$rootForm = $form;
while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent();
}
// Get the booking DTO from the root form
$bookingDto = $this->fieldOptionsProvider->getBookingDtoFromForm($form);
/** @var BookingDtoInterface $bookingDto */
$bookingDto = $rootForm->getData();
if (null === $bookingDto) {
return;
}
$this->addDynamicFields($form, $bookingDto, $participantData->index);
$this->applyFieldStates($form, $bookingDto, $participantData->index);
@@ -110,14 +107,8 @@ class BookingCreateParticipantType extends AbstractType
return;
}
// Get the root form data to access BookingDtoInterface
$rootForm = $form;
while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent();
}
/** @var BookingDtoInterface $bookingDto */
$bookingDto = $rootForm->getData();
// Get the booking DTO from the root form
$bookingDto = $this->fieldOptionsProvider->getBookingDtoFromForm($form);
if (null === $bookingDto) {
return;
+17 -18
View File
@@ -7,7 +7,6 @@ use App\BusProNet\Form\CountryType;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Service;
use App\Form\Model\ParticipantDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\EditFieldStateProvider;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
@@ -23,7 +22,8 @@ class BookingEditParticipantType extends AbstractType
{
public function __construct(
private readonly EditFieldStateProvider $fieldStateProvider,
) {}
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
@@ -39,16 +39,15 @@ class BookingEditParticipantType extends AbstractType
$form = $event->getForm();
// Traverse up the form tree to get the root form's data (BookingEditDto)
$rootForm = $form;
while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent();
// Get the booking DTO from the root form
$bookingDto = $this->fieldStateProvider->getBookingDtoFromForm($form);
if (null === $bookingDto) {
return;
}
/** @var BookingDtoInterface $bookingDto */
$bookingDto = $rootForm->getData();
// Helper to get state for a field
$getState = fn(string $field) => $this->fieldStateProvider->getFieldState($field, $bookingDto, $participantIndex);
$getState = fn (string $field) => $this->fieldStateProvider->getFieldState($field, $bookingDto, $participantIndex);
$form
->add('firstName', TextType::class, $this->mergeFieldState([
@@ -248,7 +247,7 @@ class BookingEditParticipantType extends AbstractType
$pickupLabel = $pickup->city;
if (null !== $pickup->street) {
$pickupLabel .= ' (' . $pickup->street . ')';
$pickupLabel .= ' ('.$pickup->street.')';
}
$price = $pickup->price;
@@ -285,21 +284,20 @@ class BookingEditParticipantType extends AbstractType
return;
}
// Traverse up the form tree to get the root form's data (BookingEditDto)
$rootForm = $form;
while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent();
// Get the booking DTO from the root form
$bookingDto = $this->fieldStateProvider->getBookingDtoFromForm($form);
if (null === $bookingDto) {
return;
}
/** @var BookingDtoInterface $bookingDto */
$bookingDto = $rootForm->getData();
$participantIndex = $form->getData()->index;
// Helper to get state for a field based on submitted data
$getState = fn(string $field) => $this->fieldStateProvider->getFieldState($field, $bookingDto, $participantIndex, $data);
$getState = fn (string $field) => $this->fieldStateProvider->getFieldState($field, $bookingDto, $participantIndex, $data);
// Re-apply field states to all personal data fields
$personalDataFields = [
'firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile'
'firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile',
];
foreach ($personalDataFields as $field) {
if ($form->has($field)) {
@@ -389,6 +387,7 @@ class BookingEditParticipantType extends AbstractType
$fieldOptions[$key] = $value;
}
}
return $fieldOptions;
}
}
-5
View File
@@ -40,11 +40,6 @@ class BookingCreateDto implements BookingDtoInterface
return $this->participants;
}
public function getTravel(): Travel
{
return $this->travel;
}
public function hasParticipant(int $index): bool
{
return isset($this->participants[$index]);
+1 -17
View File
@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\Form\Model;
use App\BusProNet\Model\Travel;
/**
* Interface for unified access to booking data across create and edit workflows.
*
@@ -23,13 +21,6 @@ interface BookingDtoInterface
*/
public function getParticipants(): array;
/**
* Gets the travel data for the booking.
*
* @return Travel The travel data containing services, hotels, and other booking options
*/
public function getTravel(): Travel;
/**
* Checks if a participant exists at the given index.
*
@@ -47,11 +38,4 @@ interface BookingDtoInterface
* @return ParticipantDto|null The participant DTO or null if not found
*/
public function getParticipant(int $index): ?ParticipantDto;
/**
* Gets all selected rooms for the booking.
*
* @return array<int, RoomSelectionDto> Array of selected room DTOs (may be empty for edit DTOs)
*/
public function getSelectedRooms(): array;
}
}
+6 -10
View File
@@ -10,18 +10,19 @@ use Symfony\Component\Validator\Constraints as Assert;
class BookingEditDto implements BookingDtoInterface
{
public ?Booking $booking = null;
public ?Travel $travel = null;
/**
* @var array<int, ParticipantDto>
*/
#[Assert\Valid]
public array $participants = [];
public function __construct(public Booking $booking, public Travel $travel)
{
}
public static function fromBooking(Booking $booking, Travel $travel): static
{
$instance = new static();
$instance = new static($booking, $travel);
$instance->booking = $booking;
$instance->travel = $travel;
@@ -69,11 +70,6 @@ class BookingEditDto implements BookingDtoInterface
return $this->participants;
}
public function getTravel(): Travel
{
return $this->travel;
}
public function hasParticipant(int $index): bool
{
return isset($this->participants[$index]);
@@ -87,7 +83,7 @@ class BookingEditDto implements BookingDtoInterface
/**
* Gets all selected rooms for the booking (edit context).
*
* @return array<int, RoomSelectionDto> Always returns an empty array for edit DTOs unless implemented.
* @return array<int, RoomSelectionDto> always returns an empty array for edit DTOs unless implemented
*/
public function getSelectedRooms(): array
{
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
+7 -3
View File
@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingDtoInterface;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
use App\Form\ParticipantFieldHandler\Condition\MutabilityCondition;
use App\Form\ParticipantFieldHandler\Condition\ApplicantCondition;
use App\Form\ParticipantFieldHandler\Condition\CompositeCondition;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
use App\Form\ParticipantFieldHandler\Condition\MutabilityCondition;
/**
* Field state provider for the booking edit workflow.
@@ -21,6 +21,8 @@ use App\Form\ParticipantFieldHandler\Condition\CompositeCondition;
*/
class EditFieldStateProvider implements FieldStateProviderInterface
{
use FormTraversalTrait;
/** @var array<string, array<string, FieldConditionInterface>> */
private array $fieldStateConditions = [];
@@ -109,6 +111,7 @@ class EditFieldStateProvider implements FieldStateProviderInterface
foreach ($this->fieldStateConditions[$fieldName] as $condition) {
$dependencies = array_merge($dependencies, $condition->getDependentFields());
}
return array_unique($dependencies);
}
@@ -121,6 +124,7 @@ class EditFieldStateProvider implements FieldStateProviderInterface
$allStates[$fieldName] = $fieldState;
}
}
return $allStates;
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingDtoInterface;
use Symfony\Component\Form\FormInterface;
/**
* Trait providing form tree traversal utilities.
*
* This trait contains common form navigation logic used across different
* form services and types. It centralizes the logic for finding root forms
* and extracting booking DTOs from form hierarchies.
*/
trait FormTraversalTrait
{
/**
* Gets the BookingDtoInterface from the root of the form tree.
*
* This helper method traverses up the form tree to find the root form
* and extracts the BookingDtoInterface data. This is shared logic used
* by both create and edit participant forms.
*
* @param FormInterface $form The form to start traversing from
*
* @return BookingDtoInterface|null The booking DTO or null if not found
*/
public function getBookingDtoFromForm(FormInterface $form): ?BookingDtoInterface
{
// Traverse up the form tree to get the root form's data
$rootForm = $form;
while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent();
}
$data = $rootForm->getData();
return $data instanceof BookingDtoInterface ? $data : null;
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
@@ -26,6 +27,8 @@ use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
*/
class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
{
use FormTraversalTrait;
/** @var array<string, callable> Field option providers indexed by field name */
private array $fieldOptionProviders = [];
@@ -120,7 +123,7 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
*/
private function registerFieldOptionProviders(): void
{
// Room assignment field provider
// Room assignment field provider (only available for create workflow)
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
'label' => 'Zimmer',
'placeholder' => 'Bitte wählen',
@@ -128,11 +131,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
// - Shows only available rooms for this participant
// - Excludes rooms already assigned to other participants
// - Respects room capacity and booking constraints
'choice_loader' => $this->roomChoiceLoaderFactory->create(
$bookingDto->participants,
$bookingDto->getSelectedRooms(),
$participantIndex
),
'choice_loader' => $bookingDto instanceof BookingCreateDto
? $this->roomChoiceLoaderFactory->create(
$bookingDto->participants,
$bookingDto->getSelectedRooms(),
$participantIndex
)
: null,
];
// Future field providers would be added here, for example: