Files
myep/docs/FORM_PROCESSING.md
T
2025-09-02 17:21:50 +02:00

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:

  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
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.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:

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:

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:

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:

  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:

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:

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

  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:

// 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 validations
  • booking_edit: Edit workflow validations

Extension Points

Adding New Dynamic Fields

  1. Register field options in ParticipantFieldOptionsProvider:
$this->fieldOptionProviders['newField'] = fn($bookingDto, $participantIndex) => [
    'label' => 'New Field Label',
    'choices' => $this->generateChoicesFor($bookingDto, $participantIndex),
];
  1. Add to dynamic fields list in BookingCreateParticipantType:
$dynamicFields = ['assignedRoomId', 'courses', 'newField']; // Add 'newField'

Adding New Field Conditions

  1. Implement condition class:
class NewCondition implements FieldConditionInterface
{
    public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
    {
        // Condition logic
    }
}
  1. Register in field state provider:
$this->fieldStateConditions['fieldName'] = [
    'required' => new NewCondition(),
];

Adding New Field Handlers

  1. 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
    }
}
  1. 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