(A => FALSE, B => TRUE, C => FALSE) * B => (A => FALSE, B => FALSE, C => FALSE) * C => (A => TRUE, B => FALSE, C => FALSE) * * A depends on B, C depends on A, B is independent */ readonly class DependencyOrderingService { /** * Order items by specified dependencies before/after * * The dependencies of an items are specified as: * 'someItemKey' => [ * 'before' => ['someItemKeyA', 'someItemKeyB'] * 'after' => ['someItemKeyC'] * ] * * If your items use different keys for specifying the relations, you can define the appropriate keys * by setting the $beforeKey and $afterKey parameters accordingly. * * @param string $beforeKey The key to use in a dependency which specifies the "before"-relation. eg. 'sortBefore', 'loadBefore' * @param string $afterKey The key to use in a dependency which specifies the "after"-relation. eg. 'sortAfter', 'loadAfter' */ public function orderByDependencies(array $items, string $beforeKey = 'before', string $afterKey = 'after'): array { $graph = $this->buildDependencyGraph($items, $beforeKey, $afterKey); $sortedItems = []; foreach ($this->calculateOrder($graph) as $id) { if (isset($items[$id])) { $sortedItems[$id] = $items[$id]; } } return $sortedItems; } /** * Builds the dependency graph for the given dependencies * * The dependencies have to specified in the following structure: * * ``` * $dependencies = [ * 'someKey' => [ * 'before' => ['someKeyA', 'someKeyB'] * 'after' => ['someKeyC'] * ] * ] * ``` * * We interpret a dependency like * * ``` * 'A' => [ * 'before' => ['B'], * 'after' => ['C', 'D'] * ] * ``` * * as * - A depends on C * - A depends on D * - B depends on A * * @param array $dependencies * @param string $beforeKey The key to use in a dependency which specifies the "before"-relation. eg. 'sortBefore', 'loadBefore' * @param string $afterKey The key to use in a dependency which specifies the "after"-relation. eg. 'sortAfter', 'loadAfter' * @return array> The dependency graph */ public function buildDependencyGraph(array $dependencies, string $beforeKey = 'before', string $afterKey = 'after'): array { $dependencies = $this->prepareDependencies($dependencies, $beforeKey, $afterKey); $identifiers = array_keys($dependencies); sort($identifiers); // $dependencyGraph is the adjacency matrix as two-dimensional array initialized to FALSE (empty graph) /** @var array> $dependencyGraph */ $dependencyGraph = array_fill_keys($identifiers, array_fill_keys($identifiers, false)); foreach ($identifiers as $id) { foreach ($dependencies[$id][$beforeKey] as $beforeId) { $dependencyGraph[$beforeId][$id] = true; } foreach ($dependencies[$id][$afterKey] as $afterId) { $dependencyGraph[$id][$afterId] = true; } } // @internal PackageManager // this is a dirty special case for suggestion handling of packages // see \TYPO3\CMS\Core\Package\PackageManager::convertConfigurationForGraph for details // DO NOT use this for any other case foreach ($identifiers as $id) { if (isset($dependencies[$id]['after-resilient'])) { foreach ($dependencies[$id]['after-resilient'] as $afterId) { $reverseDependencies = $this->findPathInGraph($dependencyGraph, $afterId, $id); if (empty($reverseDependencies)) { $dependencyGraph[$id][$afterId] = true; } } } } return $dependencyGraph; } /** * Calculate an ordered list for a dependencyGraph * * @param bool[][] $dependencyGraph * @return mixed[] Sorted array of keys of $dependencies */ public function calculateOrder(array $dependencyGraph): array { $rootIds = array_flip($this->findRootIds($dependencyGraph)); // Add number of dependencies for each root node foreach ($rootIds as $id => &$dependencies) { $dependencies = count(array_filter($dependencyGraph[$id])); } unset($dependencies); // This will contain our final result in reverse order, // meaning a result of [A, B, C] equals "A after B after C" $sortedIds = []; // Walk through the graph, level by level while (!empty($rootIds)) { ksort($rootIds); // We take those with fewer dependencies first, to have them at the end of the list in the final result. $minimum = PHP_INT_MAX; $currentId = 0; foreach ($rootIds as $id => $count) { if ($count <= $minimum) { $minimum = $count; $currentId = $id; } } unset($rootIds[$currentId]); $sortedIds[] = $currentId; // Process the dependencies of the current node foreach (array_filter($dependencyGraph[$currentId] ?? []) as $dependingId => $_) { // Remove the edge to this dependency $dependencyGraph[$currentId][$dependingId] = false; if (!$this->getIncomingEdgeCount($dependencyGraph, (string)$dependingId)) { // We found a new root, lets add it to the list $rootIds[$dependingId] = count(array_filter($dependencyGraph[$dependingId] ?? [])); } } } // Check for remaining edges in the graph $cycles = []; array_walk($dependencyGraph, static function ($dependencies, $fromId) use (&$cycles) { array_walk($dependencies, static function ($dependency, $toId) use (&$cycles, $fromId) { if ($dependency) { $cycles[] = $fromId . '->' . $toId; } }); }); if (!empty($cycles)) { throw new \UnexpectedValueException('Your dependencies have cycles. That will not work out. Cycles found: ' . implode(', ', $cycles), 1381960494); } // We now built a list of dependencies // Reverse the list to get the correct sorting order return array_reverse($sortedIds); } /** * Get the number of incoming edges in the dependency graph for given identifier */ protected function getIncomingEdgeCount(array $dependencyGraph, string $identifier): int { $incomingEdgeCount = 0; foreach ($dependencyGraph as $dependencies) { if ($dependencies[$identifier] ?? []) { $incomingEdgeCount++; } } return $incomingEdgeCount; } /** * Find all root nodes of a graph * * Root nodes are those, where nothing else depends on (they can be the last in the loading order). * If there are no dependencies at all, all nodes are root nodes. * * @param bool[][] $dependencyGraph * @return array List of identifiers which are root nodes */ public function findRootIds(array $dependencyGraph): array { // Filter nodes with no incoming edge (aka root nodes) $rootIds = []; foreach ($dependencyGraph as $id => $_) { if (!$this->getIncomingEdgeCount($dependencyGraph, (string)$id)) { $rootIds[] = $id; } } return $rootIds; } /** * Find any path in the graph from given start node to destination node * * @param array $graph Directed graph * @param string $from Start node * @param string $to Destination node * @return array Nodes of the found path; empty if no path is found */ protected function findPathInGraph(array $graph, string $from, string $to): array { foreach (array_filter($graph[$from] ?? []) as $node => $_) { if ($node === $to) { return [$from, $to]; } $subPath = $this->findPathInGraph($graph, $node, $to); if (!empty($subPath)) { array_unshift($subPath, $from); return $subPath; } } return []; } /** * Prepare dependencies * * Ensure that all discovered identifiers are added to the dependency list * so we can reliably use the identifiers to build the matrix. * Additionally, fix all invalid or missing before/after arrays * * @param array $dependencies * @param string $beforeKey The key to use in a dependency which specifies the "before"-relation. eg. 'sortBefore', 'loadBefore' * @param string $afterKey The key to use in a dependency which specifies the "after"-relation. eg. 'sortAfter', 'loadAfter' * @return array Prepared dependencies */ protected function prepareDependencies(array $dependencies, string $beforeKey = 'before', string $afterKey = 'after'): array { $preparedDependencies = []; foreach ($dependencies as $id => $dependency) { foreach ([$beforeKey, $afterKey] as $relation) { if (!isset($dependency[$relation]) || !is_array($dependency[$relation])) { $dependency[$relation] = []; } // add all missing, but referenced identifiers to the $dependency list foreach ($dependency[$relation] as $dependingId) { if (!isset($dependencies[$dependingId]) && !isset($preparedDependencies[$dependingId])) { $preparedDependencies[$dependingId] = [ $beforeKey => [], $afterKey => [], ]; } } } $preparedDependencies[$id] = $dependency; } return $preparedDependencies; } }