TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Service;
|
||||
|
||||
/**
|
||||
* This class provides functionality to build
|
||||
* an ordered list from a set of dependencies.
|
||||
*
|
||||
* We use an adjacency matrix for the dependency graph (DAG)
|
||||
*
|
||||
* Example structure of the DAG is:
|
||||
* A => (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<array-key, array<array-key, bool>> 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<array-key, array<array-key, bool>> $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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user