18 KiB
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:
- Step 1 (
CreateStep1Controller): Room selection with quantities and dates - Step 2 (
CreateStep2Controller): Participant details with conditional fields - 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
BookingDtoInterfacefor polymorphic handling
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
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.additionalServiceswithTOKEN_COURSESsubtype
-
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_BOARDsubtype services
-
rentals: Rental equipment options- Date-filtered rental services
- Triggers body dimension requirements when selected
- Populated from
TOKEN_RENTALSsubtype services
Provider Pattern Implementation:
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 editabledisabled: Field interaction is disabledrequired: Field becomes mandatoryhidden: Field is not displayed
Current Implementation:
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:
RentalSelectionCondition: Evaluates rental service selectionsAgeRangeCondition: Age-based conditionsFieldValueCondition: Field interdependency conditions (equals, in, empty, etc.)CompositeCondition: Complex logic with AND/OR/NOT operatorsApplicantCondition: Applicant-specific conditionsMutabilityCondition: Mutability-based conditions
Condition Interface:
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:
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:
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:
-
ParticipantDateOfBirthFieldHandler: Processes date of birth field- Converts submitted date strings to
DateTimeImmutableobjects - Normalizes various date formats
- No dependencies (foundation field for age-based logic)
- Converts submitted date strings to
-
ParticipantAssignedRoomFieldHandler: Processes room assignments- Converts form strings to integers
- Handles empty selections as null values
- No dependencies (base field)
-
Service-Based Handlers (age-aware filtering):
ParticipantAdditionalServicesFieldHandler: Additional services filteringParticipantCoursesFieldHandler: Course selections filteringParticipantBoardFieldHandler: Board/meal options filteringParticipantRentalsFieldHandler: Rental equipment filtering
All service handlers share these characteristics:
- Depend on
dateOfBirthfield (processed first) - Filter selections based on age constraints
- Instantiate
ServiceAgeEvaluatordirectly 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:
-
Service Evaluator Instantiation: Service handlers instantiate
ServiceAgeEvaluatordirectly rather than using dependency injection because:ServiceAgeEvaluatorhas 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
-
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:
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 collectionPRE_SUBMIT: Dynamic field updates and DTO synchronization
Event Processing:
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:
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
- Controller creates
BookingCreateDtowith travel data - Step2Type
PRE_SET_DATAevent fires:- Adds participants collection field
- Each participant triggers
BookingCreateParticipantTypecreation
- ParticipantType
PRE_SET_DATAevent fires:- Adds dynamic fields using
FieldOptionsProvider - Applies initial field states using
FieldStateProvider
- Adds dynamic fields using
- Form rendered with proper field options and states
2. Form Submission Processing
- Form submission received by controller
- Step2Type
PRE_SUBMITevent fires:ParticipantFieldHandlerRegistryprocesses 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
- ParticipantType
PRE_SUBMITevent fires:- Field states recalculated based on submitted data
- Form fields updated with new states
- Validation runs on updated DTO with cleaned data
- Controller handles successful submission or re-renders with errors
3. HTMX Dynamic Updates
For real-time field updates without full form submission:
- HTMX request sent with partial form data
- Same pipeline executes as form submission
- Partial response returned with updated field states
- 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, andhx-triggerattributes are applied to each choice input - Result: Real-time updates work reliably for all service selections
HTMX Attribute Placement:
// 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
#[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 validationsbooking_edit: Edit workflow validations
Extension Points
Adding New Dynamic Fields
- Register field options in
ParticipantFieldOptionsProvider:
$this->fieldOptionProviders['newField'] = fn($bookingDto, $participantIndex) => [
'label' => 'New Field Label',
'choices' => $this->generateChoicesFor($bookingDto, $participantIndex),
];
- Add to dynamic fields list in
BookingCreateParticipantType:
$dynamicFields = ['assignedRoomId', 'courses', 'newField']; // Add 'newField'
Adding New Field Conditions
- Implement condition class:
class NewCondition implements FieldConditionInterface
{
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
// Condition logic
}
}
- Register in field state provider:
$this->fieldStateConditions['fieldName'] = [
'required' => new NewCondition(),
];
Adding New Field Handlers
- Implement handler class:
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
}
}
- 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:
- Field handlers transform form data into BPN-compatible format
- Service selections map to BPN service IDs
- Room assignments align with BPN room availability
- Validation rules ensure data meets BPN requirements
Performance Considerations
Optimization Strategies
- Lazy evaluation in field option providers
- Caching in field state providers
- Dependency sorting cached until handlers change
- 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
XssCleanTransformerapplied to text inputs clean_xss: trueoption 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
- Field option providers with various travel data scenarios
- Field conditions with different participant states
- Field handlers with edge cases and dependencies
- Validation logic for body dimensions and rental services
Integration Testing
- Form submission workflows end-to-end
- HTMX dynamic updates with state changes
- Multi-participant scenarios with interdependencies
Future Enhancements
Planned Features
- Step 3 implementation for booking confirmation
- Additional field conditions for complex business rules
- Enhanced validation for service compatibility
- Performance optimizations for large participant counts
Architectural Improvements
- Event system for field state change notifications
- Caching layer for expensive field option calculations
- Async processing for complex form submissions
- Enhanced error handling with user-friendly messages