# Universal Conditional Field State System This system provides a flexible architecture for implementing conditional field states (readonly, disabled, hidden) based on participant data and interdependent field values. ## Architecture Overview The system consists of several key components organized in a clean namespace structure: ### Core Interfaces (`src/Form/Service/Contract/`) 1. **FieldStateProviderInterface** - Defines the contract for field state management 2. **FieldOptionsProviderInterface** - Defines the contract for field option generation ### Abstract Base Classes (`src/Form/Service/Abstract/`) 3. **AbstractFieldStateProvider** - Common field state functionality 4. **AbstractFieldOptionsProvider** - Common field option functionality ### Concrete Implementations (`src/Form/Service/`) 5. **CreateFieldStateProvider** - Field states for booking creation workflow 6. **EditFieldStateProvider** - Field states for booking edit workflow 7. **ParticipantFieldOptionsProvider** - Dynamic field option generation ### Condition System (`src/Form/Service/Condition/`) 8. **FieldConditionInterface** - Defines the contract for condition evaluation 9. **Concrete Conditions** - Implement specific business logic (age ranges, field values, etc.) 10. **CompositeCondition** - Combines conditions with AND/OR/NOT logic ### Form Integration 11. **BookingCreateParticipantType** - Uses CreateFieldStateProvider 12. **BookingEditParticipantType** - Uses EditFieldStateProvider ## Usage Examples ### Basic Age-Based Condition ```php // Make a field readonly for participants under 18 $this->fieldStateConditions['serviceSelection'] = [ 'readonly' => new AgeRangeCondition(null, 17), ]; ``` ### Field Dependency Condition ```php // Disable field if room is not assigned $this->fieldStateConditions['mealPreference'] = [ 'disabled' => FieldValueCondition::empty('assignedRoomId'), ]; ``` ### Complex Composite Condition ```php // Hide field for young participants OR if basic service is selected $this->fieldStateConditions['advancedOptions'] = [ 'hidden' => CompositeCondition::or( new AgeRangeCondition(null, 15), FieldValueCondition::equals('serviceType', 'basic') ), ]; ``` ### Multiple State Conditions ```php // Field with multiple conditional states $this->fieldStateConditions['specialServices'] = [ 'readonly' => new AgeRangeCondition(null, 17), 'required' => FieldValueCondition::equals('roomType', 'premium'), 'disabled' => CompositeCondition::and( FieldValueCondition::empty('assignedRoomId'), FieldValueCondition::notEquals('participantType', 'staff') ), ]; ``` ## Available Conditions ### AgeRangeCondition - `new AgeRangeCondition(18, null)` - At least 18 years old - `new AgeRangeCondition(null, 17)` - Under 18 years old - `new AgeRangeCondition(18, 65)` - Between 18 and 65 years old ### FieldValueCondition - `FieldValueCondition::equals('field', 'value')` - Field equals specific value - `FieldValueCondition::notEquals('field', 'value')` - Field does not equal value - `FieldValueCondition::in('field', ['a', 'b'])` - Field value is in array - `FieldValueCondition::empty('field')` - Field is empty or null - `FieldValueCondition::isNotEmpty('field')` - Field has a value ### CompositeCondition - `CompositeCondition::and($cond1, $cond2)` - All conditions must be true - `CompositeCondition::or($cond1, $cond2)` - At least one condition must be true - `CompositeCondition::not($condition)` - Inverts condition result ## State Types - **readonly** - Field is visible but not editable - **disabled** - Field interaction is disabled - **required** - Field becomes mandatory - **hidden** - Field is not displayed (via CSS display: none) ## Adding New Conditions ### For Create Workflow To register field state conditions for the booking creation workflow, add them to the `registerFieldStateConditions()` method in `CreateFieldStateProvider`: ```php // src/Form/Service/CreateFieldStateProvider.php protected function registerFieldStateConditions(): void { // Age-based readonly state $this->fieldStateConditions['assignedRoomId'] = [ 'readonly' => new AgeRangeCondition(null, 17), ]; // Field dependency $this->fieldStateConditions['mealPreference'] = [ 'disabled' => FieldValueCondition::empty('assignedRoomId'), ]; // Complex business logic $this->fieldStateConditions['advancedServices'] = [ 'hidden' => CompositeCondition::or( new AgeRangeCondition(null, 15), FieldValueCondition::equals('membershipLevel', 'basic') ), 'required' => FieldValueCondition::equals('roomType', 'suite'), ]; } ``` ### For Edit Workflow To register field state conditions for the booking edit workflow, add them to the `registerFieldStateConditions()` method in `EditFieldStateProvider`: ```php // src/Form/Service/EditFieldStateProvider.php protected function registerFieldStateConditions(): void { // Make personal data readonly for applicants or non-mutable fields $personalDataFields = ['firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile']; foreach ($personalDataFields as $field) { $this->fieldStateConditions[$field] = [ 'readonly' => CompositeCondition::or( new ApplicantCondition(), new MutabilityCondition() ), ]; } } ``` ### Adding Field Options To register dynamic field options, add them to the `registerFieldOptionProviders()` method in `ParticipantFieldOptionsProvider`: ```php // src/Form/Service/ParticipantFieldOptionsProvider.php protected function registerFieldOptionProviders(): void { $this->fieldOptionProviders['newField'] = fn(BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'New Field Label', 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex), ]; } ``` ## Performance Considerations - Conditions use lazy evaluation and short-circuit logic - Field state calculations are cached during form processing - Dependency tracking prevents unnecessary re-evaluations - Bulk state calculation optimizes multiple field updates ## Integration with HTMX The system supports real-time field state updates: 1. Field changes trigger dependency re-evaluation 2. State modifications are applied via form rebuilding 3. HTMX can update field states without full page refresh 4. Dependency tracking ensures only affected fields are updated ### Service Field HTMX Integration Improvements **Critical Fix for Expanded Choice Fields**: Service fields (board, skipass, courses, etc.) use expanded choice types (checkboxes/radios) which required special HTMX trigger handling: **Issue Resolved**: HTMX attributes were originally placed on container elements (`row_attr`) which don't capture individual input changes for expanded choice fields. **Solution Implemented**: HTMX triggers must be placed directly on each individual checkbox/radio input via the `attr` configuration: ```php // Correct implementation for service fields 'attr' => [ 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), 'hx-target' => '#booking-summary', 'hx-trigger' => 'change', ], // Incorrect approach (doesn't work for expanded choices) // 'row_attr' => ['hx-post' => '...'] ``` **Benefits of the Fix**: - Real-time updates now work reliably for all service field selections - Service pricing calculations update immediately when selections change - Booking summary reflects service changes without manual form submission - Consistent HTMX behavior across all form field types **Affected Field Types**: - Board/meal selection fields - Ski pass selection fields - Course selection fields - Additional services selection fields - Rental equipment selection fields This improvement ensures that the conditional field state system works seamlessly with HTMX for all field types, providing users with immediate feedback on their selections. ## Extending the System ### Custom Conditions Create new condition classes implementing `FieldConditionInterface`: ```php // src/Form/Service/Condition/CustomBusinessRuleCondition.php use App\Form\Service\Contract\FieldConditionInterface; class CustomBusinessRuleCondition implements FieldConditionInterface { public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool { // Custom business logic here return true; } public function getDependentFields(): array { return ['fieldThatTriggersThisCondition']; } public function getDescription(): string { return 'Custom business rule description'; } } ``` ### Custom Field Options Providers To create more complex field option logic, extend `AbstractFieldOptionsProvider`: ```php // src/Form/Service/CustomFieldOptionsProvider.php use App\Form\Service\Abstract\AbstractFieldOptionsProvider; class CustomFieldOptionsProvider extends AbstractFieldOptionsProvider { protected function registerFieldOptionProviders(): void { $this->fieldOptionProviders['customField'] = fn(BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Custom Field', 'choices' => $this->generateCustomChoices($bookingDto, $participantIndex), ]; } private function generateCustomChoices(BookingDtoInterface $bookingDto, int $participantIndex): array { // Custom choice generation logic return []; } } ``` This system provides a powerful, maintainable foundation for complex conditional field behavior while maintaining clean separation of concerns and extensibility.