239 lines
8.1 KiB
Markdown
239 lines
8.1 KiB
Markdown
# 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
|
|
|
|
## 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. |