204 lines
8.1 KiB
PHP
204 lines
8.1 KiB
PHP
<?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;
|
|
}
|
|
}
|