From 3f94c51bc8b1ef22e51a04e5b9b66ae22ca30754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 24 Jul 2025 11:12:06 +0200 Subject: [PATCH] wip: refactor --- .../Booking/CreateStep1Controller.php | 2 +- .../Booking/CreateStep2Controller.php | 2 +- src/Form/BookingCreateParticipantType.php | 31 ++--- src/Form/BookingEditParticipantType.php | 123 +++++++++++------ src/Form/Model/BookingDtoInterface.php | 7 + src/Form/Model/BookingEditDto.php | 10 ++ .../AbstractParticipantFieldHandler.php | 15 ++- .../Condition/AgeRangeCondition.php | 8 +- .../Condition/ApplicantCondition.php | 44 ++++++ .../Condition/CompositeCondition.php | 12 +- .../Condition/FieldConditionInterface.php | 5 +- .../Condition/FieldValueCondition.php | 10 +- .../Condition/MutabilityCondition.php | 48 +++++++ .../ParticipantAssignedRoomFieldHandler.php | 5 +- .../ParticipantFieldHandlerInterface.php | 10 +- .../ParticipantFieldHandlerRegistry.php | 5 +- src/Form/Service/EditFieldStateProvider.php | 126 ++++++++++++++++++ .../Service/FieldStateProviderInterface.php | 10 +- .../ParticipantFieldOptionsProvider.php | 20 +-- templates/booking/create_step_1.html.twig | 6 +- templates/booking/create_step_2.html.twig | 6 +- 21 files changed, 395 insertions(+), 110 deletions(-) create mode 100644 src/Form/ParticipantFieldHandler/Condition/ApplicantCondition.php create mode 100644 src/Form/ParticipantFieldHandler/Condition/MutabilityCondition.php create mode 100644 src/Form/Service/EditFieldStateProvider.php diff --git a/src/Controller/Booking/CreateStep1Controller.php b/src/Controller/Booking/CreateStep1Controller.php index a98a32b..5043a97 100644 --- a/src/Controller/Booking/CreateStep1Controller.php +++ b/src/Controller/Booking/CreateStep1Controller.php @@ -33,7 +33,7 @@ class CreateStep1Controller extends AbstractController public function index(Request $request): Response { $bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request); - $participantsCount = $this->bookingService->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travelData); + $participantsCount = $this->bookingService->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel); // Validate step access - allow step 1 or redirect to current step $this->validateStepAccess($bookingCreateDto, 1); diff --git a/src/Controller/Booking/CreateStep2Controller.php b/src/Controller/Booking/CreateStep2Controller.php index 22265ea..18b9b47 100644 --- a/src/Controller/Booking/CreateStep2Controller.php +++ b/src/Controller/Booking/CreateStep2Controller.php @@ -112,7 +112,7 @@ class CreateStep2Controller extends AbstractController { return $this ->bookingService - ->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travelData); + ->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel); } private function ensureCorrectNumberOfParticipants(BookingCreateDto $bookingCreateDto): void diff --git a/src/Form/BookingCreateParticipantType.php b/src/Form/BookingCreateParticipantType.php index 5c6c825..6213c9b 100644 --- a/src/Form/BookingCreateParticipantType.php +++ b/src/Form/BookingCreateParticipantType.php @@ -5,6 +5,7 @@ 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\Service\ParticipantFieldOptionsProvider; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\BirthdayType; @@ -90,11 +91,11 @@ class BookingCreateParticipantType extends AbstractType $rootForm = $rootForm->getParent(); } - /** @var BookingCreateDto $bookingCreateDto */ - $bookingCreateDto = $rootForm->getData(); + /** @var BookingDtoInterface $bookingDto */ + $bookingDto = $rootForm->getData(); - $this->addDynamicFields($form, $bookingCreateDto, $participantData->index); - $this->applyFieldStates($form, $bookingCreateDto, $participantData->index); + $this->addDynamicFields($form, $bookingDto, $participantData->index); + $this->applyFieldStates($form, $bookingDto, $participantData->index); } /** @@ -109,16 +110,16 @@ class BookingCreateParticipantType extends AbstractType return; } - // Get the root form data to access BookingCreateDto + // Get the root form data to access BookingDtoInterface $rootForm = $form; while ($rootForm->getParent()) { $rootForm = $rootForm->getParent(); } - /** @var BookingCreateDto $bookingCreateDto */ - $bookingCreateDto = $rootForm->getData(); + /** @var BookingDtoInterface $bookingDto */ + $bookingDto = $rootForm->getData(); - if (null === $bookingCreateDto) { + if (null === $bookingDto) { return; } @@ -129,7 +130,7 @@ class BookingCreateParticipantType extends AbstractType } // Apply updated field states based on submitted data - $this->applyFieldStates($form, $bookingCreateDto, $participantData->index, $submittedData); + $this->applyFieldStates($form, $bookingDto, $participantData->index, $submittedData); } /** @@ -139,14 +140,14 @@ class BookingCreateParticipantType extends AbstractType * the appropriate state modifications (readonly, disabled, etc.). * * @param FormInterface $form The form to modify - * @param BookingCreateDto $bookingCreateDto The booking data for context + * @param BookingDtoInterface $bookingDto The booking data for context (create or edit) * @param int $participantIndex The participant index * @param array $formData Optional submitted form data for state calculation */ - private function applyFieldStates(FormInterface $form, BookingCreateDto $bookingCreateDto, int $participantIndex, array $formData = []): void + private function applyFieldStates(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): void { // Get all fields that have state conditions - $allFieldStates = $this->fieldOptionsProvider->getAllFieldStates($bookingCreateDto, $participantIndex, $formData); + $allFieldStates = $this->fieldOptionsProvider->getAllFieldStates($bookingDto, $participantIndex, $formData); foreach ($allFieldStates as $fieldName => $fieldState) { if ($form->has($fieldName)) { @@ -167,18 +168,18 @@ class BookingCreateParticipantType extends AbstractType /** * Adds all configured dynamic fields to the form with state conditions applied. */ - private function addDynamicFields(FormInterface $form, BookingCreateDto $bookingCreateDto, int $participantIndex): void + private function addDynamicFields(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex): void { $dynamicFields = ['assignedRoomId']; // List of fields that need dynamic configuration foreach ($dynamicFields as $fieldName) { if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) { // Get base field options - $fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingCreateDto, $participantIndex); + $fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex); // Apply dynamic field state if conditions exist if ($this->fieldOptionsProvider->hasStateConditions($fieldName)) { - $fieldState = $this->fieldOptionsProvider->getFieldState($fieldName, $bookingCreateDto, $participantIndex); + $fieldState = $this->fieldOptionsProvider->getFieldState($fieldName, $bookingDto, $participantIndex); $fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState); } diff --git a/src/Form/BookingEditParticipantType.php b/src/Form/BookingEditParticipantType.php index 4470fee..f6ffa82 100644 --- a/src/Form/BookingEditParticipantType.php +++ b/src/Form/BookingEditParticipantType.php @@ -7,6 +7,8 @@ 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; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -19,6 +21,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class BookingEditParticipantType extends AbstractType { + public function __construct( + private readonly EditFieldStateProvider $fieldStateProvider, + ) {} + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) { @@ -33,34 +39,33 @@ class BookingEditParticipantType extends AbstractType $form = $event->getForm(); - $personalDataMutable = $options['personal_data_mutable'] && $participantData->mutable; - $isApplicant = 0 === $participantIndex; + // Traverse up the form tree to get the root form's data (BookingEditDto) + $rootForm = $form; + while ($rootForm->getParent()) { + $rootForm = $rootForm->getParent(); + } + /** @var BookingDtoInterface $bookingDto */ + $bookingDto = $rootForm->getData(); + + // Helper to get state for a field + $getState = fn(string $field) => $this->fieldStateProvider->getFieldState($field, $bookingDto, $participantIndex); $form - ->add('firstName', TextType::class, [ + ->add('firstName', TextType::class, $this->mergeFieldState([ 'label' => 'Vorname', - 'attr' => [ - 'readonly' => false === $personalDataMutable, - ], 'clean_xss' => true, - ]) - ->add('lastName', TextType::class, [ + ], $getState('firstName'))) + ->add('lastName', TextType::class, $this->mergeFieldState([ 'label' => 'Nachname', - 'attr' => [ - 'readonly' => false === $personalDataMutable, - ], 'clean_xss' => true, - ]) - ->add('dateOfBirth', BirthdayType::class, [ + ], $getState('lastName'))) + ->add('dateOfBirth', BirthdayType::class, $this->mergeFieldState([ 'label' => 'Geburtsdatum', 'html5' => true, 'widget' => 'single_text', 'input' => 'datetime_immutable', - 'attr' => [ - 'readonly' => false === $personalDataMutable, - ], - ]) - ->add('gender', ChoiceType::class, [ + ], $getState('dateOfBirth'))) + ->add('gender', ChoiceType::class, $this->mergeFieldState([ 'label' => 'Geschlecht', 'required' => false, 'placeholder' => 'keine Angabe', @@ -69,36 +74,22 @@ class BookingEditParticipantType extends AbstractType 'weiblich' => 'W', 'divers' => 'D', ], - 'attr' => [ - 'readonly' => false === $personalDataMutable, - 'style' => false === $personalDataMutable ? 'pointer-events: none' : null, - ], - ]) - ->add('nationality', CountryType::class, [ + ], $getState('gender'))) + ->add('nationality', CountryType::class, $this->mergeFieldState([ 'label' => 'Nationalität', 'property' => 'nationality', 'preferred_choices' => ['D', 'A', 'CH'], - 'attr' => [ - 'readonly' => false === $personalDataMutable, - 'style' => false === $personalDataMutable ? 'pointer-events: none' : null, - ], - ]) - ->add('email', EmailType::class, [ + ], $getState('nationality'))) + ->add('email', EmailType::class, $this->mergeFieldState([ 'label' => 'E-Mail', 'required' => false, - 'attr' => [ - 'readonly' => true === $isApplicant, - ], 'clean_xss' => true, - ]) - ->add('mobile', TextType::class, [ + ], $getState('email'))) + ->add('mobile', TextType::class, $this->mergeFieldState([ 'label' => 'Telefon (mobil)', 'required' => false, - 'attr' => [ - 'readonly' => true === $isApplicant, - ], 'clean_xss' => true, - ]) + ], $getState('mobile'))) ->add('bodyDimensions', BodyDimensionsType::class); $commonChoiceFieldOptions = [ @@ -294,6 +285,33 @@ 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(); + } + /** @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); + + // Re-apply field states to all personal data fields + $personalDataFields = [ + 'firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile' + ]; + foreach ($personalDataFields as $field) { + if ($form->has($field)) { + $fieldConfig = $form->get($field)->getConfig(); + $currentOptions = $fieldConfig->getOptions(); + $updatedOptions = $this->mergeFieldState($currentOptions, $getState($field)); + $fieldType = $fieldConfig->getType()->getInnerType(); + $form->remove($field); + $form->add($field, $fieldType::class, $updatedOptions); + } + } + $transportationId = $data['transportationServiceTo'] ?? null; $transportation = $options['travel']->pickups[$transportationId] ?? null; @@ -344,6 +362,33 @@ class BookingEditParticipantType extends AbstractType 'transportation_services_mutable' => true, 'pickups_mutable' => true, 'applicant_id' => null, + // Booking DTO for field state provider (must be set by parent form) + 'booking' => null, ]); } + + /** + * Merges field state modifications into existing field options. + * + * This method combines the base field options with dynamic state modifications, + * handling attribute merging and option overrides correctly. + * + * @param array $fieldOptions The base field options + * @param array $fieldState The dynamic state modifications + * + * @return array The merged field options with state applied + */ + private function mergeFieldState(array $fieldOptions, array $fieldState): array + { + foreach ($fieldState as $key => $value) { + if ('attr' === $key && isset($fieldOptions['attr'])) { + // Merge attributes instead of overwriting + $fieldOptions['attr'] = array_merge($fieldOptions['attr'], $value); + } else { + // Direct assignment for non-attribute options + $fieldOptions[$key] = $value; + } + } + return $fieldOptions; + } } diff --git a/src/Form/Model/BookingDtoInterface.php b/src/Form/Model/BookingDtoInterface.php index 821fc58..6369a05 100644 --- a/src/Form/Model/BookingDtoInterface.php +++ b/src/Form/Model/BookingDtoInterface.php @@ -47,4 +47,11 @@ 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 Array of selected room DTOs (may be empty for edit DTOs) + */ + public function getSelectedRooms(): array; } \ No newline at end of file diff --git a/src/Form/Model/BookingEditDto.php b/src/Form/Model/BookingEditDto.php index c9d2413..443b2ba 100644 --- a/src/Form/Model/BookingEditDto.php +++ b/src/Form/Model/BookingEditDto.php @@ -83,4 +83,14 @@ class BookingEditDto implements BookingDtoInterface { return $this->participants[$index] ?? null; } + + /** + * Gets all selected rooms for the booking (edit context). + * + * @return array Always returns an empty array for edit DTOs unless implemented. + */ + public function getSelectedRooms(): array + { + return []; + } } diff --git a/src/Form/ParticipantFieldHandler/AbstractParticipantFieldHandler.php b/src/Form/ParticipantFieldHandler/AbstractParticipantFieldHandler.php index 19b52a2..aec5f14 100644 --- a/src/Form/ParticipantFieldHandler/AbstractParticipantFieldHandler.php +++ b/src/Form/ParticipantFieldHandler/AbstractParticipantFieldHandler.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Form\ParticipantFieldHandler; -use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Abstract base class providing common functionality for participant field handlers. @@ -54,14 +54,15 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle * if the participant exists at the given index. Returns null if the * participant doesn't exist, preventing array access errors. * - * @param BookingCreateDto $bookingDto The booking DTO containing participants - * @param int $participantIndex The index of the participant to retrieve + * @param BookingDtoInterface $bookingDto The booking DTO containing participants (create or edit) + * @param int $participantIndex The index of the participant to retrieve * * @return object|null The participant object, or null if not found */ - protected function getParticipant(BookingCreateDto $bookingDto, int $participantIndex): ?object + protected function getParticipant(BookingDtoInterface $bookingDto, int $participantIndex): ?object { - return $bookingDto->participants[$participantIndex] ?? null; + $participants = $bookingDto->getParticipants(); + return $participants[$participantIndex] ?? null; } /** @@ -126,12 +127,12 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle * processing results. * * @param array $submittedData The submitted participant form data - * @param BookingCreateDto $bookingDto The booking DTO (potentially modified by processing) + * @param BookingDtoInterface $bookingDto The booking DTO (potentially modified by processing) * @param int $participantIndex The participant index being processed * * @return array> Empty array (no state modifications by default) */ - public function getFieldStateModifications(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): array + public function getFieldStateModifications(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): array { return []; } diff --git a/src/Form/ParticipantFieldHandler/Condition/AgeRangeCondition.php b/src/Form/ParticipantFieldHandler/Condition/AgeRangeCondition.php index 37264ee..856d0a4 100644 --- a/src/Form/ParticipantFieldHandler/Condition/AgeRangeCondition.php +++ b/src/Form/ParticipantFieldHandler/Condition/AgeRangeCondition.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Form\ParticipantFieldHandler\Condition; -use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Condition that evaluates participant age against specified range criteria. @@ -53,15 +53,15 @@ class AgeRangeCondition implements FieldConditionInterface * checks if it falls within the configured age range. Returns false if * the participant has no date of birth set. * - * @param BookingCreateDto $bookingDto The current booking data + * @param BookingDtoInterface $bookingDto The current booking data (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data (unused for age conditions) * * @return bool True if the participant's age meets the criteria, false otherwise */ - public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool + public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool { - $participant = $bookingDto->participants[$participantIndex] ?? null; + $participant = $bookingDto->getParticipant($participantIndex); if (null === $participant || null === $participant->dateOfBirth) { return false; diff --git a/src/Form/ParticipantFieldHandler/Condition/ApplicantCondition.php b/src/Form/ParticipantFieldHandler/Condition/ApplicantCondition.php new file mode 100644 index 0000000..5d7523d --- /dev/null +++ b/src/Form/ParticipantFieldHandler/Condition/ApplicantCondition.php @@ -0,0 +1,44 @@ + $formData Current form data (unused) + * + * @return bool True if the participant is the applicant, false otherwise + */ + public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool + { + // Default: applicant is the first participant (index 0) + return $participantIndex === 0; + } + + public function getDependentFields(): array + { + return []; + } + + public function getDescription(): string + { + return 'Checks if the participant is the applicant (index 0)'; + } +} \ No newline at end of file diff --git a/src/Form/ParticipantFieldHandler/Condition/CompositeCondition.php b/src/Form/ParticipantFieldHandler/Condition/CompositeCondition.php index a7ba1ae..2b5a1dc 100644 --- a/src/Form/ParticipantFieldHandler/Condition/CompositeCondition.php +++ b/src/Form/ParticipantFieldHandler/Condition/CompositeCondition.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Form\ParticipantFieldHandler\Condition; -use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Composite condition that combines multiple conditions with logical operators. @@ -63,13 +63,13 @@ class CompositeCondition implements FieldConditionInterface * evaluation for optimal performance. The evaluation stops as soon as the * final result can be determined. * - * @param BookingCreateDto $bookingDto The current booking data + * @param BookingDtoInterface $bookingDto The current booking data (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data for condition evaluation * * @return bool True if the composite condition is met, false otherwise */ - public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool + public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool { return match ($this->operator) { self::OPERATOR_AND => $this->evaluateAnd($bookingDto, $participantIndex, $formData), @@ -167,7 +167,7 @@ class CompositeCondition implements FieldConditionInterface * Returns false as soon as any condition evaluates to false, * avoiding unnecessary evaluation of remaining conditions. */ - private function evaluateAnd(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool + private function evaluateAnd(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool { foreach ($this->conditions as $condition) { if (!$condition->evaluate($bookingDto, $participantIndex, $formData)) { @@ -184,7 +184,7 @@ class CompositeCondition implements FieldConditionInterface * Returns true as soon as any condition evaluates to true, * avoiding unnecessary evaluation of remaining conditions. */ - private function evaluateOr(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool + private function evaluateOr(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool { foreach ($this->conditions as $condition) { if ($condition->evaluate($bookingDto, $participantIndex, $formData)) { @@ -198,7 +198,7 @@ class CompositeCondition implements FieldConditionInterface /** * Evaluates NOT logic by inverting the result of the single condition. */ - private function evaluateNot(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool + private function evaluateNot(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool { return !$this->conditions[0]->evaluate($bookingDto, $participantIndex, $formData); } diff --git a/src/Form/ParticipantFieldHandler/Condition/FieldConditionInterface.php b/src/Form/ParticipantFieldHandler/Condition/FieldConditionInterface.php index 5cae08c..e9710c4 100644 --- a/src/Form/ParticipantFieldHandler/Condition/FieldConditionInterface.php +++ b/src/Form/ParticipantFieldHandler/Condition/FieldConditionInterface.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Form\ParticipantFieldHandler\Condition; use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Interface for evaluating field state conditions. @@ -29,13 +30,13 @@ interface FieldConditionInterface * state of the booking, participant data, and submitted form values. * The result is used to determine field state (enabled/disabled/readonly). * - * @param BookingCreateDto $bookingDto The current booking data + * @param BookingDtoInterface $bookingDto The current booking data (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data (may include partial submissions) * * @return bool True if the condition is met, false otherwise */ - public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool; + public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool; /** * Returns field names that trigger re-evaluation of this condition. diff --git a/src/Form/ParticipantFieldHandler/Condition/FieldValueCondition.php b/src/Form/ParticipantFieldHandler/Condition/FieldValueCondition.php index 42943ff..66c7fb9 100644 --- a/src/Form/ParticipantFieldHandler/Condition/FieldValueCondition.php +++ b/src/Form/ParticipantFieldHandler/Condition/FieldValueCondition.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Form\ParticipantFieldHandler\Condition; -use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Condition that evaluates field states based on other field values. @@ -59,13 +59,13 @@ class FieldValueCondition implements FieldConditionInterface * against the expected value using the configured operator. Supports * both participant-level fields and booking-level fields. * - * @param BookingCreateDto $bookingDto The current booking data + * @param BookingDtoInterface $bookingDto The current booking data (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data for condition evaluation * * @return bool True if the field value meets the condition criteria, false otherwise */ - public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool + public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool { $fieldValue = $this->getFieldValue($formData, $participantIndex, $bookingDto); @@ -174,7 +174,7 @@ class FieldValueCondition implements FieldConditionInterface /** * Retrieves field value from form data or participant data. */ - private function getFieldValue(array $formData, int $participantIndex, BookingCreateDto $bookingDto): mixed + private function getFieldValue(array $formData, int $participantIndex, BookingDtoInterface $bookingDto): mixed { // First check participant-specific form data if (isset($formData['participants'][$participantIndex][$this->fieldName])) { @@ -182,7 +182,7 @@ class FieldValueCondition implements FieldConditionInterface } // Then check participant DTO data - $participant = $bookingDto->participants[$participantIndex] ?? null; + $participant = $bookingDto->getParticipant($participantIndex); if (null !== $participant && property_exists($participant, $this->fieldName)) { return $participant->{$this->fieldName}; } diff --git a/src/Form/ParticipantFieldHandler/Condition/MutabilityCondition.php b/src/Form/ParticipantFieldHandler/Condition/MutabilityCondition.php new file mode 100644 index 0000000..6dde404 --- /dev/null +++ b/src/Form/ParticipantFieldHandler/Condition/MutabilityCondition.php @@ -0,0 +1,48 @@ + $formData Current form data (unused) + * + * @return bool True if the participant's data is mutable, false otherwise + */ + public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool + { + $participant = $bookingDto->getParticipant($participantIndex); + if (null === $participant) { + return false; + } + + return $participant->mutable; + } + + public function getDependentFields(): array + { + return []; + } + + public function getDescription(): string + { + return 'Checks if the participant\'s personal data is mutable (edit flow)'; + } +} \ No newline at end of file diff --git a/src/Form/ParticipantFieldHandler/ParticipantAssignedRoomFieldHandler.php b/src/Form/ParticipantFieldHandler/ParticipantAssignedRoomFieldHandler.php index 6dfc88a..d0566c2 100644 --- a/src/Form/ParticipantFieldHandler/ParticipantAssignedRoomFieldHandler.php +++ b/src/Form/ParticipantFieldHandler/ParticipantAssignedRoomFieldHandler.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Form\ParticipantFieldHandler; use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Handles processing of the assignedRoomId field for booking participants. @@ -52,10 +53,10 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle * 4. Updates the participant's assignedRoomId property * * @param array $submittedData The submitted participant form data - * @param BookingCreateDto $bookingDto The booking DTO to update + * @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit) * @param int $participantIndex The index of the participant being processed */ - public function processField(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): void + public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void { // Safely get the participant object, returning early if not found $participant = $this->getParticipant($bookingDto, $participantIndex); diff --git a/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerInterface.php b/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerInterface.php index f8a1194..a34d344 100644 --- a/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerInterface.php +++ b/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerInterface.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Form\ParticipantFieldHandler; -use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Interface for handling dynamic participant form field processing and state modification. @@ -32,10 +32,10 @@ interface ParticipantFieldHandlerInterface * Processes the participant field data from submitted form data and updates the DTO. * * @param array $submittedData The submitted participant form data - * @param BookingCreateDto $bookingDto The booking DTO to update + * @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit) * @param int $participantIndex The participant index being processed */ - public function processField(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): void; + public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void; /** * Determines if this handler should process the field based on submitted participant data. @@ -50,12 +50,12 @@ interface ParticipantFieldHandlerInterface * to enable/disable/hide fields based on the handler's processing results. * * @param array $submittedData The submitted participant form data - * @param BookingCreateDto $bookingDto The booking DTO (potentially modified by processing) + * @param BookingDtoInterface $bookingDto The booking DTO (potentially modified by processing) * @param int $participantIndex The participant index being processed * * @return array> Field state modifications indexed by field name */ - public function getFieldStateModifications(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): array; + public function getFieldStateModifications(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): array; /** * Returns field names whose state is affected by this handler's processing. diff --git a/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerRegistry.php b/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerRegistry.php index 75e43bc..9ffa615 100644 --- a/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerRegistry.php +++ b/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerRegistry.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Form\ParticipantFieldHandler; use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Registry for managing and executing participant field handlers in dependency order. @@ -70,9 +71,9 @@ class ParticipantFieldHandlerRegistry * participant's data and updates the booking DTO accordingly. * * @param array $submittedData The submitted form data containing participants array - * @param BookingCreateDto $bookingDto The booking DTO to update with processed field values + * @param BookingDtoInterface $bookingDto The booking DTO to update with processed field values (create or edit) */ - public function processFields(array $submittedData, BookingCreateDto $bookingDto): void + public function processFields(array $submittedData, BookingDtoInterface $bookingDto): void { // Early return if no participant data exists in submission if (!isset($submittedData['participants']) || !is_array($submittedData['participants'])) { diff --git a/src/Form/Service/EditFieldStateProvider.php b/src/Form/Service/EditFieldStateProvider.php new file mode 100644 index 0000000..9d73d22 --- /dev/null +++ b/src/Form/Service/EditFieldStateProvider.php @@ -0,0 +1,126 @@ +> */ + private array $fieldStateConditions = []; + + public function __construct() + { + $this->registerFieldStateConditions(); + } + + /** + * Registers field state conditions for the edit workflow. + * + * Add or modify conditions as needed for your domain. + */ + private function registerFieldStateConditions(): void + { + // Make all personal data fields readonly if not mutable OR if applicant + $personalDataFields = [ + 'firstName', + 'lastName', + 'dateOfBirth', + 'gender', + 'nationality', + 'email', + 'mobile', + ]; + foreach ($personalDataFields as $field) { + $this->fieldStateConditions[$field] = [ + 'readonly' => CompositeCondition::or( + new ApplicantCondition(), + new MutabilityCondition() + ), + ]; + } + + // Example: Only applicant can edit email + $this->fieldStateConditions['email']['readonly'] = new ApplicantCondition(); + } + + public function getFieldState(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array + { + if (!isset($this->fieldStateConditions[$fieldName])) { + return []; + } + + $stateModifications = []; + $attributes = []; + + foreach ($this->fieldStateConditions[$fieldName] as $stateType => $condition) { + if ($condition->evaluate($bookingDto, $participantIndex, $formData)) { + switch ($stateType) { + case 'readonly': + $attributes['readonly'] = true; + break; + case 'disabled': + $stateModifications['disabled'] = true; + break; + case 'required': + $stateModifications['required'] = true; + break; + case 'hidden': + $attributes['style'] = ($attributes['style'] ?? '').' display: none;'; + break; + } + } + } + + if (!empty($attributes)) { + $stateModifications['attr'] = $attributes; + } + + return $stateModifications; + } + + public function hasStateConditions(string $fieldName): bool + { + return isset($this->fieldStateConditions[$fieldName]) && !empty($this->fieldStateConditions[$fieldName]); + } + + public function getFieldStateDependencies(string $fieldName): array + { + if (!isset($this->fieldStateConditions[$fieldName])) { + return []; + } + + $dependencies = []; + foreach ($this->fieldStateConditions[$fieldName] as $condition) { + $dependencies = array_merge($dependencies, $condition->getDependentFields()); + } + return array_unique($dependencies); + } + + public function getAllFieldStates(BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array + { + $allStates = []; + foreach (array_keys($this->fieldStateConditions) as $fieldName) { + $fieldState = $this->getFieldState($fieldName, $bookingDto, $participantIndex, $formData); + if (!empty($fieldState)) { + $allStates[$fieldName] = $fieldState; + } + } + return $allStates; + } +} \ No newline at end of file diff --git a/src/Form/Service/FieldStateProviderInterface.php b/src/Form/Service/FieldStateProviderInterface.php index 51b99fa..21baef7 100644 --- a/src/Form/Service/FieldStateProviderInterface.php +++ b/src/Form/Service/FieldStateProviderInterface.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Form\Service; -use App\Form\Model\BookingCreateDto; +use App\Form\Model\BookingDtoInterface; /** * Interface for providing dynamic field state based on conditions. @@ -46,13 +46,13 @@ interface FieldStateProviderInterface * - 'attr' => ['class' => 'conditional-field'] - Add CSS classes * * @param string $fieldName The name of the field to evaluate - * @param BookingCreateDto $bookingDto The current booking data for context + * @param BookingDtoInterface $bookingDto The current booking data for context (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data (may include partial submissions) * * @return array Symfony form field options for state modifications, empty if no changes needed */ - public function getFieldState(string $fieldName, BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array; + public function getFieldState(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array; /** * Checks whether a field has state conditions configured. @@ -91,11 +91,11 @@ interface FieldStateProviderInterface * when multiple field states need to be determined simultaneously. It's * particularly useful during form building and bulk state updates. * - * @param BookingCreateDto $bookingDto The current booking data for context + * @param BookingDtoInterface $bookingDto The current booking data for context (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data for condition evaluation * * @return array> Field states indexed by field name */ - public function getAllFieldStates(BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array; + public function getAllFieldStates(BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array; } \ No newline at end of file diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index f738ea1..17f6553 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -4,7 +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; /** @@ -64,13 +64,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface * - 'disabled' - Whether the field should be disabled * - 'required' - Whether the field is required * - * @param string $fieldName The name of the field to configure - * @param BookingCreateDto $bookingDto The current booking data for context - * @param int $participantIndex The index of the participant being configured + * @param string $fieldName The name of the field to configure + * @param BookingDtoInterface $bookingDto The current booking data for context (create or edit) + * @param int $participantIndex The index of the participant being configured * * @return array Symfony form field options, or empty array if field not supported */ - public function getFieldOptions(string $fieldName, BookingCreateDto $bookingDto, int $participantIndex): array + public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex): array { // Check if we have a provider for this field if (!isset($this->fieldOptionProviders[$fieldName])) { @@ -121,7 +121,7 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface private function registerFieldOptionProviders(): void { // Room assignment field provider - $this->fieldOptionProviders['assignedRoomId'] = fn (BookingCreateDto $bookingDto, int $participantIndex) => [ + $this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Zimmer', 'placeholder' => 'Bitte wählen', // Use factory to create context-aware choice loader that: @@ -155,13 +155,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface * disabled, etc.) and evaluated independently. * * @param string $fieldName The name of the field to evaluate - * @param BookingCreateDto $bookingDto The current booking data for context + * @param BookingDtoInterface $bookingDto The current booking data for context (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data for condition evaluation * * @return array Symfony form field options for state modifications */ - public function getFieldState(string $fieldName, BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array + public function getFieldState(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array { if (!isset($this->fieldStateConditions[$fieldName])) { return []; @@ -233,13 +233,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface /** * Calculates field states for all configured fields at once. * - * @param BookingCreateDto $bookingDto The current booking data for context + * @param BookingDtoInterface $bookingDto The current booking data for context (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data for condition evaluation * * @return array> Field states indexed by field name */ - public function getAllFieldStates(BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array + public function getAllFieldStates(BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array { $allStates = []; diff --git a/templates/booking/create_step_1.html.twig b/templates/booking/create_step_1.html.twig index cd93a8d..3802763 100644 --- a/templates/booking/create_step_1.html.twig +++ b/templates/booking/create_step_1.html.twig @@ -16,9 +16,9 @@

Zusammenfassung

-

Reise: {{ bookingCreateDto.travelData.label }}

-

Datum: {{ bookingCreateDto.travelData.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travelData.dateTo | date('d.m.Y') }}

-

Hotel: {{ bookingCreateDto.travelData.hotel.name }}

+

Reise: {{ bookingCreateDto.travel.label }}

+

Datum: {{ bookingCreateDto.travel.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo | date('d.m.Y') }}

+

Hotel: {{ bookingCreateDto.travel.hotel.name }}

{% if participantsCount > 0 %}

Teilnehmerzahl: {{ participantsCount }}

{% endif %} diff --git a/templates/booking/create_step_2.html.twig b/templates/booking/create_step_2.html.twig index be407ee..47e90f7 100644 --- a/templates/booking/create_step_2.html.twig +++ b/templates/booking/create_step_2.html.twig @@ -71,19 +71,19 @@ Reise

- {{ bookingCreateDto.travelData.label }} + {{ bookingCreateDto.travel.label }}

Datum

- {{ bookingCreateDto.travelData.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travelData.dateTo | date('d.m.Y') }} + {{ bookingCreateDto.travel.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo | date('d.m.Y') }}

Unterkunft

- {{ bookingCreateDto.travelData.hotel.name }} + {{ bookingCreateDto.travel.hotel.name }}

{% if participantsCount > 0 %}