361 lines
13 KiB
PHP
361 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\Service\Abstract;
|
|
|
|
use App\BusProNet\Model\Booking;
|
|
use App\BusProNet\Model\Service;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
|
|
|
|
/**
|
|
* Abstract base class providing common functionality for participant field handlers.
|
|
*
|
|
* This class implements common patterns used across participant field handlers,
|
|
* including default implementations for field state modification methods.
|
|
* Reduces boilerplate code in concrete implementations.
|
|
*/
|
|
abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandlerInterface
|
|
{
|
|
/**
|
|
* Returns the field names this handler depends on.
|
|
*
|
|
* Default implementation returns an empty array, meaning no dependencies.
|
|
* Override this method in concrete handlers that depend on other fields
|
|
* being processed first (e.g., a meal preference handler might depend on
|
|
* the room assignment being processed first).
|
|
*
|
|
* @return string[] Array of field names that must be processed before this handler
|
|
*/
|
|
public function getDependencies(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Determines if this handler should process the field based on submitted data.
|
|
*
|
|
* Default implementation checks if the field exists in the submitted data.
|
|
* Override this method for more complex processing conditions (e.g., only
|
|
* process if certain other conditions are met, or skip processing in edit mode).
|
|
*
|
|
* @param array<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, array<string, mixed>> 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;
|
|
}
|
|
}
|