Files
myep/docs/FIELD_STATE_SYSTEM.md
T

368 lines
13 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
### 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.
### Transportation Services HTMX Integration
Transportation services benefit significantly from the enhanced HTMX integration:
**Service-Specific Triggers**: Transportation, pickup, and parking fields use individual input-level triggers for immediate state updates:
```php
// Transportation field HTMX configuration
'attr' => [
'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'),
'hx-target' => '#booking-summary',
'hx-trigger' => 'change',
],
```
**Conditional Field Updates**: When users change transportation type:
- Pickup fields automatically show/hide based on bus vs. car selection
- Parking field visibility toggles for car transport
- Pricing updates reflect transportation service costs
- Booking summary updates in real-time
**Mutual Exclusivity**: The system ensures logical field relationships:
- Car transport → Parking field visible, pickup fields hidden
- Bus transport → Pickup fields visible, parking field hidden
- No transport selected → Both pickup and parking hidden
## 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 [];
}
}
```
## Current Implementation Examples
### Body Dimensions and Rental Insurance Conditional Fields
The current system implements sophisticated conditional field visibility for body dimensions and rental insurance:
```php
// CreateFieldStateProvider.php
protected function registerFieldStateConditions(): void
{
$rentalCondition = new RentalSelectionCondition();
// Hide body dimensions section unless rental services are selected
$this->fieldStateConditions['bodyDimensions'] = [
'hidden' => CompositeCondition::not($rentalCondition),
];
// Hide rental insurance unless rental services are selected
$this->fieldStateConditions['rentalInsurance'] = [
'hidden' => CompositeCondition::not($rentalCondition),
];
// Hide parking unless outbound transportation is PKW (car)
$this->fieldStateConditions['parking'] = [
'hidden' => CompositeCondition::not(
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API)
),
];
}
```
### Rental Insurance Checkbox Implementation
The rental insurance field demonstrates the checkbox pattern used for service selection:
```php
// Field Options Provider
$this->fieldOptionProviders['rentalInsurance'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
'label' => $this->getRentalInsuranceCheckboxLabel($rentalInsuranceServices),
'required' => false,
'property_path' => 'rentalInsuranceSelected', // Maps to boolean property
];
// Field Handler Processing
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
{
$isRentalInsuranceSelected = (bool) $this->getFieldValue($submittedData, $this->getFieldName());
// Store boolean value for form state
$participant->rentalInsuranceSelected = $isRentalInsuranceSelected;
// Store Service object for pricing calculations
if ($isRentalInsuranceSelected) {
$participant->rentalInsurance = $this->findRentalInsuranceService($bookingDto);
} else {
$participant->rentalInsurance = null;
}
}
```
### Benefits of Current Architecture
1. **Clean Separation**: Boolean properties handle form state, Service objects handle business logic
2. **Automatic Pricing Integration**: Service objects are automatically included in pricing calculations
3. **Dynamic Visibility**: Fields appear/disappear based on related selections
4. **Consistent UX**: Checkbox pattern provides intuitive user interface
5. **Validation-Free**: Conditional visibility eliminates need for complex validation rules
This system provides a powerful, maintainable foundation for complex conditional field behavior while maintaining clean separation of concerns and extensibility.