wip: update docs

This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent bd5c6359fe
commit 552dd029fa
6 changed files with 2161 additions and 0 deletions
+518
View File
@@ -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