wip: refactor forms
This commit is contained in:
@@ -66,3 +66,12 @@ services:
|
|||||||
App\Form\Service\ParticipantRoomChoiceLoaderFactory:
|
App\Form\Service\ParticipantRoomChoiceLoaderFactory:
|
||||||
arguments:
|
arguments:
|
||||||
$choiceListFactory: '@form.choice_list_factory.default'
|
$choiceListFactory: '@form.choice_list_factory.default'
|
||||||
|
|
||||||
|
# Participant Field Handler Registry with hybrid handler configuration
|
||||||
|
App\Form\ParticipantFieldHandler\ParticipantFieldHandlerRegistry:
|
||||||
|
arguments:
|
||||||
|
$handlers:
|
||||||
|
# Simple handlers (no dependencies) - use class names
|
||||||
|
- 'App\Form\ParticipantFieldHandler\ParticipantAssignedRoomFieldHandler'
|
||||||
|
# Complex handlers (with dependencies) would use service references like:
|
||||||
|
# - '@participant.complex.handler'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace App\Form;
|
|||||||
use App\BusProNet\Form\CountryType;
|
use App\BusProNet\Form\CountryType;
|
||||||
use App\Form\Model\BookingCreateDto;
|
use App\Form\Model\BookingCreateDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Form\Service\ParticipantFormConfigurator;
|
use App\Form\Service\ParticipantFieldOptionsProvider;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
|
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
@@ -14,12 +14,13 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
|
|||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\Form\FormEvent;
|
use Symfony\Component\Form\FormEvent;
|
||||||
use Symfony\Component\Form\FormEvents;
|
use Symfony\Component\Form\FormEvents;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
class BookingCreateParticipantType extends AbstractType
|
class BookingCreateParticipantType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ParticipantFormConfigurator $formConfigurator,
|
private readonly ParticipantFieldOptionsProvider $fieldOptionsProvider,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +67,14 @@ class BookingCreateParticipantType extends AbstractType
|
|||||||
'clean_xss' => true,
|
'clean_xss' => true,
|
||||||
])
|
])
|
||||||
->add('bodyDimensions', BodyDimensionsType::class)
|
->add('bodyDimensions', BodyDimensionsType::class)
|
||||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
|
->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'onPreSetData']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds dynamic fields to the form based on participant data.
|
||||||
|
*/
|
||||||
|
public function onPreSetData(FormEvent $event): void
|
||||||
|
{
|
||||||
/** @var ParticipantDto|null $participantData */
|
/** @var ParticipantDto|null $participantData */
|
||||||
$participantData = $event->getData();
|
$participantData = $event->getData();
|
||||||
$form = $event->getForm();
|
$form = $event->getForm();
|
||||||
@@ -84,9 +92,22 @@ class BookingCreateParticipantType extends AbstractType
|
|||||||
/** @var BookingCreateDto $bookingCreateDto */
|
/** @var BookingCreateDto $bookingCreateDto */
|
||||||
$bookingCreateDto = $rootForm->getData();
|
$bookingCreateDto = $rootForm->getData();
|
||||||
|
|
||||||
$roomOptions = $this->formConfigurator->getRoomFieldOptions($bookingCreateDto, $participantData->index);
|
$this->addDynamicFields($form, $bookingCreateDto, $participantData->index);
|
||||||
$form->add('assignedRoomId', ChoiceType::class, $roomOptions);
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* Adds all configured dynamic fields to the form.
|
||||||
|
*/
|
||||||
|
private function addDynamicFields(FormInterface $form, BookingCreateDto $bookingCreateDto, int $participantIndex): void
|
||||||
|
{
|
||||||
|
$dynamicFields = ['assignedRoomId']; // List of fields that need dynamic configuration
|
||||||
|
|
||||||
|
foreach ($dynamicFields as $fieldName) {
|
||||||
|
if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) {
|
||||||
|
$fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingCreateDto, $participantIndex);
|
||||||
|
$form->add($fieldName, ChoiceType::class, $fieldOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Form;
|
namespace App\Form;
|
||||||
|
|
||||||
use App\Form\Model\BookingCreateDto;
|
use App\Form\Model\BookingCreateDto;
|
||||||
|
use App\Form\ParticipantFieldHandler\ParticipantFieldHandlerRegistry;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@@ -13,6 +14,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
|
|
||||||
class BookingCreateStep2Type extends AbstractType
|
class BookingCreateStep2Type extends AbstractType
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ParticipantFieldHandlerRegistry $participantFieldHandlerRegistry,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
@@ -35,9 +41,9 @@ class BookingCreateStep2Type extends AbstractType
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles dynamic updates on POST requests (e.g., from HTMX).
|
* Handles dynamic participant form field updates on POST requests (e.g., from HTMX).
|
||||||
*
|
*
|
||||||
* This listener synchronizes the DTO with the submitted data *before*
|
* This listener synchronizes the BookingCreateDto with the submitted participant data *before*
|
||||||
* the form's children are processed. It then rebuilds the participants
|
* the form's children are processed. It then rebuilds the participants
|
||||||
* field to ensure choice loaders are created with the fresh state.
|
* field to ensure choice loaders are created with the fresh state.
|
||||||
*/
|
*/
|
||||||
@@ -49,21 +55,10 @@ class BookingCreateStep2Type extends AbstractType
|
|||||||
/** @var BookingCreateDto $bookingDto */
|
/** @var BookingCreateDto $bookingDto */
|
||||||
$bookingDto = $form->getData();
|
$bookingDto = $form->getData();
|
||||||
|
|
||||||
// If participant data isn't in the submission, we can't do anything.
|
// Process all registered participant field handlers
|
||||||
if (!isset($submittedData['participants']) || !is_array($submittedData['participants'])) {
|
$this->participantFieldHandlerRegistry->processFields($submittedData, $bookingDto);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manually update the DTO with the submitted room assignments.
|
// Rebuild the 'participants' field with the updated DTO.
|
||||||
foreach ($submittedData['participants'] as $index => $participantData) {
|
|
||||||
if (isset($participantData['assignedRoomId']) && isset($bookingDto->participants[$index])) {
|
|
||||||
$roomId = $participantData['assignedRoomId'];
|
|
||||||
// An unselected choice submits an empty string.
|
|
||||||
$bookingDto->participants[$index]->assignedRoomId = empty($roomId) ? null : (int)$roomId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, rebuild the 'participants' field with the updated DTO.
|
|
||||||
$this->addParticipantsField($form);
|
$this->addParticipantsField($form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\ParticipantFieldHandler;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingCreateDto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class providing common functionality for participant field handlers.
|
||||||
|
*
|
||||||
|
* This class implements common patterns used across participant field handlers,
|
||||||
|
* reducing boilerplate code in concrete implementations.
|
||||||
|
*/
|
||||||
|
abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the field names this handler depends on.
|
||||||
|
*
|
||||||
|
* Default implementation returns an empty array, meaning no dependencies.
|
||||||
|
* Override this method in concrete handlers that depend on other fields
|
||||||
|
* being processed first (e.g., a meal preference handler might depend on
|
||||||
|
* the room assignment being processed first).
|
||||||
|
*
|
||||||
|
* @return string[] Array of field names that must be processed before this handler
|
||||||
|
*/
|
||||||
|
public function getDependencies(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this handler should process the field based on submitted data.
|
||||||
|
*
|
||||||
|
* Default implementation checks if the field exists in the submitted data.
|
||||||
|
* Override this method for more complex processing conditions (e.g., only
|
||||||
|
* process if certain other conditions are met).
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param int $participantIndex The index of the participant being processed
|
||||||
|
*
|
||||||
|
* @return bool True if the handler should process this field, false otherwise
|
||||||
|
*/
|
||||||
|
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
||||||
|
{
|
||||||
|
return isset($submittedData[$this->getFieldName()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely retrieves a participant object from the booking DTO.
|
||||||
|
*
|
||||||
|
* This helper method provides safe access to participant data by checking
|
||||||
|
* if the participant exists at the given index. Returns null if the
|
||||||
|
* participant doesn't exist, preventing array access errors.
|
||||||
|
*
|
||||||
|
* @param BookingCreateDto $bookingDto The booking DTO containing participants
|
||||||
|
* @param int $participantIndex The index of the participant to retrieve
|
||||||
|
*
|
||||||
|
* @return object|null The participant object, or null if not found
|
||||||
|
*/
|
||||||
|
protected function getParticipant(BookingCreateDto $bookingDto, int $participantIndex): ?object
|
||||||
|
{
|
||||||
|
return $bookingDto->participants[$participantIndex] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely extracts a field value from submitted participant data.
|
||||||
|
*
|
||||||
|
* This helper method provides safe access to form field values using
|
||||||
|
* the null coalescing operator. Useful for extracting field values
|
||||||
|
* without worrying about undefined array keys.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $fieldName The name of the field to retrieve
|
||||||
|
* @param mixed $default The default value to return if field is not set
|
||||||
|
*
|
||||||
|
* @return mixed The field value, or the default value if not found
|
||||||
|
*/
|
||||||
|
protected function getFieldValue(array $submittedData, string $fieldName, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $submittedData[$fieldName] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes empty string values to null.
|
||||||
|
*
|
||||||
|
* Form inputs often submit empty strings for unselected/empty fields.
|
||||||
|
* This helper converts those empty strings to null values, which is
|
||||||
|
* typically more appropriate for database storage and business logic.
|
||||||
|
*
|
||||||
|
* @param mixed $value The value to normalize
|
||||||
|
*
|
||||||
|
* @return mixed The normalized value (null if empty, original value otherwise)
|
||||||
|
*/
|
||||||
|
protected function normalizeEmptyValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
return empty($value) ? null : $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts and normalizes string values to integers.
|
||||||
|
*
|
||||||
|
* Form inputs submit all values as strings. This helper safely converts
|
||||||
|
* string values to integers while handling empty strings and non-numeric
|
||||||
|
* values gracefully by returning null.
|
||||||
|
*
|
||||||
|
* @param mixed $value The value to convert to integer
|
||||||
|
*
|
||||||
|
* @return int|null The integer value, or null if empty/non-numeric
|
||||||
|
*/
|
||||||
|
protected function normalizeIntValue(mixed $value): ?int
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_numeric($value) ? (int) $value : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\ParticipantFieldHandler;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingCreateDto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles processing of the assignedRoomId field for booking participants.
|
||||||
|
*
|
||||||
|
* This handler manages room assignments for participants in the booking creation
|
||||||
|
* process. It processes the assignedRoomId field from form submissions and updates
|
||||||
|
* the participant DTO with the selected room. The handler properly handles empty
|
||||||
|
* selections (converting them to null) and validates numeric room IDs.
|
||||||
|
*
|
||||||
|
* Field Processing:
|
||||||
|
* - Extracts room ID from submitted form data
|
||||||
|
* - Converts empty strings to null (unselected choice)
|
||||||
|
* - Normalizes string values to integers
|
||||||
|
* - Updates the participant's assignedRoomId property
|
||||||
|
*
|
||||||
|
* Dependencies: None (this is a base field that other handlers may depend on)
|
||||||
|
*/
|
||||||
|
class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the form field name this handler processes.
|
||||||
|
*
|
||||||
|
* This handler is responsible for the 'assignedRoomId' field, which contains
|
||||||
|
* the selected room ID for each participant in the booking form.
|
||||||
|
*
|
||||||
|
* @return string The field name 'assignedRoomId'
|
||||||
|
*/
|
||||||
|
public function getFieldName(): string
|
||||||
|
{
|
||||||
|
return 'assignedRoomId';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the assignedRoomId field for a specific participant.
|
||||||
|
*
|
||||||
|
* This method extracts the room assignment from the submitted form data and
|
||||||
|
* updates the corresponding participant in the booking DTO. It handles the
|
||||||
|
* common form processing pattern where empty selections are submitted as
|
||||||
|
* empty strings but should be stored as null values.
|
||||||
|
*
|
||||||
|
* Processing steps:
|
||||||
|
* 1. Safely retrieves the participant object from the DTO
|
||||||
|
* 2. Extracts the assignedRoomId value from submitted data
|
||||||
|
* 3. Normalizes the value (empty string → null, numeric string → integer)
|
||||||
|
* 4. Updates the participant's assignedRoomId property
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param BookingCreateDto $bookingDto The booking DTO to update
|
||||||
|
* @param int $participantIndex The index of the participant being processed
|
||||||
|
*/
|
||||||
|
public function processField(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): void
|
||||||
|
{
|
||||||
|
// Safely get the participant object, returning early if not found
|
||||||
|
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||||
|
|
||||||
|
if (null === $participant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the room ID from form data (defaults to null if not present)
|
||||||
|
$roomId = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||||
|
|
||||||
|
// Convert form string to integer, handling empty selections as null
|
||||||
|
$participant->assignedRoomId = $this->normalizeIntValue($roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\ParticipantFieldHandler;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingCreateDto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for handling dynamic participant form field processing.
|
||||||
|
*
|
||||||
|
* Participant field handlers are responsible for processing submitted form data
|
||||||
|
* for booking participants and updating the BookingCreateDto accordingly.
|
||||||
|
* They support dependency management to ensure fields are processed in the correct order.
|
||||||
|
*/
|
||||||
|
interface ParticipantFieldHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the field name this handler processes.
|
||||||
|
*/
|
||||||
|
public function getFieldName(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns field names this handler depends on.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getDependencies(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the participant field data from submitted form data and updates the DTO.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param BookingCreateDto $bookingDto The booking DTO to update
|
||||||
|
* @param int $participantIndex The participant index being processed
|
||||||
|
*/
|
||||||
|
public function processField(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this handler should process the field based on submitted participant data.
|
||||||
|
*/
|
||||||
|
public function shouldProcess(array $submittedData, int $participantIndex): bool;
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\ParticipantFieldHandler;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingCreateDto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry for managing and executing participant field handlers in dependency order.
|
||||||
|
*
|
||||||
|
* This class ensures that participant field handlers are executed in the correct order
|
||||||
|
* based on their dependencies, preventing issues where a field depends on
|
||||||
|
* another field that hasn't been processed yet.
|
||||||
|
*/
|
||||||
|
class ParticipantFieldHandlerRegistry
|
||||||
|
{
|
||||||
|
/** @var ParticipantFieldHandlerInterface[] Registered handlers indexed by field name */
|
||||||
|
private array $handlers = [];
|
||||||
|
|
||||||
|
/** @var string[]|null Cached array of handler names sorted by dependency order */
|
||||||
|
private ?array $sortedHandlers = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the registry with a hybrid array of handlers.
|
||||||
|
*
|
||||||
|
* This constructor supports both simple handlers (passed as class names) and
|
||||||
|
* complex handlers (passed as instantiated service objects). Simple handlers
|
||||||
|
* with no dependencies can be passed as strings and will be instantiated
|
||||||
|
* automatically. Complex handlers with dependencies should be passed as
|
||||||
|
* already-instantiated objects via dependency injection.
|
||||||
|
*
|
||||||
|
* @param array<ParticipantFieldHandlerInterface|string> $handlers Array of handler instances or class names
|
||||||
|
*
|
||||||
|
* @throws \Exception If a class name cannot be instantiated
|
||||||
|
*/
|
||||||
|
public function __construct(array $handlers)
|
||||||
|
{
|
||||||
|
foreach ($handlers as $handler) {
|
||||||
|
if (is_string($handler)) {
|
||||||
|
// It's a class name - instantiate it (for simple handlers without dependencies)
|
||||||
|
$handler = new $handler();
|
||||||
|
}
|
||||||
|
// If it's already an object (service), use as-is (for complex handlers with dependencies)
|
||||||
|
$this->addHandler($handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a participant field handler with the registry.
|
||||||
|
*
|
||||||
|
* Handlers are indexed by their field name to ensure uniqueness and enable
|
||||||
|
* fast lookups. Adding a handler invalidates the dependency sort cache,
|
||||||
|
* forcing a re-sort on the next processing request.
|
||||||
|
*
|
||||||
|
* @param ParticipantFieldHandlerInterface $handler The handler to register
|
||||||
|
*/
|
||||||
|
public function addHandler(ParticipantFieldHandlerInterface $handler): void
|
||||||
|
{
|
||||||
|
$this->handlers[$handler->getFieldName()] = $handler;
|
||||||
|
$this->sortedHandlers = null; // Reset cache to force re-sorting with new handler
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes all participant fields from submitted form data using registered handlers.
|
||||||
|
*
|
||||||
|
* This is the main entry point for field processing. It iterates through all
|
||||||
|
* participants in the submitted data and applies the appropriate handlers in
|
||||||
|
* dependency order. Each handler determines whether it should process the
|
||||||
|
* participant's data and updates the booking DTO accordingly.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $submittedData The submitted form data containing participants array
|
||||||
|
* @param BookingCreateDto $bookingDto The booking DTO to update with processed field values
|
||||||
|
*/
|
||||||
|
public function processFields(array $submittedData, BookingCreateDto $bookingDto): void
|
||||||
|
{
|
||||||
|
// Early return if no participant data exists in submission
|
||||||
|
if (!isset($submittedData['participants']) || !is_array($submittedData['participants'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get handlers sorted by dependency order (uses cache if available)
|
||||||
|
$sortedHandlerNames = $this->getSortedHandlers();
|
||||||
|
|
||||||
|
// Process each participant's data
|
||||||
|
foreach ($submittedData['participants'] as $participantIndex => $participantData) {
|
||||||
|
// Skip invalid participant data
|
||||||
|
if (!is_array($participantData)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply each handler in dependency order
|
||||||
|
foreach ($sortedHandlerNames as $handlerName) {
|
||||||
|
$handler = $this->handlers[$handlerName];
|
||||||
|
|
||||||
|
// Let each handler decide if it should process this participant's data
|
||||||
|
if ($handler->shouldProcess($participantData, (int) $participantIndex)) {
|
||||||
|
$handler->processField($participantData, $bookingDto, (int) $participantIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns handler names sorted by dependency order using cached results when possible.
|
||||||
|
*
|
||||||
|
* This method implements lazy loading with caching for performance. The dependency
|
||||||
|
* sort is only performed once and the result is cached until handlers are added
|
||||||
|
* or modified.
|
||||||
|
*
|
||||||
|
* @return string[] Array of handler field names in dependency execution order
|
||||||
|
*/
|
||||||
|
private function getSortedHandlers(): array
|
||||||
|
{
|
||||||
|
// Return cached result if available
|
||||||
|
if (null !== $this->sortedHandlers) {
|
||||||
|
return $this->sortedHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform topological sort and cache the result
|
||||||
|
$this->sortedHandlers = $this->topologicalSort();
|
||||||
|
|
||||||
|
return $this->sortedHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs topological sort to determine safe handler execution order.
|
||||||
|
*
|
||||||
|
* This method uses Kahn's algorithm to sort handlers based on their declared
|
||||||
|
* dependencies. It ensures that no handler is executed before its dependencies
|
||||||
|
* have been processed, preventing data consistency issues.
|
||||||
|
*
|
||||||
|
* The algorithm works by:
|
||||||
|
* 1. Building a dependency graph of handlers
|
||||||
|
* 2. Finding handlers with no dependencies (in-degree = 0)
|
||||||
|
* 3. Iteratively removing handlers and updating dependencies
|
||||||
|
* 4. Detecting circular dependencies (deadlock prevention)
|
||||||
|
*
|
||||||
|
* @return string[] Array of handler field names in safe execution order
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException When a handler depends on a non-existent handler
|
||||||
|
* @throws \InvalidArgumentException When circular dependencies are detected
|
||||||
|
*/
|
||||||
|
private function topologicalSort(): array
|
||||||
|
{
|
||||||
|
$inDegree = []; // Count of dependencies for each handler
|
||||||
|
$graph = []; // Adjacency list of handler dependencies
|
||||||
|
$handlerNames = array_keys($this->handlers);
|
||||||
|
|
||||||
|
// Initialize all handlers with zero dependencies
|
||||||
|
foreach ($handlerNames as $handlerName) {
|
||||||
|
$inDegree[$handlerName] = 0;
|
||||||
|
$graph[$handlerName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build dependency graph by examining each handler's dependencies
|
||||||
|
foreach ($this->handlers as $handlerName => $handler) {
|
||||||
|
foreach ($handler->getDependencies() as $dependency) {
|
||||||
|
// Validate that the dependency exists
|
||||||
|
if (!isset($this->handlers[$dependency])) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('Handler "%s" depends on unknown handler "%s"', $handlerName, $dependency));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add edge from dependency to dependent handler
|
||||||
|
$graph[$dependency][] = $handlerName;
|
||||||
|
++$inDegree[$handlerName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kahn's topological sort algorithm
|
||||||
|
$queue = []; // Handlers ready to be processed (no remaining dependencies)
|
||||||
|
$result = []; // Final sorted order
|
||||||
|
|
||||||
|
// Start with handlers that have no dependencies
|
||||||
|
foreach ($inDegree as $handlerName => $degree) {
|
||||||
|
if (0 === $degree) {
|
||||||
|
$queue[] = $handlerName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process handlers in dependency order
|
||||||
|
while (!empty($queue)) {
|
||||||
|
$current = array_shift($queue);
|
||||||
|
$result[] = $current;
|
||||||
|
|
||||||
|
// Remove this handler's dependencies from dependent handlers
|
||||||
|
foreach ($graph[$current] as $dependent) {
|
||||||
|
--$inDegree[$dependent];
|
||||||
|
// If dependent now has no remaining dependencies, add to queue
|
||||||
|
if (0 === $inDegree[$dependent]) {
|
||||||
|
$queue[] = $dependent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect circular dependencies (if not all handlers were processed)
|
||||||
|
if (count($result) !== count($handlerNames)) {
|
||||||
|
throw new \InvalidArgumentException('Circular dependency detected in participant field handlers');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Service;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingCreateDto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides dynamic field options 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.
|
||||||
|
*
|
||||||
|
* Key Responsibilities:
|
||||||
|
* - Manages field option providers for dynamic fields
|
||||||
|
* - Provides context-aware field configurations
|
||||||
|
* - Handles interdependent field relationships
|
||||||
|
* - Supports extensible field option 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.
|
||||||
|
*/
|
||||||
|
class ParticipantFieldOptionsProvider
|
||||||
|
{
|
||||||
|
/** @var array<string, callable> Field option providers indexed by field name */
|
||||||
|
private array $fieldOptionProviders = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
|
||||||
|
) {
|
||||||
|
$this->registerFieldOptionProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 BookingCreateDto $bookingDto The current booking data for context
|
||||||
|
* @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, BookingCreateDto $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 (BookingCreateDto $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',
|
||||||
|
// ],
|
||||||
|
// // Could depend on room assignment or other factors
|
||||||
|
// 'disabled' => !$this->hasMealOptions($bookingDto, $participantIndex),
|
||||||
|
// ];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Form\Service;
|
|
||||||
|
|
||||||
use App\Form\Model\BookingCreateDto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Central service for configuring dynamic fields in the participant form.
|
|
||||||
*
|
|
||||||
* This class encapsulates all business logic for determining field options,
|
|
||||||
* choices, and states (e.g., visibility, disabled status) based on the
|
|
||||||
* overall booking state and individual participant data.
|
|
||||||
*/
|
|
||||||
class ParticipantFormConfigurator
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the complete form options for the 'assignedRoomId' field.
|
|
||||||
*/
|
|
||||||
public function getRoomFieldOptions(BookingCreateDto $bookingDto, int $participantIndex): array
|
|
||||||
{
|
|
||||||
$choiceLoader = $this->roomChoiceLoaderFactory->create(
|
|
||||||
$bookingDto->participants,
|
|
||||||
$bookingDto->getSelectedRooms(),
|
|
||||||
$participantIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'label' => 'Zimmer',
|
|
||||||
'placeholder' => 'Bitte wählen',
|
|
||||||
'choice_loader' => $choiceLoader,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user