# Form Processing System Documentation ## Overview The MyEP Next Booking application implements a sophisticated form processing system designed to handle complex multi-step booking workflows with dynamic participant forms and conditional field logic. The system integrates with the Bus Pro Net (BPN) XML API for travel management and provides real-time field updates through HTMX integration. ## Architecture Components ### 1. Multi-Step Booking Flow The booking process consists of three main steps: 1. **Step 1 (`CreateStep1Controller`)**: Room selection with quantities and dates 2. **Step 2 (`CreateStep2Controller`)**: Participant details with conditional fields 3. **Step 3**: Final confirmation and submission to BPN API ### 2. Data Transfer Objects (DTOs) #### BookingCreateDto (`src/Form/Model/BookingCreateDto.php`) - Main container for the entire booking process - Contains travel data, hotel ID, room selections, and participants - Implements `BookingDtoInterface` for polymorphic handling ```php class BookingCreateDto implements BookingDtoInterface { public int $currentStep = 1; public array $roomSelections = []; // RoomSelectionDto[] public array $participants = []; // ParticipantDto[] public Travel $travel; public int $hotelId; } ``` #### ParticipantDto (`src/Form/Model/ParticipantDto.php`) - Individual participant data container - Includes personal data, body dimensions, and service selections - Custom validation for body dimensions when rental services are selected ```php class ParticipantDto { // Personal data public ?string $firstName = null; public ?string $lastName = null; public ?\DateTimeImmutable $dateOfBirth = null; public ?string $email = null; // Body dimensions (conditional) public ?string $height = null; public ?string $weight = null; public ?string $shoeSize = null; // Service selections public ?int $assignedRoomId = null; public array $courses = []; public array $additionalServices = []; public array $rentals = []; // ... other service arrays } ``` ### 3. Dynamic Field Options System #### ParticipantFieldOptionsProvider (`src/Form/Service/ParticipantFieldOptionsProvider.php`) Central registry for dynamic field configurations using a provider pattern with lazy evaluation. **Registered Field Providers:** - **`assignedRoomId`**: Context-aware room selection - Shows only available rooms for the participant - Excludes rooms already assigned to other participants - Respects room capacity and booking constraints - **`courses`**: Available courses from travel data - Multiple selection with checkboxes - Populated from `travel.additionalServices` with `TOKEN_COURSES` subtype - **`additionalServices`**: Additional services with mandatory logic - Mandatory services are pre-selected and readonly - Choice attributes include visual indicators for mandatory items - **`board`**: Board/meal options - Multiple selection from travel data - Populated from `TOKEN_BOARD` subtype services - **`rentals`**: Rental equipment options - Date-filtered rental services - Triggers body dimension requirements when selected - Populated from `TOKEN_RENTALS` subtype services **Provider Pattern Implementation:** ```php protected function registerFieldOptionProviders(): void { $this->fieldOptionProviders['fieldName'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Field Label', 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex), // ... other Symfony form options ]; } ``` ### 4. Conditional Field State System #### Field State Providers **CreateFieldStateProvider (`src/Form/Service/CreateFieldStateProvider.php`)** - Manages field states for the booking creation workflow - Currently implements body dimension requirements for rental services **Field State Types:** - `readonly`: Field is visible but not editable - `disabled`: Field interaction is disabled - `required`: Field becomes mandatory - `hidden`: Field is not displayed **Current Implementation:** ```php protected function registerFieldStateConditions(): void { $rentalCondition = new RentalSelectionCondition(); // Body dimensions become required when rentals are selected $this->fieldStateConditions['height'] = ['required' => $rentalCondition]; $this->fieldStateConditions['weight'] = ['required' => $rentalCondition]; $this->fieldStateConditions['shoeSize'] = ['required' => $rentalCondition]; } ``` #### Field Conditions **Available Condition Types:** 1. **`RentalSelectionCondition`**: Evaluates rental service selections 2. **`AgeRangeCondition`**: Age-based conditions 3. **`FieldValueCondition`**: Field interdependency conditions (equals, in, empty, etc.) 4. **`CompositeCondition`**: Complex logic with AND/OR/NOT operators 5. **`ApplicantCondition`**: Applicant-specific conditions 6. **`MutabilityCondition`**: Mutability-based conditions **Condition Interface:** ```php interface FieldConditionInterface { public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool; public function getDependentFields(): array; public function getDescription(): string; } ``` ### 5. Participant Field Processing #### ParticipantFieldHandlerRegistry (`src/Form/Service/ParticipantFieldHandlerRegistry.php`) Manages field handlers in dependency order using topological sorting to ensure proper processing sequence. **Key Features:** - Dependency resolution using Kahn's algorithm - Circular dependency detection - Support for both simple and complex handler instantiation - Batch processing of all participants **Processing Flow:** ```php 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); } public function processFields(array $submittedData, BookingDtoInterface $bookingDto): void { foreach ($submittedData['participants'] as $participantIndex => $participantData) { foreach ($this->getSortedHandlers() as $handlerName) { $handler = $this->handlers[$handlerName]; if ($handler->shouldProcess($participantData, $participantIndex)) { $handler->processField($participantData, $bookingDto, $participantIndex); } } } } ``` **Data Synchronization:** The registry includes a critical synchronization feature to maintain consistency between DTO state and form submitted data: ```php private function syncSubmittedDataWithDto(array $submittedData, BookingDtoInterface $bookingDto): array { // Updates submitted data to match cleaned DTO state // Converts DTO objects back to form-expected formats // Ensures form rendering shows valid selections only } ``` This prevents validation errors when field handlers remove invalid selections from DTOs but the original submitted data still contains those invalid choices. #### Field Handlers **AbstractParticipantFieldHandler (`src/Form/Service/Abstract/AbstractParticipantFieldHandler.php`)** Base class providing common functionality: - Default dependency resolution - Safe participant data access - Field value extraction utilities - Value normalization methods **Concrete Implementations:** 1. **`ParticipantDateOfBirthFieldHandler`**: Processes date of birth field - Converts submitted date strings to `DateTimeImmutable` objects - Normalizes various date formats - No dependencies (foundation field for age-based logic) 2. **`ParticipantAssignedRoomFieldHandler`**: Processes room assignments - Converts form strings to integers - Handles empty selections as null values - No dependencies (base field) 3. **Service-Based Handlers** (age-aware filtering): - **`ParticipantAdditionalServicesFieldHandler`**: Additional services filtering - **`ParticipantCoursesFieldHandler`**: Course selections filtering - **`ParticipantBoardFieldHandler`**: Board/meal options filtering - **`ParticipantRentalsFieldHandler`**: Rental equipment filtering All service handlers share these characteristics: - Depend on `dateOfBirth` field (processed first) - Filter selections based on age constraints - Instantiate `ServiceAgeEvaluator` directly when needed - Remove invalid selections to prevent form validation errors - **Store complete Service objects** in ParticipantDto (not just IDs) for pricing calculations **Key Architecture Decisions**: 1. **Service Evaluator Instantiation**: Service handlers instantiate `ServiceAgeEvaluator` directly rather than using dependency injection because: - `ServiceAgeEvaluator` has no dependencies itself - Handlers are registered as simple class names in service configuration - Avoids complex service wiring for lightweight utility classes - Maintains clean separation between handlers and evaluator logic 2. **Data Storage Strategy**: Service handlers store complete Service objects in ParticipantDto rather than just IDs because: - Pricing calculations require access to service price data - Eliminates need for additional database lookups during price calculation - Provides immediate access to all service metadata (labels, descriptions, etc.) - Maintains data consistency throughout the booking flow **Handler Interface:** ```php interface ParticipantFieldHandlerInterface { public function getFieldName(): string; public function getDependencies(): array; public function shouldProcess(array $submittedData, int $participantIndex): bool; public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void; } ``` ### 6. Form Type Integration #### BookingCreateStep2Type (`src/Form/BookingCreateStep2Type.php`) Main form type for participant data collection with event-driven processing. **Form Events:** - **`PRE_SET_DATA`**: Initial form setup with participants collection - **`PRE_SUBMIT`**: Dynamic field updates and DTO synchronization **Event Processing:** ```php public function onPreSubmit(FormEvent $event): void { $form = $event->getForm(); $submittedData = $event->getData(); $bookingDto = $form->getData(); // 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); } ``` #### BookingCreateParticipantType (`src/Form/BookingCreateParticipantType.php`) Individual participant form with dynamic field management. **Static Fields:** - Personal data (name, email, birth date, etc.) - Body dimensions (embedded `BodyDimensionsType`) - Contact information **Dynamic Fields:** - Room assignment (`assignedRoomId`) - Service selections (courses, additional services, board, rentals) **Dynamic State Application:** ```php private function applyFieldStates(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): void { $allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex, $formData); foreach ($allFieldStates as $fieldName => $fieldState) { // Apply state modifications to form fields // Handle nested body dimension fields specially } } ``` ## Form Processing Pipeline ### 1. Initial Form Rendering 1. **Controller** creates `BookingCreateDto` with travel data 2. **Step2Type** `PRE_SET_DATA` event fires: - Adds participants collection field - Each participant triggers `BookingCreateParticipantType` creation 3. **ParticipantType** `PRE_SET_DATA` event fires: - Adds dynamic fields using `FieldOptionsProvider` - Applies initial field states using `FieldStateProvider` 4. **Form rendered** with proper field options and states ### 2. Form Submission Processing 1. **Form submission** received by controller 2. **Step2Type** `PRE_SUBMIT` event fires: - `ParticipantFieldHandlerRegistry` processes all submitted data to update DTOs - Registry synchronizes submitted data with cleaned DTO state - Event data updated with cleaned submitted data - Form rebuilt with updated DTO state 3. **ParticipantType** `PRE_SUBMIT` event fires: - Field states recalculated based on submitted data - Form fields updated with new states 4. **Validation** runs on updated DTO with cleaned data 5. **Controller** handles successful submission or re-renders with errors ### 3. HTMX Dynamic Updates For real-time field updates without full form submission: 1. **HTMX request** sent with partial form data 2. **Same pipeline** executes as form submission 3. **Partial response** returned with updated field states 4. **Frontend** updates only changed form sections **Service Field HTMX Integration:** Service fields (board, skipass, courses, etc.) use expanded choice types (checkboxes/radios) which require special HTMX trigger handling: - **Issue**: HTMX attributes on container elements don't capture individual input changes - **Solution**: HTMX triggers must be placed on each individual checkbox/radio input - **Implementation**: Field handlers ensure `hx-post`, `hx-target`, and `hx-trigger` attributes are applied to each choice input - **Result**: Real-time updates work reliably for all service selections **HTMX Attribute Placement:** ```php // Correct: Individual input triggers 'attr' => [ 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), 'hx-target' => '#booking-summary', 'hx-trigger' => 'change', ], // Incorrect: Container-level triggers (doesn't work for expanded choices) // 'row_attr' => ['hx-post' => '...'] ``` ## Validation System ### DTO-Level Validation **ParticipantDto Validation:** - Symfony validation constraints on properties - Custom callback validation for body dimensions when rentals selected ```php #[Assert\Callback('validateBodyDimensionsForRentals', groups: ['booking_create_step_2'])] public function validateBodyDimensionsForRentals(ExecutionContextInterface $context): void { if (!empty($this->rentals)) { // Validate height, weight, shoeSize are provided } } ``` ### Form-Level Validation **Validation Groups:** - `booking_create_step_2`: Step 2 specific validations - `booking_edit`: Edit workflow validations ## Extension Points ### Adding New Dynamic Fields 1. **Register field options** in `ParticipantFieldOptionsProvider`: ```php $this->fieldOptionProviders['newField'] = fn($bookingDto, $participantIndex) => [ 'label' => 'New Field Label', 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex), ]; ``` 2. **Add to dynamic fields list** in `BookingCreateParticipantType`: ```php $dynamicFields = ['assignedRoomId', 'courses', 'newField']; // Add 'newField' ``` ### Adding New Field Conditions 1. **Implement condition class**: ```php class NewCondition implements FieldConditionInterface { public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool { // Condition logic } } ``` 2. **Register in field state provider**: ```php $this->fieldStateConditions['fieldName'] = [ 'required' => new NewCondition(), ]; ``` ### Adding New Field Handlers 1. **Implement handler class**: ```php class NewFieldHandler extends AbstractParticipantFieldHandler { public function getFieldName(): string { return 'newField'; } public function getDependencies(): array { return ['dependentField']; } public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void { // Processing logic } } ``` 2. **Register in service configuration** (services.yaml or through DI) ## Integration with BPN API The form system is designed to prepare data for submission to the Bus Pro Net XML API: 1. **Field handlers** transform form data into BPN-compatible format 2. **Service selections** map to BPN service IDs 3. **Room assignments** align with BPN room availability 4. **Validation rules** ensure data meets BPN requirements ## Performance Considerations ### Optimization Strategies 1. **Lazy evaluation** in field option providers 2. **Caching** in field state providers 3. **Dependency sorting** cached until handlers change 4. **Minimal form rebuilding** only when necessary ### Memory Management - DTOs use typed properties to minimize memory footprint - Field handlers process data in-place where possible - Form events only rebuild changed portions ## Security Considerations ### XSS Protection - Custom `XssCleanTransformer` applied to text inputs - `clean_xss: true` option on relevant form fields ### Data Validation - Strict type declarations throughout - Yoda conditions for safety - Explicit validation constraints on all user inputs ## Testing Strategy ### Unit Testing Focus Areas 1. **Field option providers** with various travel data scenarios 2. **Field conditions** with different participant states 3. **Field handlers** with edge cases and dependencies 4. **Validation logic** for body dimensions and rental services ### Integration Testing 1. **Form submission workflows** end-to-end 2. **HTMX dynamic updates** with state changes 3. **Multi-participant scenarios** with interdependencies ## Future Enhancements ### Planned Features 1. **Step 3 implementation** for booking confirmation 2. **Additional field conditions** for complex business rules 3. **Enhanced validation** for service compatibility 4. **Performance optimizations** for large participant counts ### Architectural Improvements 1. **Event system** for field state change notifications 2. **Caching layer** for expensive field option calculations 3. **Async processing** for complex form submissions 4. **Enhanced error handling** with user-friendly messages