$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 $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; } }