1
0
mirror of https://github.com/flarum/core.git synced 2025-10-14 08:24:28 +02:00

Optional Dependencies (#2579)

* Add and calculate optional dependencies
* Add extension dependency resolver (Kahn's algorithm), plus unit tests
* Resolve extension dependency on enable/disable
This commit is contained in:
Alexander Skvortsov
2021-02-21 13:49:33 -05:00
committed by GitHub
parent 40ede179cd
commit fa10d794a4
3 changed files with 283 additions and 10 deletions

View File

@@ -86,7 +86,9 @@ class ExtensionManager
// We calculate and store a set of composer package names for all installed Flarum extensions,
// so we know what is and isn't a flarum extension in `calculateDependencies`.
// Using keys of an associative array allows us to do these checks in constant time.
// We do the same for enabled extensions, for optional dependencies.
$installedSet = [];
$enabledIds = array_flip($this->getEnabled());
foreach ($installed as $package) {
if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) {
@@ -110,7 +112,7 @@ class ExtensionManager
}
foreach ($extensions as $extension) {
$extension->calculateDependencies($installedSet);
$extension->calculateDependencies($installedSet, $enabledIds);
}
$this->extensions = $extensions->sortBy(function ($extension, $name) {
@@ -348,13 +350,21 @@ class ExtensionManager
/**
* Persist the currently enabled extensions.
*
* @param array $enabled
* @param array $enabledIds
*/
protected function setEnabled(array $enabled)
protected function setEnabled(array $enabledIds)
{
$enabled = array_values(array_unique($enabled));
$enabled = array_map(function ($id) {
return $this->getExtension($id);
}, array_unique($enabledIds));
$this->config->set('extensions_enabled', json_encode($enabled));
$sortedEnabled = static::resolveExtensionOrder($enabled)['valid'];
$sortedEnabledIds = array_map(function (Extension $extension) {
return $extension->getId();
}, $sortedEnabled);
$this->config->set('extensions_enabled', json_encode($sortedEnabledIds));
}
/**
@@ -382,4 +392,92 @@ class ExtensionManager
return $extension->getTitle();
}, $exts);
}
/**
* Sort a list of extensions so that they are properly resolved in respect to order.
* Effectively just topological sorting.
*
* @param Extension[] $extensionList: an array of \Flarum\Extension\Extension objects
*
* @return array with 2 keys: 'valid' points to an ordered array of \Flarum\Extension\Extension
* 'missingDependencies' points to an associative array of extensions that could not be resolved due
* to missing dependencies, in the format extension id => array of missing dependency IDs.
* 'circularDependencies' points to an array of extensions ids of extensions
* that cannot be processed due to circular dependencies
*/
public static function resolveExtensionOrder($extensionList)
{
$extensionIdMapping = []; // Used for caching so we don't rerun ->getExtensions every time.
// This is an implementation of Kahn's Algorithm (https://dl.acm.org/doi/10.1145/368996.369025)
$extensionGraph = [];
$output = [];
$missingDependencies = []; // Extensions are invalid if they are missing dependencies, or have circular dependencies.
$circularDependencies = [];
$pendingQueue = [];
$inDegreeCount = []; // How many extensions are dependent on a given extension?
foreach ($extensionList as $extension) {
$extensionIdMapping[$extension->getId()] = $extension;
}
foreach ($extensionList as $extension) {
$optionalDependencies = array_filter($extension->getOptionalDependencyIds(), function ($id) use ($extensionIdMapping) {
return array_key_exists($id, $extensionIdMapping);
});
$extensionGraph[$extension->getId()] = array_merge($extension->getExtensionDependencyIds(), $optionalDependencies);
foreach ($extensionGraph[$extension->getId()] as $dependency) {
$inDegreeCount[$dependency] = array_key_exists($dependency, $inDegreeCount) ? $inDegreeCount[$dependency] + 1 : 1;
}
}
foreach ($extensionList as $extension) {
if (! array_key_exists($extension->getId(), $inDegreeCount)) {
$inDegreeCount[$extension->getId()] = 0;
$pendingQueue[] = $extension->getId();
}
}
while (! empty($pendingQueue)) {
$activeNode = array_shift($pendingQueue);
$output[] = $activeNode;
foreach ($extensionGraph[$activeNode] as $dependency) {
$inDegreeCount[$dependency] -= 1;
if ($inDegreeCount[$dependency] === 0) {
if (! array_key_exists($dependency, $extensionGraph)) {
// Missing Dependency
$missingDependencies[$activeNode] = array_merge(
Arr::get($missingDependencies, $activeNode, []),
[$dependency]
);
} else {
$pendingQueue[] = $dependency;
}
}
}
}
$validOutput = array_filter($output, function ($extension) use ($missingDependencies) {
return ! array_key_exists($extension, $missingDependencies);
});
$validExtensions = array_reverse(array_map(function ($extensionId) use ($extensionIdMapping) {
return $extensionIdMapping[$extensionId];
}, $validOutput)); // Reversed as required by Kahn's algorithm.
foreach ($inDegreeCount as $id => $count) {
if ($count != 0) {
$circularDependencies[] = $id;
}
}
return [
'valid' => $validExtensions,
'missingDependencies' => $missingDependencies,
'circularDependencies' => $circularDependencies
];
}
}