9.5 KiB
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/)
- FieldStateProviderInterface - Defines the contract for field state management
- FieldOptionsProviderInterface - Defines the contract for field option generation
Abstract Base Classes (src/Form/Service/Abstract/)
- AbstractFieldStateProvider - Common field state functionality
- AbstractFieldOptionsProvider - Common field option functionality
Concrete Implementations (src/Form/Service/)
- CreateFieldStateProvider - Field states for booking creation workflow
- EditFieldStateProvider - Field states for booking edit workflow
- ParticipantFieldOptionsProvider - Dynamic field option generation
Condition System (src/Form/Service/Condition/)
- FieldConditionInterface - Defines the contract for condition evaluation
- Concrete Conditions - Implement specific business logic (age ranges, field values, etc.)
- CompositeCondition - Combines conditions with AND/OR/NOT logic
Form Integration
- BookingCreateParticipantType - Uses CreateFieldStateProvider
- BookingEditParticipantType - Uses EditFieldStateProvider
Usage Examples
Basic Age-Based Condition
// Make a field readonly for participants under 18
$this->fieldStateConditions['serviceSelection'] = [
'readonly' => new AgeRangeCondition(null, 17),
];
Field Dependency Condition
// Disable field if room is not assigned
$this->fieldStateConditions['mealPreference'] = [
'disabled' => FieldValueCondition::empty('assignedRoomId'),
];
Complex Composite Condition
// 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
// 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 oldnew AgeRangeCondition(null, 17)- Under 18 years oldnew AgeRangeCondition(18, 65)- Between 18 and 65 years old
FieldValueCondition
FieldValueCondition::equals('field', 'value')- Field equals specific valueFieldValueCondition::notEquals('field', 'value')- Field does not equal valueFieldValueCondition::in('field', ['a', 'b'])- Field value is in arrayFieldValueCondition::empty('field')- Field is empty or nullFieldValueCondition::isNotEmpty('field')- Field has a value
CompositeCondition
CompositeCondition::and($cond1, $cond2)- All conditions must be trueCompositeCondition::or($cond1, $cond2)- At least one condition must be trueCompositeCondition::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:
// 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:
// 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:
// 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:
- Field changes trigger dependency re-evaluation
- State modifications are applied via form rebuilding
- HTMX can update field states without full page refresh
- 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:
// 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:
// 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:
// 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.