557 lines
20 KiB
Markdown
557 lines
20 KiB
Markdown
# 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
|
|
- Body dimensions are hidden unless rental services are selected (conditional visibility)
|
|
- Rental insurance field with checkbox interface and boolean state tracking
|
|
|
|
```php
|
|
class ParticipantDto
|
|
{
|
|
// Personal data
|
|
public ?string $firstName = null;
|
|
public ?string $lastName = null;
|
|
public ?\DateTimeImmutable $dateOfBirth = null;
|
|
public ?string $email = null;
|
|
|
|
// Body dimensions (hidden unless rental services are selected)
|
|
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 = [];
|
|
public ?Service $rentalInsurance = null;
|
|
public bool $rentalInsuranceSelected = false; // Checkbox state
|
|
|
|
// Transportation services
|
|
public ?Service $transportationOutbound = null;
|
|
public ?Service $transportationInbound = null;
|
|
public ?Pickup $pickupOutbound = null;
|
|
public ?Pickup $pickupInbound = null;
|
|
public bool $parking = false;
|
|
public ?Service $parkingService = null;
|
|
}
|
|
```
|
|
|
|
### 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
|
|
- Controls visibility of body dimensions and rental insurance fields
|
|
- Populated from `TOKEN_RENTALS` subtype services
|
|
|
|
- **`rentalInsurance`**: Rental insurance checkbox
|
|
- Checkbox interface (similar to parking field)
|
|
- Maps to `rentalInsuranceSelected` boolean property
|
|
- Only visible when rental services are selected
|
|
- Automatically manages Service object for pricing calculations
|
|
- Populated from `TOKEN_RENTAL_INSURANCE` subtype services
|
|
|
|
- **`parking`**: Parking service checkbox
|
|
- Boolean checkbox for self-organized transportation
|
|
- Only visible when outbound transportation is PKW (car)
|
|
- Manages both boolean state and Service object for pricing
|
|
|
|
**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
|
|
- Controls conditional field visibility and state based on participant data
|
|
- Implements body dimensions and rental insurance conditional visibility
|
|
|
|
**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();
|
|
|
|
// Hide body dimensions section unless rental services are selected
|
|
$this->fieldStateConditions['bodyDimensions'] = [
|
|
'hidden' => CompositeCondition::not($rentalCondition),
|
|
];
|
|
|
|
// Hide rental insurance unless rental services are selected
|
|
$this->fieldStateConditions['rentalInsurance'] = [
|
|
'hidden' => CompositeCondition::not($rentalCondition),
|
|
];
|
|
|
|
// Hide parking unless outbound transportation is PKW (car)
|
|
$this->fieldStateConditions['parking'] = [
|
|
'hidden' => CompositeCondition::not(
|
|
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API)
|
|
),
|
|
];
|
|
}
|
|
```
|
|
|
|
#### 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
|
|
- **`ParticipantRentalInsuranceFieldHandler`**: Rental insurance checkbox handling
|
|
- Depends on `['dateOfBirth', 'rentals']` (only visible when rentals selected)
|
|
- Processes boolean checkbox input and converts to Service object
|
|
- Manages both `rentalInsuranceSelected` (bool) and `rentalInsurance` (Service) properties
|
|
|
|
All service handlers share these characteristics:
|
|
- Depend on `dateOfBirth` field (processed first)
|
|
- Filter selections based on age constraints (except rental insurance which uses conditional visibility)
|
|
- 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 |