$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 True if the handler should process this field, false otherwise */ public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool { return isset($submittedData[$this->getFieldName()]); } /** * Safely retrieves a participant object from the booking DTO. * * This helper method provides safe access to participant data by checking * if the participant exists at the given index. Returns null if the * participant doesn't exist, preventing array access errors. * * @param BookingDto $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(BookingDto $bookingDto, int $participantIndex): ?object { $participants = $bookingDto->getParticipants(); return $participants[$participantIndex] ?? null; } /** * Checks if the booking flow is in edit mode. * * Handlers may need to behave differently in edit mode, particularly when * dealing with data that isn't returned by the BPN API (like insurance selections). * In edit mode, missing data doesn't mean "clear this field" - it means the API * doesn't provide it and it should be preserved as-is. * * @param BookingDto $bookingDto The booking DTO to check * * @return bool True if in edit mode, false otherwise */ protected function isEditMode(BookingDto $bookingDto): bool { return BookingDto::MODE_EDIT === $bookingDto->getMode(); } /** * Checks if the booking flow is in create mode. * * @param BookingDto $bookingDto The booking DTO to check * * @return bool True if in create mode, false otherwise */ protected function isCreateMode(BookingDto $bookingDto): bool { return BookingDto::MODE_CREATE === $bookingDto->getMode(); } /** * Safely extracts a field value from submitted participant data. * * This helper method provides safe access to form field values using * the null coalescing operator. Useful for extracting field values * without worrying about undefined array keys. * * @param array $submittedData The submitted participant form data * @param string $fieldName The name of the field to retrieve * @param mixed $default The default value to return if field is not set * * @return mixed The field value, or the default value if not found */ protected function getFieldValue(array $submittedData, string $fieldName, mixed $default = null): mixed { return $submittedData[$fieldName] ?? $default; } /** * Normalizes empty string values to null. * * Form inputs often submit empty strings for unselected/empty fields. * This helper converts those empty strings to null values, which is * typically more appropriate for database storage and business logic. * * @param mixed $value The value to normalize * * @return mixed The normalized value (null if empty, original value otherwise) */ protected function normalizeEmptyValue(mixed $value): mixed { return $this->isEmptyValue($value) ? null : $value; } /** * Converts and normalizes string values to integers. * * Form inputs submit all values as strings. This helper safely converts * string values to integers while handling empty strings and non-numeric * values gracefully by returning null. * * @param mixed $value The value to convert to integer * * @return int|null The integer value, or null if empty/non-numeric */ protected function normalizeIntValue(mixed $value): ?int { if ($this->isEmptyValue($value)) { return null; } return is_numeric($value) ? (int) $value : null; } private function isEmptyValue(mixed $value): bool { return null === $value || false === $value || 0 === $value || 0.0 === $value || '' === $value || '0' === $value || [] === $value; } /** * Normalizes a checkbox/form toggle value to a boolean. * * Form checkboxes may submit true, false, 1, 0, '1', '0', or be absent. * This method treats only true, 1, and '1' as checked; everything else as unchecked. * * @param mixed $value The raw value from form submission * * @return bool True if the checkbox is considered checked, false otherwise */ protected function normalizeCheckboxValue(mixed $value): bool { return true === $value || 1 === $value || '1' === $value; } /** * Returns field state modifications that should be applied after processing. * * Default implementation returns no state modifications. Override this method * in concrete handlers that need to modify field states based on their * processing results. * * @param array $submittedData The submitted participant form data * @param BookingDto $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, BookingDto $bookingDto, int $participantIndex): array { return []; } /** * Returns field names whose state is affected by this handler's processing. * * Default implementation returns an empty array, meaning this handler doesn't * affect the state of any fields. Override this method in concrete handlers * that modify field states. * * @return string[] Empty array (no affected fields by default) */ public function getAffectedFieldNames(): array { return []; } /** * Finds an item by ID from an array of objects. * * Generic helper for validating a selected ID against an array of available * items and returning the matching object. Used by handlers that need to * validate service, pickup, or other selections against available options. * * Normalizes the input ID to integer before comparison since Service, Pickup, * and Room models all use integer IDs. * * @param mixed $selectedId The submitted ID to find (string or int) * @param object[] $items Array of objects with an `id` property (must be int) * * @return object|null The matching item, or null if not found or invalid ID */ protected function findItemById(mixed $selectedId, array $items): ?object { $id = $this->normalizeIntValue($selectedId); if (null === $id) { return null; } foreach ($items as $item) { if ($item->id === $id) { return $item; } } return null; } /** * Finds a Service in available services by matching the selected value. * * Handles various input types that may come from form submissions: * - Service object: matches by object identity or ID * - Integer/numeric string: matches by ID * * @param mixed $selectedService The selected service (Service object, int, or numeric string) * @param Service[] $availableServices Array of available Service objects * * @return Service|null The matching Service, or null if not found */ protected function findServiceInAvailableServices(mixed $selectedService, array $availableServices): ?Service { foreach ($availableServices as $availableService) { if ($this->servicesMatch($selectedService, $availableService)) { return $availableService; } } return null; } /** * Checks if a selected service matches an available service. * * Compares services by object identity first, then by ID. Handles Service objects, * integers, and numeric strings from form submissions. * * @param mixed $selectedService The selected service to compare * @param Service $availableService The available service to compare against * * @return bool True if the services match, false otherwise */ protected 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 (int or numeric string from form) if (is_numeric($selectedService)) { return (int) $selectedService === $availableService->id; } return false; } /** * Retrieves available services from travel data, merged with booked services in edit mode. * * In edit mode, services that were previously booked for the participant are included * even if they're no longer available in travel data. This prevents booked services * from being dropped during form validation when the travel catalog has changed. * * @param BookingDto $bookingDto The booking DTO containing travel and booking data * @param int $participantIndex The participant index to retrieve booked services for * @param mixed $serviceGroup The service group(s) to filter by (e.g., TOKEN_RENTALS) * @param bool $filterByTravelDateRange Whether to filter travel services by date range * * @return Service[] Array of available services keyed by service ID */ protected function getAvailableServicesWithBooked( BookingDto $bookingDto, int $participantIndex, mixed $serviceGroup, bool $filterByTravelDateRange = false, ): array { $availableServices = $bookingDto->travel->getAdditionalServicesBySubTypes($serviceGroup, $filterByTravelDateRange); if ($this->isEditMode($bookingDto) && null !== $bookingDto->booking) { $availableServices = $this->mergeBookedServices( $availableServices, $bookingDto->booking, $participantIndex, $serviceGroup ); } return $availableServices; } /** * Merges booked services into the available services array. * * @param Service[] $availableServices The services available from travel data * @param Booking $booking The booking containing previously booked services * @param int $participantIndex The participant index to retrieve booked services for * @param mixed $serviceGroup The service group(s) to filter by * * @return Service[] Merged array of services keyed by service ID */ private function mergeBookedServices( array $availableServices, Booking $booking, int $participantIndex, mixed $serviceGroup, ): array { $bookedServices = $booking->getAdditionalServicesForParticipantByGroup($participantIndex, $serviceGroup); foreach ($bookedServices as $bookedService) { if (false === isset($availableServices[$bookedService->id])) { $availableServices[$bookedService->id] = $bookedService; } } return $availableServices; } }