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