304 lines
12 KiB
PHP
304 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\Service;
|
|
|
|
use App\Form\Model\BookingDtoInterface;
|
|
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
|
|
|
|
/**
|
|
* Provides dynamic field options and state for participant form fields.
|
|
*
|
|
* 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 and state generation
|
|
*
|
|
* The provider uses a callable pattern for field options and a condition-based
|
|
* system for field states, enabling complex conditional field behavior.
|
|
*/
|
|
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 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<string, mixed> 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<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, 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<string, mixed> $formData Current form data for condition evaluation
|
|
*
|
|
* @return array<string, array<string, mixed>> 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')
|
|
// ),
|
|
// ];
|
|
}
|
|
}
|