From 7003c0d81bf1ad57b4e76d52e040033fecb6de0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 27 Aug 2025 15:02:45 +0200 Subject: [PATCH] wip: age based filtering of options --- config/services.yaml | 10 +- src/BusProNet/Model/AgeConstraintResult.php | 4 +- .../AgeConstraintParserInterface.php | 2 +- .../XmlParser/AgeConstraintParserRegistry.php | 2 +- .../XmlParser/BirthYearConstraintParser.php | 4 +- src/BusProNet/XmlParser/TravelParser.php | 1 + .../Booking/CreateStep2Controller.php | 2 +- src/Form/BookingCreateStep2Type.php | 5 +- .../DateOfBirthProvidedCondition.php | 2 +- ...ticipantAdditionalServicesFieldHandler.php | 223 ++++++++++++++++++ .../Service/ParticipantBoardFieldHandler.php | 122 ++++++++++ .../ParticipantCoursesFieldHandler.php | 202 ++++++++++++++++ .../ParticipantDateOfBirthFieldHandler.php | 2 +- .../ParticipantFieldHandlerRegistry.php | 149 ++++++++++++ .../ParticipantFieldOptionsProvider.php | 1 - .../ParticipantRentalsFieldHandler.php | 122 ++++++++++ src/Form/Service/ServiceAgeEvaluator.php | 15 +- 17 files changed, 845 insertions(+), 23 deletions(-) create mode 100644 src/Form/Service/ParticipantAdditionalServicesFieldHandler.php create mode 100644 src/Form/Service/ParticipantBoardFieldHandler.php create mode 100644 src/Form/Service/ParticipantCoursesFieldHandler.php create mode 100644 src/Form/Service/ParticipantRentalsFieldHandler.php diff --git a/config/services.yaml b/config/services.yaml index 370340d..3937eb4 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -67,12 +67,14 @@ services: arguments: $choiceListFactory: '@form.choice_list_factory.default' - # Participant Field Handler Registry with hybrid handler configuration + # Participant Field Handler Registry - all handlers now instantiate dependencies directly App\Form\Service\ParticipantFieldHandlerRegistry: arguments: $handlers: - # Simple handlers (no dependencies) - use class names + # All handlers now have no external dependencies and can be instantiated directly - 'App\Form\Service\ParticipantDateOfBirthFieldHandler' - 'App\Form\Service\ParticipantAssignedRoomFieldHandler' - # Complex handlers (with dependencies) would use service references like: - # - '@participant.complex.handler' + - 'App\Form\Service\ParticipantAdditionalServicesFieldHandler' + - 'App\Form\Service\ParticipantCoursesFieldHandler' + - 'App\Form\Service\ParticipantBoardFieldHandler' + - 'App\Form\Service\ParticipantRentalsFieldHandler' diff --git a/src/BusProNet/Model/AgeConstraintResult.php b/src/BusProNet/Model/AgeConstraintResult.php index 0a308fb..e508837 100644 --- a/src/BusProNet/Model/AgeConstraintResult.php +++ b/src/BusProNet/Model/AgeConstraintResult.php @@ -19,7 +19,7 @@ class AgeConstraintResult public readonly ?int $birthYearFrom = null, public readonly ?int $birthYearTo = null, public readonly array $metadata = [], - public readonly ?string $rawData = null + public readonly ?string $rawData = null, ) { } @@ -37,4 +37,4 @@ class AgeConstraintResult { return false === $this->hasAgeConstraints() && false === $this->hasBirthYearConstraints(); } -} \ No newline at end of file +} diff --git a/src/BusProNet/XmlParser/AgeConstraintParserInterface.php b/src/BusProNet/XmlParser/AgeConstraintParserInterface.php index 93764ef..847285e 100644 --- a/src/BusProNet/XmlParser/AgeConstraintParserInterface.php +++ b/src/BusProNet/XmlParser/AgeConstraintParserInterface.php @@ -42,4 +42,4 @@ interface AgeConstraintParserInterface * @return string The constraint type identifier (e.g., 'birth_year', 'grade_level') */ public function getConstraintType(): string; -} \ No newline at end of file +} diff --git a/src/BusProNet/XmlParser/AgeConstraintParserRegistry.php b/src/BusProNet/XmlParser/AgeConstraintParserRegistry.php index 744fe57..bdd19ad 100644 --- a/src/BusProNet/XmlParser/AgeConstraintParserRegistry.php +++ b/src/BusProNet/XmlParser/AgeConstraintParserRegistry.php @@ -112,4 +112,4 @@ class AgeConstraintParserRegistry return min($current, $new); // Most restrictive maximum } -} \ No newline at end of file +} diff --git a/src/BusProNet/XmlParser/BirthYearConstraintParser.php b/src/BusProNet/XmlParser/BirthYearConstraintParser.php index dfd8e9f..35e76cf 100644 --- a/src/BusProNet/XmlParser/BirthYearConstraintParser.php +++ b/src/BusProNet/XmlParser/BirthYearConstraintParser.php @@ -24,7 +24,7 @@ class BirthYearConstraintParser implements AgeConstraintParserInterface public function parse(string $constraintData): AgeConstraintResult { if (false === $this->canParse($constraintData)) { - throw new \InvalidArgumentException('Cannot parse constraint data: ' . $constraintData); + throw new \InvalidArgumentException('Cannot parse constraint data: '.$constraintData); } $yearData = substr($constraintData, strlen(self::BIRTH_YEAR_PREFIX)); @@ -64,4 +64,4 @@ class BirthYearConstraintParser implements AgeConstraintParserInterface { return 'birth_year'; } -} \ No newline at end of file +} diff --git a/src/BusProNet/XmlParser/TravelParser.php b/src/BusProNet/XmlParser/TravelParser.php index 9794e5c..4a9966b 100644 --- a/src/BusProNet/XmlParser/TravelParser.php +++ b/src/BusProNet/XmlParser/TravelParser.php @@ -28,6 +28,7 @@ class TravelParser extends AbstractParser { $this->ageConstraintRegistry = new AgeConstraintParserRegistry(); } + /** * Parse XML node into a Travel object. * diff --git a/src/Controller/Booking/CreateStep2Controller.php b/src/Controller/Booking/CreateStep2Controller.php index 8dcceb6..11dc515 100644 --- a/src/Controller/Booking/CreateStep2Controller.php +++ b/src/Controller/Booking/CreateStep2Controller.php @@ -98,7 +98,7 @@ class CreateStep2Controller extends AbstractController $availableRooms = $bookingCreateDto->travel->getAvailableRooms(); $groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms); - // The DTO is now updated with the latest selection. + // The DTO is now updated with the latest selection and submitted data has been cleaned. // We can now render the blocks with the fresh data. return $this->htmxOobResponse( 'booking/create_step_2.html.twig', diff --git a/src/Form/BookingCreateStep2Type.php b/src/Form/BookingCreateStep2Type.php index f4496b3..cc20e71 100644 --- a/src/Form/BookingCreateStep2Type.php +++ b/src/Form/BookingCreateStep2Type.php @@ -55,8 +55,9 @@ class BookingCreateStep2Type extends AbstractType /** @var BookingCreateDto $bookingDto */ $bookingDto = $form->getData(); - // Process all registered participant field handlers - $this->participantFieldHandlerRegistry->processFields($submittedData, $bookingDto); + // Process field handlers and synchronize submitted data with cleaned DTO state + $cleanedSubmittedData = $this->participantFieldHandlerRegistry->processFieldsAndSync($submittedData, $bookingDto); + $event->setData($cleanedSubmittedData); // Rebuild the 'participants' field with the updated DTO. $this->addParticipantsField($form); diff --git a/src/Form/Service/Condition/DateOfBirthProvidedCondition.php b/src/Form/Service/Condition/DateOfBirthProvidedCondition.php index 7c747a6..0ba0271 100644 --- a/src/Form/Service/Condition/DateOfBirthProvidedCondition.php +++ b/src/Form/Service/Condition/DateOfBirthProvidedCondition.php @@ -66,4 +66,4 @@ class DateOfBirthProvidedCondition implements FieldConditionInterface { return 'Participant must provide their date of birth'; } -} \ No newline at end of file +} diff --git a/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php b/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php new file mode 100644 index 0000000..4c5a6f7 --- /dev/null +++ b/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php @@ -0,0 +1,223 @@ + $submittedData The submitted participant form data + * @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, BookingDtoInterface $bookingDto, int $participantIndex): void + { + // Safely get the participant object, returning early if not found + $participant = $this->getParticipant($bookingDto, $participantIndex); + if (null === $participant) { + return; + } + + // Extract current service selections from submitted data + $selectedServices = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; + + // Get available additional services from travel data + $availableServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL); + + // Filter selections to keep only age-appropriate services + $validSelections = $this->filterValidServiceSelections( + $selectedServices, + $availableServices, + $bookingDto, + $participantIndex + ); + + // Update participant with validated selections + $participant->additionalServices = $validSelections; + } + + /** + * Filters service selections to keep only those valid for the participant's age. + * + * This method validates each selected service against the available services + * and the participant's age constraints. Services that are no longer available + * or appropriate for the participant's age are filtered out. + * + * @param array $selectedServices List of currently selected services + * @param array $availableServices List of all available additional services + * @param BookingDtoInterface $bookingDto The booking DTO for context + * @param int $participantIndex The participant index for age evaluation + * + * @return array Filtered array of valid service selections + */ + private function filterValidServiceSelections( + array $selectedServices, + array $availableServices, + BookingDtoInterface $bookingDto, + int $participantIndex, + ): array { + $validSelections = []; + + foreach ($selectedServices as $selectedService) { + if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) { + $validSelections[] = $selectedService; + } + } + + return $validSelections; + } + + /** + * Validates if a selected service is still valid for the participant. + * + * This method checks if a selected service exists in the available services + * and meets the age constraints for the current participant. + * + * @param mixed $selectedService The selected service to validate + * @param array $availableServices Array of available services + * @param BookingDtoInterface $bookingDto The booking DTO for context + * @param int $participantIndex The participant index for age evaluation + * + * @return bool True if the service is valid for the participant, false otherwise + */ + private function isServiceValidForParticipant( + mixed $selectedService, + array $availableServices, + BookingDtoInterface $bookingDto, + int $participantIndex, + ): bool { + // Find the service in available services + $service = $this->findServiceInAvailableServices($selectedService, $availableServices); + + if (null === $service) { + return false; // Service not found in available services + } + + // Check if service has age constraints + $ageEvaluator = new ServiceAgeEvaluator(); + if (!$ageEvaluator->canEvaluate($service)) { + return true; // No age restrictions, service is valid + } + + // Validate service against participant's age + return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex); + } + + /** + * Finds a selected service in the list of available services. + * + * This method handles different representations of services (objects, IDs, etc.) + * and locates the corresponding service in the available services array. + * + * @param mixed $selectedService The selected service to find + * @param array $availableServices Array of available Service objects + * + * @return Service|null The found service or null if not found + */ + private function findServiceInAvailableServices(mixed $selectedService, array $availableServices): ?Service + { + foreach ($availableServices as $availableService) { + // Handle different comparison scenarios + if ($this->servicesMatch($selectedService, $availableService)) { + return $availableService; + } + } + + return null; + } + + /** + * Determines if a selected service matches an available service. + * + * This method handles various service representation formats that might + * come from form submissions (objects, IDs, arrays, etc.). + * + * @param mixed $selectedService The selected service from form data + * @param Service $availableService The available service to compare against + * + * @return bool True if the services match, false otherwise + */ + private function servicesMatch(mixed $selectedService, Service $availableService): bool + { + // Direct object comparison + if ($selectedService === $availableService) { + return true; + } + + // ID comparison for Service objects + if ($selectedService instanceof Service) { + return $selectedService->id === $availableService->id; + } + + // ID comparison for numeric values + if (is_numeric($selectedService)) { + return (int) $selectedService === $availableService->id; + } + + // String ID comparison + if (is_string($selectedService)) { + return $selectedService === (string) $availableService->id; + } + + return false; + } +} diff --git a/src/Form/Service/ParticipantBoardFieldHandler.php b/src/Form/Service/ParticipantBoardFieldHandler.php new file mode 100644 index 0000000..54383ad --- /dev/null +++ b/src/Form/Service/ParticipantBoardFieldHandler.php @@ -0,0 +1,122 @@ +getParticipant($bookingDto, $participantIndex); + if (null === $participant) { + return; + } + + $selectedBoard = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; + $availableBoard = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD); + + $validSelections = $this->filterValidServiceSelections( + $selectedBoard, + $availableBoard, + $bookingDto, + $participantIndex + ); + + $participant->board = $validSelections; + } + + private function filterValidServiceSelections( + array $selectedServices, + array $availableServices, + BookingDtoInterface $bookingDto, + int $participantIndex, + ): array { + $validSelections = []; + + foreach ($selectedServices as $selectedService) { + if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) { + $validSelections[] = $selectedService; + } + } + + return $validSelections; + } + + private function isServiceValidForParticipant( + mixed $selectedService, + array $availableServices, + BookingDtoInterface $bookingDto, + int $participantIndex, + ): bool { + $service = $this->findServiceInAvailableServices($selectedService, $availableServices); + + if (null === $service) { + return false; + } + + $ageEvaluator = new ServiceAgeEvaluator(); + if (!$ageEvaluator->canEvaluate($service)) { + return true; + } + + return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex); + } + + private function findServiceInAvailableServices(mixed $selectedService, array $availableServices): ?Service + { + foreach ($availableServices as $availableService) { + if ($this->servicesMatch($selectedService, $availableService)) { + return $availableService; + } + } + + return null; + } + + private function servicesMatch(mixed $selectedService, Service $availableService): bool + { + if ($selectedService === $availableService) { + return true; + } + + if ($selectedService instanceof Service) { + return $selectedService->id === $availableService->id; + } + + if (is_numeric($selectedService)) { + return (int) $selectedService === $availableService->id; + } + + if (is_string($selectedService)) { + return $selectedService === (string) $availableService->id; + } + + return false; + } +} diff --git a/src/Form/Service/ParticipantCoursesFieldHandler.php b/src/Form/Service/ParticipantCoursesFieldHandler.php new file mode 100644 index 0000000..1581725 --- /dev/null +++ b/src/Form/Service/ParticipantCoursesFieldHandler.php @@ -0,0 +1,202 @@ + $submittedData The submitted participant form data + * @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, BookingDtoInterface $bookingDto, int $participantIndex): void + { + // Safely get the participant object, returning early if not found + $participant = $this->getParticipant($bookingDto, $participantIndex); + if (null === $participant) { + return; + } + + // Extract current course selections from submitted data + $selectedCourses = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; + + // Get available courses from travel data + $availableCourses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES); + + // Filter selections to keep only age-appropriate courses + $validSelections = $this->filterValidServiceSelections( + $selectedCourses, + $availableCourses, + $bookingDto, + $participantIndex + ); + + // Update participant with validated selections + $participant->courses = $validSelections; + } + + /** + * Filters course selections to keep only those valid for the participant's age. + * + * @param array $selectedServices List of currently selected courses + * @param array $availableServices List of all available courses + * @param BookingDtoInterface $bookingDto The booking DTO for context + * @param int $participantIndex The participant index for age evaluation + * + * @return array Filtered array of valid course selections + */ + private function filterValidServiceSelections( + array $selectedServices, + array $availableServices, + BookingDtoInterface $bookingDto, + int $participantIndex, + ): array { + $validSelections = []; + + foreach ($selectedServices as $selectedService) { + if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) { + $validSelections[] = $selectedService; + } + } + + return $validSelections; + } + + /** + * Validates if a selected course is still valid for the participant. + * + * @param mixed $selectedService The selected course to validate + * @param array $availableServices Array of available courses + * @param BookingDtoInterface $bookingDto The booking DTO for context + * @param int $participantIndex The participant index for age evaluation + * + * @return bool True if the course is valid for the participant, false otherwise + */ + private function isServiceValidForParticipant( + mixed $selectedService, + array $availableServices, + BookingDtoInterface $bookingDto, + int $participantIndex, + ): bool { + // Find the service in available services + $service = $this->findServiceInAvailableServices($selectedService, $availableServices); + + if (null === $service) { + return false; // Service not found in available services + } + + // Check if service has age constraints + $ageEvaluator = new ServiceAgeEvaluator(); + if (!$ageEvaluator->canEvaluate($service)) { + return true; // No age restrictions, service is valid + } + + // Validate service against participant's age + return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex); + } + + /** + * Finds a selected course in the list of available courses. + * + * @param mixed $selectedService The selected course to find + * @param array $availableServices Array of available Service objects + * + * @return Service|null The found service or null if not found + */ + private function findServiceInAvailableServices(mixed $selectedService, array $availableServices): ?Service + { + foreach ($availableServices as $availableService) { + if ($this->servicesMatch($selectedService, $availableService)) { + return $availableService; + } + } + + return null; + } + + /** + * Determines if a selected course matches an available course. + * + * @param mixed $selectedService The selected course from form data + * @param Service $availableService The available course to compare against + * + * @return bool True if the courses match, false otherwise + */ + private function servicesMatch(mixed $selectedService, Service $availableService): bool + { + // Direct object comparison + if ($selectedService === $availableService) { + return true; + } + + // ID comparison for Service objects + if ($selectedService instanceof Service) { + return $selectedService->id === $availableService->id; + } + + // ID comparison for numeric values + if (is_numeric($selectedService)) { + return (int) $selectedService === $availableService->id; + } + + // String ID comparison + if (is_string($selectedService)) { + return $selectedService === (string) $availableService->id; + } + + return false; + } +} diff --git a/src/Form/Service/ParticipantDateOfBirthFieldHandler.php b/src/Form/Service/ParticipantDateOfBirthFieldHandler.php index c1d0541..fe196bd 100644 --- a/src/Form/Service/ParticipantDateOfBirthFieldHandler.php +++ b/src/Form/Service/ParticipantDateOfBirthFieldHandler.php @@ -107,4 +107,4 @@ class ParticipantDateOfBirthFieldHandler extends AbstractParticipantFieldHandler // Unsupported type, return null return null; } -} \ No newline at end of file +} diff --git a/src/Form/Service/ParticipantFieldHandlerRegistry.php b/src/Form/Service/ParticipantFieldHandlerRegistry.php index 9391cb0..96f725c 100644 --- a/src/Form/Service/ParticipantFieldHandlerRegistry.php +++ b/src/Form/Service/ParticipantFieldHandlerRegistry.php @@ -62,6 +62,28 @@ class ParticipantFieldHandlerRegistry $this->sortedHandlers = null; // Reset cache to force re-sorting with new handler } + /** + * Processes all participant fields and synchronizes submitted data with cleaned DTO state. + * + * This method combines field processing with data synchronization to ensure that + * the submitted form data reflects any changes made by field handlers. This is + * particularly useful for HTMX form updates where invalid selections need to be + * automatically cleared. + * + * @param array $submittedData The submitted form data containing participants array + * @param BookingDtoInterface $bookingDto The booking DTO to update with processed field values + * + * @return array The synchronized submitted data reflecting DTO changes + */ + public function processFieldsAndSync(array $submittedData, BookingDtoInterface $bookingDto): array + { + // Process all field handlers to clean the DTO + $this->processFields($submittedData, $bookingDto); + + // Synchronize submitted data with the cleaned DTO state + return $this->syncSubmittedDataWithDto($submittedData, $bookingDto); + } + /** * Processes all participant fields from submitted form data using registered handlers. * @@ -201,4 +223,131 @@ class ParticipantFieldHandlerRegistry return $result; } + + /** + * Synchronizes submitted data with the cleaned DTO state. + * + * This method updates the submitted form data to reflect any changes made by + * field handlers (such as clearing invalid service selections). This ensures + * that the form continues processing with cleaned data rather than the original + * submitted data that may contain invalid selections. + * + * The synchronization is generic and works with any field handlers by examining + * the current DTO state and updating the corresponding submitted data fields. + * + * @param array $submittedData The original submitted form data + * @param BookingDtoInterface $bookingDto The DTO with cleaned data from field handlers + * + * @return array Updated submitted data reflecting DTO state + */ + private function syncSubmittedDataWithDto(array $submittedData, BookingDtoInterface $bookingDto): array + { + // Ensure participants array exists in submitted data + if (!isset($submittedData['participants']) || !is_array($submittedData['participants'])) { + return $submittedData; + } + + // Sync each participant's data with the cleaned DTO + foreach ($submittedData['participants'] as $index => $participantData) { + if (!is_array($participantData)) { + continue; + } + + $participant = $bookingDto->getParticipant((int) $index); + if (null === $participant) { + continue; + } + + // Update participant data to match cleaned DTO state + $submittedData['participants'][$index] = $this->syncParticipantData($participantData, $participant); + } + + return $submittedData; + } + + /** + * Synchronizes individual participant submitted data with cleaned participant DTO. + * + * This method examines the participant DTO and updates the submitted data to match + * any changes made by field handlers. It automatically detects which fields have + * been processed by checking against registered handlers. + * + * @param array $participantData The submitted participant data + * @param object $participant The cleaned participant DTO + * + * @return array Updated participant data with synchronized field values + */ + private function syncParticipantData(array $participantData, object $participant): array + { + // Sync fields for all registered handlers + foreach ($this->handlers as $fieldName => $handler) { + if (property_exists($participant, $fieldName)) { + $dtoValue = $participant->{$fieldName}; + $participantData[$fieldName] = $this->convertDtoValueToSubmittedFormat($dtoValue); + } + } + + return $participantData; + } + + /** + * Converts DTO field values to the format expected in submitted form data. + * + * This method handles the conversion from DTO field values to the format + * that Symfony forms expect in submitted data. It supports various data types + * including service objects, arrays, and primitive values. + * + * @param mixed $dtoValue The field value from the DTO + * + * @return mixed The value in submitted data format + */ + private function convertDtoValueToSubmittedFormat(mixed $dtoValue): mixed + { + // Handle null values + if (null === $dtoValue) { + return null; + } + + // Handle arrays (service collections, etc.) + if (is_array($dtoValue)) { + $submittedFormat = []; + + foreach ($dtoValue as $item) { + $submittedFormat[] = $this->convertSingleValueToSubmittedFormat($item); + } + + return $submittedFormat; + } + + // Handle single values + return $this->convertSingleValueToSubmittedFormat($dtoValue); + } + + /** + * Converts a single DTO value to submitted form format. + * + * @param mixed $value The value to convert + * + * @return mixed The converted value + */ + private function convertSingleValueToSubmittedFormat(mixed $value): mixed + { + // Handle Service objects -> convert to ID + if ($value instanceof \App\BusProNet\Model\Service) { + return $value->id; + } + + // Handle DateTimeInterface -> convert to string format + if ($value instanceof \DateTimeInterface) { + return $value->format('Y-m-d'); + } + + // Handle numeric values + if (is_numeric($value)) { + return $value; + } + + // Handle strings and other primitive types + return $value; + } } diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index e6c7334..58ff584 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -10,7 +10,6 @@ use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface; use App\Form\Service\Abstract\AbstractFieldOptionsProvider; use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory; -use App\Form\Service\ServiceAgeEvaluator; /** * Provides dynamic field options for participant form fields. diff --git a/src/Form/Service/ParticipantRentalsFieldHandler.php b/src/Form/Service/ParticipantRentalsFieldHandler.php new file mode 100644 index 0000000..cb89730 --- /dev/null +++ b/src/Form/Service/ParticipantRentalsFieldHandler.php @@ -0,0 +1,122 @@ +getParticipant($bookingDto, $participantIndex); + if (null === $participant) { + return; + } + + $selectedRentals = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; + $availableRentals = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true); + + $validSelections = $this->filterValidServiceSelections( + $selectedRentals, + $availableRentals, + $bookingDto, + $participantIndex + ); + + $participant->rentals = $validSelections; + } + + private function filterValidServiceSelections( + array $selectedServices, + array $availableServices, + BookingDtoInterface $bookingDto, + int $participantIndex, + ): array { + $validSelections = []; + + foreach ($selectedServices as $selectedService) { + if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) { + $validSelections[] = $selectedService; + } + } + + return $validSelections; + } + + private function isServiceValidForParticipant( + mixed $selectedService, + array $availableServices, + BookingDtoInterface $bookingDto, + int $participantIndex, + ): bool { + $service = $this->findServiceInAvailableServices($selectedService, $availableServices); + + if (null === $service) { + return false; + } + + $ageEvaluator = new ServiceAgeEvaluator(); + if (!$ageEvaluator->canEvaluate($service)) { + return true; + } + + return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex); + } + + private function findServiceInAvailableServices(mixed $selectedService, array $availableServices): ?Service + { + foreach ($availableServices as $availableService) { + if ($this->servicesMatch($selectedService, $availableService)) { + return $availableService; + } + } + + return null; + } + + private function servicesMatch(mixed $selectedService, Service $availableService): bool + { + if ($selectedService === $availableService) { + return true; + } + + if ($selectedService instanceof Service) { + return $selectedService->id === $availableService->id; + } + + if (is_numeric($selectedService)) { + return (int) $selectedService === $availableService->id; + } + + if (is_string($selectedService)) { + return $selectedService === (string) $availableService->id; + } + + return false; + } +} diff --git a/src/Form/Service/ServiceAgeEvaluator.php b/src/Form/Service/ServiceAgeEvaluator.php index d6d6987..12043dc 100644 --- a/src/Form/Service/ServiceAgeEvaluator.php +++ b/src/Form/Service/ServiceAgeEvaluator.php @@ -49,12 +49,12 @@ class ServiceAgeEvaluator return false; // Cannot evaluate without birth date } - return match($service->ageConstraintType) { + return match ($service->ageConstraintType) { 'absolute_age' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth), 'birth_year' => $this->evaluateBirthYear($service, $participant->dateOfBirth), - 'mixed' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth) + 'mixed' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth) && $this->evaluateBirthYear($service, $participant->dateOfBirth), - default => true // No constraints or unknown type + default => true, // No constraints or unknown type }; } @@ -133,13 +133,13 @@ class ServiceAgeEvaluator */ public function getConstraintDescription(Service $service): string { - return match($service->ageConstraintType) { + return match ($service->ageConstraintType) { 'absolute_age' => $this->getAbsoluteAgeDescription($service), 'birth_year' => $this->getBirthYearDescription($service), - 'mixed' => sprintf('%s and %s', + 'mixed' => sprintf('%s and %s', $this->getAbsoluteAgeDescription($service), $this->getBirthYearDescription($service)), - default => 'No age restrictions' + default => 'No age restrictions', }; } @@ -180,6 +180,7 @@ class ServiceAgeEvaluator if ($service->birthYearFrom === $service->birthYearTo) { return sprintf('Born in %d', $service->birthYearFrom); } + return sprintf('Born %d-%d', $service->birthYearFrom, $service->birthYearTo); } @@ -193,4 +194,4 @@ class ServiceAgeEvaluator return ''; } -} \ No newline at end of file +}