wip: field states and dependencies

This commit is contained in:
Björn Fromme
2026-03-16 11:59:09 +01:00
parent cd7a724e62
commit d0d6844fbd
10 changed files with 1302 additions and 22 deletions
@@ -5,36 +5,40 @@ declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingCreateDto;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
/**
* Provides dynamic field options for participant form fields.
* Provides dynamic field options and state for participant form fields.
*
* This service acts as a field option provider registry for the participant form
* system. It generates context-aware Symfony form field options based on the
* overall booking context and individual participant data.
* This service acts as both a field option provider and field state provider
* for the participant form system. It generates context-aware Symfony form
* field options and calculates dynamic field states based on conditional logic.
*
* Key Responsibilities:
* - Manages field option providers for dynamic fields
* - Calculates field states based on conditional logic
* - Provides context-aware field configurations
* - Handles interdependent field relationships
* - Supports extensible field option generation
* - Supports extensible field option and state generation
*
* The provider uses a callable pattern where each field has a function
* that generates the appropriate Symfony form options based on the current
* booking and participant state.
* The provider uses a callable pattern for field options and a condition-based
* system for field states, enabling complex conditional field behavior.
*/
class ParticipantFieldOptionsProvider
class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
{
/** @var array<string, callable> Field option providers indexed by field name */
private array $fieldOptionProviders = [];
/** @var array<string, array<string, FieldConditionInterface>> Field state conditions indexed by field name and state type */
private array $fieldStateConditions = [];
/**
* Initializes the provider with required dependencies.
*
* The provider automatically registers all field option providers
* during construction to ensure they're available for form building.
* This approach keeps all field configuration logic centralized and
* makes it easy to add new dynamic fields.
* The provider automatically registers all field option providers and
* field state conditions during construction to ensure they're available
* for form building. This approach keeps all field configuration logic
* centralized and makes it easy to add new dynamic fields.
*
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
*/
@@ -42,6 +46,7 @@ class ParticipantFieldOptionsProvider
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
) {
$this->registerFieldOptionProviders();
$this->registerFieldStateConditions();
}
/**
@@ -139,8 +144,160 @@ class ParticipantFieldOptionsProvider
// 'Vegetarian' => 'vegetarian',
// 'Vegan' => 'vegan',
// ],
// // Could depend on room assignment or other factors
// 'disabled' => !$this->hasMealOptions($bookingDto, $participantIndex),
// ];
}
}
/**
* Calculates the dynamic state for a specified field.
*
* Evaluates all configured conditions for a field and returns the appropriate
* state modifications. State conditions are organized by state type (readonly,
* disabled, etc.) and evaluated independently.
*
* @param string $fieldName The name of the field to evaluate
* @param BookingCreateDto $bookingDto The current booking data for context
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, mixed> Symfony form field options for state modifications
*/
public function getFieldState(string $fieldName, BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array
{
if (!isset($this->fieldStateConditions[$fieldName])) {
return [];
}
$stateModifications = [];
$attributes = [];
foreach ($this->fieldStateConditions[$fieldName] as $stateType => $condition) {
if ($condition->evaluate($bookingDto, $participantIndex, $formData)) {
switch ($stateType) {
case 'readonly':
$attributes['readonly'] = true;
break;
case 'disabled':
$stateModifications['disabled'] = true;
break;
case 'required':
$stateModifications['required'] = true;
break;
case 'hidden':
$attributes['style'] = ($attributes['style'] ?? '').' display: none;';
break;
}
}
}
if (!empty($attributes)) {
$stateModifications['attr'] = $attributes;
}
return $stateModifications;
}
/**
* Checks whether a field has state conditions configured.
*
* @param string $fieldName The name of the field to check
*
* @return bool True if the field has state conditions configured
*/
public function hasStateConditions(string $fieldName): bool
{
return isset($this->fieldStateConditions[$fieldName]) && !empty($this->fieldStateConditions[$fieldName]);
}
/**
* Returns field names that trigger state re-evaluation for a given field.
*
* @param string $fieldName The name of the field to get dependencies for
*
* @return string[] Array of field names that affect the specified field's state
*/
public function getFieldStateDependencies(string $fieldName): array
{
if (!isset($this->fieldStateConditions[$fieldName])) {
return [];
}
$dependencies = [];
foreach ($this->fieldStateConditions[$fieldName] as $condition) {
$dependencies = array_merge($dependencies, $condition->getDependentFields());
}
return array_unique($dependencies);
}
/**
* Calculates field states for all configured fields at once.
*
* @param BookingCreateDto $bookingDto The current booking data for context
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, array<string, mixed>> Field states indexed by field name
*/
public function getAllFieldStates(BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array
{
$allStates = [];
foreach (array_keys($this->fieldStateConditions) as $fieldName) {
$fieldState = $this->getFieldState($fieldName, $bookingDto, $participantIndex, $formData);
if (!empty($fieldState)) {
$allStates[$fieldName] = $fieldState;
}
}
return $allStates;
}
/**
* Registers field state conditions during service initialization.
*
* This method defines the conditional logic for field states. Each field
* can have multiple state conditions (readonly, disabled, hidden, required)
* that are evaluated independently.
*
* Adding new field state conditions:
* To add conditional state logic for a field, register conditions here:
*
* $this->fieldStateConditions['fieldName'] = [
* 'readonly' => new SomeCondition(),
* 'disabled' => CompositeCondition::and(
* new AgeRangeCondition(null, 17),
* FieldValueCondition::equals('someField', 'someValue')
* ),
* ];
*
* Condition Types:
* - 'readonly': Field is visible but not editable
* - 'disabled': Field interaction is disabled
* - 'required': Field becomes mandatory
* - 'hidden': Field is not displayed
*/
private function registerFieldStateConditions(): void
{
// Example field state conditions would be registered here
// For demonstration purposes, here are some example patterns:
// Example 1: Make assignedRoomId readonly for participants under 18
// $this->fieldStateConditions['assignedRoomId'] = [
// 'readonly' => new AgeRangeCondition(null, 17),
// ];
// Example 2: Disable service selection if no room is assigned
// $this->fieldStateConditions['serviceSelection'] = [
// 'disabled' => FieldValueCondition::isEmpty('assignedRoomId'),
// ];
// Example 3: Complex condition with multiple criteria
// $this->fieldStateConditions['advancedOptions'] = [
// 'hidden' => CompositeCondition::or(
// new AgeRangeCondition(null, 15),
// FieldValueCondition::equals('userType', 'basic')
// ),
// ];
}
}