wip: update docs
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user