Files
myep/src/Form/Service/Abstract/AbstractParticipantFieldHandler.php
T

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;
}
}