Files
myep/src/Form/ParticipantFieldHandler/ParticipantFieldHandlerRegistry.php
T
2026-03-16 11:59:09 +01:00

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