wip: form refactoring

This commit is contained in:
Björn Fromme
2025-07-25 09:28:13 +02:00
parent 2b122a7cc4
commit ae2ed306b9
13 changed files with 158 additions and 100 deletions
@@ -23,8 +23,7 @@ class CreateStep1Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
)
{
) {
}
/**
@@ -34,6 +33,10 @@ class CreateStep1Controller extends AbstractController
public function index(Request $request): Response
{
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
// Capture the current room selection state before form processing
$oldRoomSelectionSnapshot = $this->bookingService->createRoomSelectionSnapshot($bookingCreateDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
// Validate step access - allow step 1 or redirect to current step
@@ -46,6 +49,9 @@ class CreateStep1Controller extends AbstractController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($this->bookingService->hasRoomSelectionChanged($oldRoomSelectionSnapshot, $bookingCreateDto)) {
$this->bookingService->resetParticipantAssignments($bookingCreateDto);
}
$bookingCreateDto->currentStep = 2;
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
@@ -80,8 +86,6 @@ class CreateStep1Controller extends AbstractController
]);
$form->handleRequest($request);
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($summary['selectedRooms'], $availableRooms);
@@ -27,8 +27,7 @@ class CreateStep2Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
)
{
) {
}
/**
+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;
+14 -15
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,13 +39,12 @@ 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);
@@ -285,13 +284,12 @@ 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
@@ -299,7 +297,7 @@ class BookingEditParticipantType extends AbstractType
// 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]);
-16
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;
/**
+6 -2
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(
'choice_loader' => $bookingDto instanceof BookingCreateDto
? $this->roomChoiceLoaderFactory->create(
$bookingDto->participants,
$bookingDto->getSelectedRooms(),
$participantIndex
),
)
: null,
];
// Future field providers would be added here, for example:
+53 -12
View File
@@ -6,7 +6,6 @@ use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\RoomSelectionDto;
use App\Service\TravelDataService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -14,8 +13,7 @@ class BookingService
{
public function __construct(
private readonly TravelDataService $travelDataService,
) {
}
) {}
public function getOrCreateBookingCreateDto(Request $request): BookingCreateDto
{
@@ -103,7 +101,6 @@ class BookingService
return $participantsCount;
}
/**
* Calculates the number of participants assigned to each room ID.
*
@@ -124,7 +121,6 @@ class BookingService
/**
* Returns a summary of selected rooms and the resulting participant count for a booking.
*
* @param BookingCreateDto $bookingCreateDto
* @return array{selectedRooms: array, participantCount: int}
*/
public function getRoomSummaryAndParticipantCount(BookingCreateDto $bookingCreateDto): array
@@ -142,19 +138,20 @@ class BookingService
* Groups available rooms by selection type ('by_pax' or 'by_room').
*
* @param array<int, Room> $rooms Rooms indexed by room ID
*
* @return array{by_pax: array<int, Room>, by_room: array<int, Room>}
*/
public function groupRoomsBySelectionType(array $rooms): array
{
$groups = [
'by_pax' => [],
'by_room' => [],
Room::SELECTION_TYPE_BY_PAX => [],
Room::SELECTION_TYPE_BY_ROOM => [],
];
foreach ($rooms as $room) {
if (stripos($room->label, 'bett') !== false) {
$groups['by_pax'][$room->id] = $room;
if (false !== stripos($room->label, 'bett')) {
$groups[Room::SELECTION_TYPE_BY_PAX][$room->id] = $room;
} else {
$groups['by_room'][$room->id] = $room;
$groups[Room::SELECTION_TYPE_BY_ROOM][$room->id] = $room;
}
}
@@ -166,13 +163,14 @@ class BookingService
*
* @param array $roomSelections Array of selected RoomSelectionDto
* @param array<int, Room> $roomsById Rooms indexed by room ID
*
* @return array{by_pax: array, by_room: array}
*/
public function groupRoomSelectionsByType(array $roomSelections, array $roomsById): array
{
$groups = [
'by_pax' => [],
'by_room' => [],
Room::SELECTION_TYPE_BY_PAX => [],
Room::SELECTION_TYPE_BY_ROOM => [],
];
foreach ($roomSelections as $roomSelection) {
$room = $roomsById[$roomSelection->roomId] ?? null;
@@ -184,4 +182,47 @@ class BookingService
return $groups;
}
/**
* Determines if the room selection has changed between two DTOs.
*/
public function shouldResetAssignments(BookingCreateDto $oldDto, BookingCreateDto $newDto): bool
{
$old = array_map(fn($roomSelectionDto) => [$roomSelectionDto->roomId, $roomSelectionDto->quantity], $oldDto->roomSelections);
$new = array_map(fn($roomSelectionDto) => [$roomSelectionDto->roomId, $roomSelectionDto->quantity], $newDto->roomSelections);
return $old !== $new;
}
/**
* Resets all participant room assignments in the DTO.
*/
public function resetParticipantAssignments(BookingCreateDto $dto): void
{
foreach ($dto->participants as $participant) {
$participant->assignedRoomId = null;
}
}
/**
* Creates a snapshot of the current room selection state.
*
* @return array<int, array{int, int}> Array of [roomId, quantity] pairs
*/
public function createRoomSelectionSnapshot(BookingCreateDto $dto): array
{
return array_map(
fn($roomSelection) => [$roomSelection->roomId, $roomSelection->quantity],
$dto->roomSelections
);
}
/**
* Checks if room selection has changed compared to a previous snapshot.
*/
public function hasRoomSelectionChanged(array $oldSnapshot, BookingCreateDto $newDto): bool
{
$newSnapshot = $this->createRoomSelectionSnapshot($newDto);
return $oldSnapshot !== $newSnapshot;
}
}