$submittedData The submitted participant form data * @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT) * @param int $participantIndex The index of the participant being processed * * @return bool Always returns true for service selection fields */ public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool { return true; // Always process to handle deselection cases } /** * Processes the additionalServices field for a specific participant. * * This method extracts additional service selections from submitted form data, * validates each selection against the participant's age constraints, and * updates the participant DTO with only valid selections. Services that are * no longer appropriate for the participant's age are automatically removed. * * Processing steps: * 1. Safely retrieves the participant object from the DTO * 2. Extracts current service selections from submitted data * 3. Gets available additional services from travel data * 4. Validates each selection against age constraints * 5. Updates participant with filtered valid selections * * @param array $submittedData The submitted participant form data * @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT) * @param BookingDto $bookingDto The booking DTO to update (create or edit) * @param int $participantIndex The index of the participant being processed */ public function processField(array $submittedData, BookingDto $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()) ?? []; // Debug: Log what was submitted $submittedIds = array_map(fn($s) => is_object($s) ? $s->id : $s, $selectedServices); error_log(sprintf('[AdditionalServices] Participant %d: Submitted service IDs: [%s]', $participantIndex, implode(', ', $submittedIds))); // 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 ); // Debug: Log what passed validation $validIds = array_map(fn($s) => $s->id, $validSelections); error_log(sprintf('[AdditionalServices] Participant %d: Valid service IDs after filtering: [%s]', $participantIndex, implode(', ', $validIds))); // 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 BookingDto $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, BookingDto $bookingDto, int $participantIndex, ): array { $validSelections = []; foreach ($selectedServices as $selectedService) { if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) { // Convert the selected service ID/data back to the actual Service object $serviceObject = $this->findServiceInAvailableServices($selectedService, $availableServices); if (null !== $serviceObject) { $validSelections[] = $serviceObject; } } } 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 BookingDto $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, BookingDto $bookingDto, int $participantIndex, ): bool { // Find the service in available services $service = $this->findServiceInAvailableServices($selectedService, $availableServices); if (null === $service) { error_log(sprintf('[AdditionalServices] Participant %d: Service %s NOT FOUND in available services', $participantIndex, is_object($selectedService) ? $selectedService->id : $selectedService)); return false; // Service not found in available services } // Check if service has age constraints $ageEvaluator = new ServiceAgeEvaluator(); if (false === $ageEvaluator->canEvaluate($service)) { error_log(sprintf('[AdditionalServices] Participant %d: Service %d (%s) has NO age constraints - VALID', $participantIndex, $service->id, $service->label)); return true; // No age restrictions, service is valid } // Validate service against participant's age $isValid = $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex); $participant = $bookingDto->getParticipant($participantIndex); $age = $participant?->getAge($bookingDto->travel->dateFrom); error_log(sprintf( '[AdditionalServices] Participant %d (age %s): Service %d (%s) age validation = %s. Constraints: %s', $participantIndex, $age ?? 'unknown', $service->id, $service->label, $isValid ? 'VALID' : 'INVALID', $ageEvaluator->getConstraintDescription($service) )); return $isValid; } /** * 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; } }