Field option providers indexed by field name */ private array $fieldOptionProviders = []; /** @var array> 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 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 */ public function __construct( private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory, ) { $this->registerFieldOptionProviders(); $this->registerFieldStateConditions(); } /** * Retrieves form field options for a specified dynamic field. * * This is the main entry point for getting field configurations. It looks up * the appropriate option provider for the field and executes it with the * current booking and participant context to generate dynamic field options. * * The returned array contains Symfony form field options such as: * - 'label' - The field label * - 'placeholder' - Placeholder text * - 'choices' - Available choices for choice fields * - 'choice_loader' - Dynamic choice loader for complex choices * - 'disabled' - Whether the field should be disabled * - 'required' - Whether the field is required * * @param string $fieldName The name of the field to configure * @param BookingDtoInterface $bookingDto The current booking data for context (create or edit) * @param int $participantIndex The index of the participant being configured * * @return array Symfony form field options, or empty array if field not supported */ public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex): array { // Check if we have a provider for this field if (!isset($this->fieldOptionProviders[$fieldName])) { return []; } // Execute the provider with current context to generate dynamic options return $this->fieldOptionProviders[$fieldName]($bookingDto, $participantIndex); } /** * Checks whether a field has option provider support. * * This method allows form builders to determine if a field can be * dynamically configured by this service. It's useful for deciding * whether to use static field options or dynamic configuration. * * @param string $fieldName The name of the field to check * * @return bool True if the field has registered option providers, false otherwise */ public function hasFieldOptions(string $fieldName): bool { return isset($this->fieldOptionProviders[$fieldName]); } /** * Registers all field option providers during service initialization. * * This method defines the option generation logic for each supported dynamic field. * Each provider is a callable that receives the booking DTO and participant * index and returns appropriate Symfony form field options. * * Adding new fields: * To add support for a new dynamic field, simply add a new provider here: * * $this->fieldOptionProviders['newField'] = fn($bookingDto, $participantIndex) => [ * 'label' => 'New Field Label', * 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex), * ]; * * Provider Pattern Benefits: * - Lazy evaluation (options only generated when needed) * - Context-aware configuration * - Easy to test individual field logic * - Supports complex interdependencies */ private function registerFieldOptionProviders(): void { // Room assignment field provider $this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Zimmer', 'placeholder' => 'Bitte wählen', // Use factory to create context-aware choice loader that: // - Shows only available rooms for this participant // - Excludes rooms already assigned to other participants // - Respects room capacity and booking constraints 'choice_loader' => $this->roomChoiceLoaderFactory->create( $bookingDto->participants, $bookingDto->getSelectedRooms(), $participantIndex ), ]; // Future field providers would be added here, for example: // // $this->fieldOptionProviders['mealPreference'] = fn($bookingDto, $participantIndex) => [ // 'label' => 'Meal Preference', // 'choices' => [ // 'Standard' => 'standard', // 'Vegetarian' => 'vegetarian', // 'Vegan' => 'vegan', // ], // ]; } /** * 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 BookingDtoInterface $bookingDto The current booking data for context (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data for condition evaluation * * @return array Symfony form field options for state modifications */ public function getFieldState(string $fieldName, BookingDtoInterface $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 BookingDtoInterface $bookingDto The current booking data for context (create or edit) * @param int $participantIndex The index of the participant being evaluated * @param array $formData Current form data for condition evaluation * * @return array> Field states indexed by field name */ public function getAllFieldStates(BookingDtoInterface $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') // ), // ]; } }