From c8cfdadc8850f0415e7ba868b494ff7fd2d23830 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Thu, 28 May 2020 15:53:00 -0400 Subject: [PATCH 01/32] Add relevant exceptions for enabling/disabling --- .../DependentExtensionsException.php | 35 +++++++++++++++++++ .../MissingDependenciesException.php | 35 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/Extension/Exception/DependentExtensionsException.php create mode 100644 src/Extension/Exception/MissingDependenciesException.php diff --git a/src/Extension/Exception/DependentExtensionsException.php b/src/Extension/Exception/DependentExtensionsException.php new file mode 100644 index 0000000000..a906add1c3 --- /dev/null +++ b/src/Extension/Exception/DependentExtensionsException.php @@ -0,0 +1,35 @@ +extension = $extension; + $this->$dependent_extensions = $dependent_extensions; + } + + public function getType(): string + { + return 'dependent_extensions'; + } +} diff --git a/src/Extension/Exception/MissingDependenciesException.php b/src/Extension/Exception/MissingDependenciesException.php new file mode 100644 index 0000000000..f3216df8a1 --- /dev/null +++ b/src/Extension/Exception/MissingDependenciesException.php @@ -0,0 +1,35 @@ +extension = $extension; + $this->$flarum_dependencies = $flarum_dependencies; + } + + public function getType(): string + { + return 'missing_dependencies'; + } +} From e50f22ba1750da0da86fa6aa5a0ccc26a245e450 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Thu, 28 May 2020 15:53:29 -0400 Subject: [PATCH 02/32] Add helper method for getting flarum extension dependencies --- src/Extension/Extension.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index 3707bfae08..648aef78df 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -221,6 +221,13 @@ public function getIcon() return $icon; } + /** + * Get the list of flarum extensions that this extension depends on + */ + public function getFlarumExtensionDependencies() { + return $this->composerJsonAttribute('extra.flarum-extension.dependencies.extensions'); + } + public function enable(Container $container) { foreach ($this->getLifecycleExtenders() as $extender) { @@ -363,12 +370,13 @@ public function migrate(Migrator $migrator, $direction = 'up') public function toArray() { return (array) array_merge([ - 'id' => $this->getId(), - 'version' => $this->getVersion(), - 'path' => $this->path, - 'icon' => $this->getIcon(), - 'hasAssets' => $this->hasAssets(), - 'hasMigrations' => $this->hasMigrations(), + 'id' => $this->getId(), + 'version' => $this->getVersion(), + 'path' => $this->path, + 'icon' => $this->getIcon(), + 'hasAssets' => $this->hasAssets(), + 'hasMigrations' => $this->hasMigrations(), + 'extensionDependencies' => $this->getFlarumExtensionDependencies(), ], $this->composerJson); } } From 7937f82f3146ae442a010d30d534133f44c38cfa Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Thu, 28 May 2020 15:54:22 -0400 Subject: [PATCH 03/32] Add extension dependency resolver (Kahn's algorithm), plus unit tests --- src/Extension/ExtensionManager.php | 112 ++++++++++++++++++ .../ExtensionDependencyResolutionTest.php | 107 +++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 tests/unit/Foundation/ExtensionDependencyResolutionTest.php diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 40136e3484..db0c945d0a 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -15,6 +15,7 @@ use Flarum\Extension\Event\Enabled; use Flarum\Extension\Event\Enabling; use Flarum\Extension\Event\Uninstalled; +use Flarum\Extension\Exception\DependentExtensionsException; use Flarum\Foundation\Paths; use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Container\Container; @@ -133,6 +134,18 @@ public function enable($name) $extension = $this->getExtension($name); + $missingDependencies = []; + $enabled = $this->getEnabled(); + foreach ($extension->getFlarumExtensionDependencies() as $dependency) { + if (!in_array($dependency, $enabled)) { + $missingDependencies[] = $dependency; + } + } + + if (!empty($missingDependencies)) { + throw new Exception\MissingDependenciesException($extension, $missingDependencies); + } + $this->dispatcher->dispatch(new Enabling($extension)); $enabled = $this->getEnabled(); @@ -165,6 +178,22 @@ public function disable($name) $extension = $this->getExtension($name); + $dependentExtensions = []; + + foreach ($this->getEnabledExtensions() as $possibleDependent) { + foreach ($possibleDependent->getFlarumExtensionDependencies() as $dependency) { + if ($dependency === $extension->getId()) { + $dependentExtensions[] = $dependency; + // No need to cycle through the rest of this extension's dependencies. + break; + } + } + } + + if (!empty($dependentExtensions)) { + throw new DependentExtensionsException($extension, $dependentExtensions); + } + $this->dispatcher->dispatch(new Disabling($extension)); unset($enabled[$k]); @@ -335,4 +364,87 @@ public function isEnabled($extension) return isset($enabled[$extension]); } + + /** + * Sort a list of extensions so that they are properly resolved in respect to order. + * Effectively just topological sorting. + * + * @param $extensionList: an array of \Flarum\Extension\Extension objects + * + * @return array with 2 keys: 'extensions' 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; + $extensionGraph[$extension->getId()] = $extension->getFlarumExtensionDependencies(); + + foreach ($extension->getFlarumExtensionDependencies() 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 + ]; + } } diff --git a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php new file mode 100644 index 0000000000..6cbae7fe39 --- /dev/null +++ b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php @@ -0,0 +1,107 @@ +tags = new FakeExtension('flarum-tags', []); + $this->categories = new FakeExtension('flarum-categories', ['flarum-tags', 'flarum-tag-backgrounds']); + $this->tagBackgrounds = new FakeExtension('flarum-tag-backgrounds', ['flarum-tags']); + $this->something = new FakeExtension('flarum-something', ['flarum-categories', 'flarum-help']); + $this->help = new FakeExtension('flarum-help', []); + $this->missing = new FakeExtension('flarum-missing', ['this-does-not-exist', 'flarum-tags', 'also-not-exists']); + $this->circular1 = new FakeExtension('circular1', ['circular2']); + $this->circular2 = new FakeExtension('circular2', ['circular1']); + } + + + /** @test */ + public function works_with_empty_set() + { + $expected = [ + 'valid' => [], + 'missingDependencies' => [], + 'circularDependencies' => [], + ]; + + $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder([])); + } + + /** @test */ + public function works_with_proper_data() + { + $exts = [$this->tags, $this->categories, $this->tagBackgrounds, $this->something, $this->help]; + + $expected = [ + 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], + 'missingDependencies' => [], + 'circularDependencies' => [], + ]; + + $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); + } + + /** @test */ + public function works_with_missing_dependencies() + { + $exts = [$this->tags, $this->categories, $this->tagBackgrounds, $this->something, $this->help, $this->missing]; + + $expected = [ + 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], + 'missingDependencies' => ['flarum-missing' => ['this-does-not-exist', 'also-not-exists']], + 'circularDependencies' => [], + ]; + + $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); + } + + /** @test */ + public function works_with_circular_dependencies() + { + $exts = [$this->tags, $this->categories, $this->tagBackgrounds, $this->something, $this->help, $this->circular1, $this->circular2]; + + $expected = [ + 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], + 'missingDependencies' => [], + 'circularDependencies' => ['circular2', 'circular1'], + ]; + + $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); + } +} + +class FakeExtension +{ + protected $id; + protected $dependencies; + + public function __construct($id, $dependencies) + { + $this->id = $id; + $this->dependencies = $dependencies; + } + + public function getId() + { + return $this->id; + } + + public function getFlarumExtensionDependencies() + { + return $this->dependencies; + } +} From acc4f65ba8a9886647253189eddfb2a9d5bbcaa2 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Thu, 28 May 2020 16:01:36 -0400 Subject: [PATCH 04/32] Enable extensions in proper order --- src/Extension/ExtensionManager.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index db0c945d0a..22df21866c 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -325,7 +325,9 @@ public function getEnabledExtensions() */ public function extend(Container $app) { - foreach ($this->getEnabledExtensions() as $extension) { + $enabled = static::resolveExtensionOrder($this->getEnabledExtensions()); + + foreach ($enabled['valid'] as $extension) { $extension->extend($app); } } From ec3cd0cf3a01ae83b6bbfc62c7877d35c329a642 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Thu, 28 May 2020 16:29:39 -0400 Subject: [PATCH 05/32] Fix no default when getting extension dependencies --- src/Extension/Extension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index 648aef78df..ce84a187b8 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -225,7 +225,7 @@ public function getIcon() * Get the list of flarum extensions that this extension depends on */ public function getFlarumExtensionDependencies() { - return $this->composerJsonAttribute('extra.flarum-extension.dependencies.extensions'); + return Arr::get($this->composerJson, 'extra.flarum-extension.dependencies.extensions', []); } public function enable(Container $container) From 0722382978c1624dca71564d42920da04b005b87 Mon Sep 17 00:00:00 2001 From: luceos Date: Thu, 28 May 2020 20:30:15 +0000 Subject: [PATCH 06/32] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Extension/Extension.php | 5 +++-- src/Extension/ExtensionManager.php | 15 +++++++-------- .../ExtensionDependencyResolutionTest.php | 1 - 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index ce84a187b8..881d6a2472 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -222,9 +222,10 @@ public function getIcon() } /** - * Get the list of flarum extensions that this extension depends on + * Get the list of flarum extensions that this extension depends on. */ - public function getFlarumExtensionDependencies() { + public function getFlarumExtensionDependencies() + { return Arr::get($this->composerJson, 'extra.flarum-extension.dependencies.extensions', []); } diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 22df21866c..e6fdd96639 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -137,12 +137,12 @@ public function enable($name) $missingDependencies = []; $enabled = $this->getEnabled(); foreach ($extension->getFlarumExtensionDependencies() as $dependency) { - if (!in_array($dependency, $enabled)) { + if (! in_array($dependency, $enabled)) { $missingDependencies[] = $dependency; } } - if (!empty($missingDependencies)) { + if (! empty($missingDependencies)) { throw new Exception\MissingDependenciesException($extension, $missingDependencies); } @@ -190,7 +190,7 @@ public function disable($name) } } - if (!empty($dependentExtensions)) { + if (! empty($dependentExtensions)) { throw new DependentExtensionsException($extension, $dependentExtensions); } @@ -391,7 +391,6 @@ public static function resolveExtensionOrder($extensionList) $pendingQueue = []; $inDegreeCount = []; // How many extensions are dependent on a given extension? - foreach ($extensionList as $extension) { $extensionIdMapping[$extension->getId()] = $extension; $extensionGraph[$extension->getId()] = $extension->getFlarumExtensionDependencies(); @@ -402,7 +401,7 @@ public static function resolveExtensionOrder($extensionList) } foreach ($extensionList as $extension) { - if (!array_key_exists($extension->getId(), $inDegreeCount)) { + if (! array_key_exists($extension->getId(), $inDegreeCount)) { $inDegreeCount[$extension->getId()] = 0; $pendingQueue[] = $extension->getId(); } @@ -416,7 +415,7 @@ public static function resolveExtensionOrder($extensionList) $inDegreeCount[$dependency] -= 1; if ($inDegreeCount[$dependency] === 0) { - if (!array_key_exists($dependency, $extensionGraph)) { + if (! array_key_exists($dependency, $extensionGraph)) { // Missing Dependency $missingDependencies[$activeNode] = array_merge( Arr::get($missingDependencies, $activeNode, []), @@ -430,10 +429,10 @@ public static function resolveExtensionOrder($extensionList) } $validOutput = array_filter($output, function ($extension) use ($missingDependencies) { - return !array_key_exists($extension, $missingDependencies); + return ! array_key_exists($extension, $missingDependencies); }); - $validExtensions = array_reverse(array_map(function($extensionId) use ($extensionIdMapping) { + $validExtensions = array_reverse(array_map(function ($extensionId) use ($extensionIdMapping) { return $extensionIdMapping[$extensionId]; }, $validOutput)); // Reversed as required by Kahn's algorithm. diff --git a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php index 6cbae7fe39..e0ec68d991 100644 --- a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php +++ b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php @@ -28,7 +28,6 @@ public function setUp() $this->circular2 = new FakeExtension('circular2', ['circular1']); } - /** @test */ public function works_with_empty_set() { From 6c686bb7eb0b0fc393dd513c99361a7796e98858 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 24 Jul 2020 15:09:09 -0400 Subject: [PATCH 07/32] Use require instead of dedicated field to get extension dependencies --- src/Extension/Extension.php | 67 ++++++++++++++++++++---------- src/Extension/ExtensionManager.php | 22 ++++++++-- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index 881d6a2472..f3cc440bf6 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -15,6 +15,7 @@ use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem; @@ -51,6 +52,13 @@ class Extension implements Arrayable 'jpg' => 'image/jpeg', ]; + protected static function nameToId($name) + { + list($vendor, $package) = explode('/', $name); + $package = str_replace(['flarum-ext-', 'flarum-'], '', $package); + return "$vendor-$package"; + } + /** * Unique Id of the extension. * @@ -74,6 +82,13 @@ class Extension implements Arrayable */ protected $composerJson; + /** + * An array of extension ids for extension dependencies + * + * @var string[] + */ + protected $dependencies; + /** * Whether the extension is installed. * @@ -104,9 +119,7 @@ public function __construct($path, $composerJson) */ protected function assignId() { - list($vendor, $package) = explode('/', $this->name); - $package = str_replace(['flarum-ext-', 'flarum-'], '', $package); - $this->id = "$vendor-$package"; + $this->id = static::nameToId($this->name); } public function extend(Container $app) @@ -182,6 +195,22 @@ public function setVersion($version) return $this; } + /** + * Get the list of flarum extensions that this extension depends on + * + * @param array $lockJson: The json manifest of all installed php extensions + */ + public function calculateDependencies($lockJson) + { + $this->extensionDependencies = (new Collection(Arr::get($this->composerJson, 'require', []))) + ->keys() + ->filter(function ($key) use ($lockJson) { + return array_key_exists($key, $lockJson); + })->map(function ($key) { + return static::nameToId($key); + })->toArray(); + } + /** * @return string */ @@ -204,11 +233,11 @@ public function getIcon() return $icon; } - $file = $this->path.'/'.$file; + $file = $this->path . '/' . $file; if (file_exists($file)) { $extension = pathinfo($file, PATHINFO_EXTENSION); - if (! array_key_exists($extension, self::LOGO_MIMETYPES)) { + if (!array_key_exists($extension, self::LOGO_MIMETYPES)) { throw new \RuntimeException('Invalid image type'); } @@ -221,14 +250,6 @@ public function getIcon() return $icon; } - /** - * Get the list of flarum extensions that this extension depends on. - */ - public function getFlarumExtensionDependencies() - { - return Arr::get($this->composerJson, 'extra.flarum-extension.dependencies.extensions', []); - } - public function enable(Container $container) { foreach ($this->getLifecycleExtenders() as $extender) { @@ -265,13 +286,13 @@ private function getExtenders(): array { $extenderFile = $this->getExtenderFile(); - if (! $extenderFile) { + if (!$extenderFile) { return []; } $extenders = require $extenderFile; - if (! is_array($extenders)) { + if (!is_array($extenders)) { $extenders = [$extenders]; } @@ -318,17 +339,17 @@ private function getExtenderFile(): ?string */ public function hasAssets() { - return realpath($this->path.'/assets/') !== false; + return realpath($this->path . '/assets/') !== false; } public function copyAssetsTo(FilesystemInterface $target) { - if (! $this->hasAssets()) { + if (!$this->hasAssets()) { return; } $mount = new MountManager([ - 'source' => $source = new Filesystem(new Local($this->getPath().'/assets')), + 'source' => $source = new Filesystem(new Local($this->getPath() . '/assets')), 'target' => $target, ]); @@ -347,19 +368,19 @@ public function copyAssetsTo(FilesystemInterface $target) */ public function hasMigrations() { - return realpath($this->path.'/migrations/') !== false; + return realpath($this->path . '/migrations/') !== false; } public function migrate(Migrator $migrator, $direction = 'up') { - if (! $this->hasMigrations()) { + if (!$this->hasMigrations()) { return; } if ($direction == 'up') { - return $migrator->run($this->getPath().'/migrations', $this); + return $migrator->run($this->getPath() . '/migrations', $this); } else { - return $migrator->reset($this->getPath().'/migrations', $this); + return $migrator->reset($this->getPath() . '/migrations', $this); } } @@ -377,7 +398,7 @@ public function toArray() 'icon' => $this->getIcon(), 'hasAssets' => $this->hasAssets(), 'hasMigrations' => $this->hasMigrations(), - 'extensionDependencies' => $this->getFlarumExtensionDependencies(), + 'extensionDependencies' => $this->extensionDependencies, ], $this->composerJson); } } diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index e6fdd96639..8baa7b9c79 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -84,11 +84,20 @@ public function getExtensions() // Composer 2.0 changes the structure of the installed.json manifest $installed = $installed['packages'] ?? $installed; + // We calculate and store an associative copy of the json manifest + // to avoid an O(n * m) complexity when finding extension dependencies. + // Additionally, by only including flarum extensions, we know that + // anything present in this associative copy is a flarum extension, removing + // the need to check for it's type. + $associativeInstalled = []; + foreach ($installed as $package) { if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) { continue; } + $associativeInstalled[Arr::get($package, 'name')] = $package; + $path = isset($package['install-path']) ? $this->paths->vendor.'/composer/'.$package['install-path'] : $this->paths->vendor.'/'.Arr::get($package, 'name'); @@ -102,6 +111,11 @@ public function getExtensions() $extensions->put($extension->getId(), $extension); } + + foreach ($extensions as $extension) { + $extension->calculateDependencies($associativeInstalled); + } + $this->extensions = $extensions->sortBy(function ($extension, $name) { return $extension->composerJsonAttribute('extra.flarum-extension.title'); }); @@ -136,7 +150,7 @@ public function enable($name) $missingDependencies = []; $enabled = $this->getEnabled(); - foreach ($extension->getFlarumExtensionDependencies() as $dependency) { + foreach ($extension->extensionDependencies as $dependency) { if (! in_array($dependency, $enabled)) { $missingDependencies[] = $dependency; } @@ -181,7 +195,7 @@ public function disable($name) $dependentExtensions = []; foreach ($this->getEnabledExtensions() as $possibleDependent) { - foreach ($possibleDependent->getFlarumExtensionDependencies() as $dependency) { + foreach ($possibleDependent->extensionDependencies as $dependency) { if ($dependency === $extension->getId()) { $dependentExtensions[] = $dependency; // No need to cycle through the rest of this extension's dependencies. @@ -393,9 +407,9 @@ public static function resolveExtensionOrder($extensionList) foreach ($extensionList as $extension) { $extensionIdMapping[$extension->getId()] = $extension; - $extensionGraph[$extension->getId()] = $extension->getFlarumExtensionDependencies(); + $extensionGraph[$extension->getId()] = $extension->extensionDependencies; - foreach ($extension->getFlarumExtensionDependencies() as $dependency) { + foreach ($extension->extensionDependencies as $dependency) { $inDegreeCount[$dependency] = array_key_exists($dependency, $inDegreeCount) ? $inDegreeCount[$dependency] + 1 : 1; } } From 787515d515124a482996ef2a06e07816881ded49 Mon Sep 17 00:00:00 2001 From: luceos Date: Fri, 24 Jul 2020 19:10:02 +0000 Subject: [PATCH 08/32] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Extension/Extension.php | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index f3cc440bf6..57bac972f0 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -56,6 +56,7 @@ protected static function nameToId($name) { list($vendor, $package) = explode('/', $name); $package = str_replace(['flarum-ext-', 'flarum-'], '', $package); + return "$vendor-$package"; } @@ -83,7 +84,7 @@ protected static function nameToId($name) protected $composerJson; /** - * An array of extension ids for extension dependencies + * An array of extension ids for extension dependencies. * * @var string[] */ @@ -196,7 +197,7 @@ public function setVersion($version) } /** - * Get the list of flarum extensions that this extension depends on + * Get the list of flarum extensions that this extension depends on. * * @param array $lockJson: The json manifest of all installed php extensions */ @@ -233,11 +234,11 @@ public function getIcon() return $icon; } - $file = $this->path . '/' . $file; + $file = $this->path.'/'.$file; if (file_exists($file)) { $extension = pathinfo($file, PATHINFO_EXTENSION); - if (!array_key_exists($extension, self::LOGO_MIMETYPES)) { + if (! array_key_exists($extension, self::LOGO_MIMETYPES)) { throw new \RuntimeException('Invalid image type'); } @@ -286,13 +287,13 @@ private function getExtenders(): array { $extenderFile = $this->getExtenderFile(); - if (!$extenderFile) { + if (! $extenderFile) { return []; } $extenders = require $extenderFile; - if (!is_array($extenders)) { + if (! is_array($extenders)) { $extenders = [$extenders]; } @@ -339,17 +340,17 @@ private function getExtenderFile(): ?string */ public function hasAssets() { - return realpath($this->path . '/assets/') !== false; + return realpath($this->path.'/assets/') !== false; } public function copyAssetsTo(FilesystemInterface $target) { - if (!$this->hasAssets()) { + if (! $this->hasAssets()) { return; } $mount = new MountManager([ - 'source' => $source = new Filesystem(new Local($this->getPath() . '/assets')), + 'source' => $source = new Filesystem(new Local($this->getPath().'/assets')), 'target' => $target, ]); @@ -368,19 +369,19 @@ public function copyAssetsTo(FilesystemInterface $target) */ public function hasMigrations() { - return realpath($this->path . '/migrations/') !== false; + return realpath($this->path.'/migrations/') !== false; } public function migrate(Migrator $migrator, $direction = 'up') { - if (!$this->hasMigrations()) { + if (! $this->hasMigrations()) { return; } if ($direction == 'up') { - return $migrator->run($this->getPath() . '/migrations', $this); + return $migrator->run($this->getPath().'/migrations', $this); } else { - return $migrator->reset($this->getPath() . '/migrations', $this); + return $migrator->reset($this->getPath().'/migrations', $this); } } From c374bafbfc513bc64f7533b693e4292483acd046 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 24 Jul 2020 15:22:15 -0400 Subject: [PATCH 09/32] Fix fake extension to not fail tests and be compliant --- .../Foundation/ExtensionDependencyResolutionTest.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php index e0ec68d991..ef1d84a2b6 100644 --- a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php +++ b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php @@ -86,21 +86,16 @@ public function works_with_circular_dependencies() class FakeExtension { protected $id; - protected $dependencies; + public $extensionDependencies; - public function __construct($id, $dependencies) + public function __construct($id, $extensionDependencies) { $this->id = $id; - $this->dependencies = $dependencies; + $this->extensionDependencies = $extensionDependencies; } public function getId() { return $this->id; } - - public function getFlarumExtensionDependencies() - { - return $this->dependencies; - } } From 30ee917db673f477619208fb486e4671ea94fe39 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sun, 6 Sep 2020 17:26:15 -0400 Subject: [PATCH 10/32] Remove unnecessary import --- src/Extension/ExtensionManager.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 8baa7b9c79..9416bff583 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -15,7 +15,6 @@ use Flarum\Extension\Event\Enabled; use Flarum\Extension\Event\Enabling; use Flarum\Extension\Event\Uninstalled; -use Flarum\Extension\Exception\DependentExtensionsException; use Flarum\Foundation\Paths; use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Container\Container; @@ -205,7 +204,7 @@ public function disable($name) } if (! empty($dependentExtensions)) { - throw new DependentExtensionsException($extension, $dependentExtensions); + throw new Exception\DependentExtensionsException($extension, $dependentExtensions); } $this->dispatcher->dispatch(new Disabling($extension)); From fec6ff357bb4df2c85c9779b1b79ff2fa252b820 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sun, 6 Sep 2020 17:26:38 -0400 Subject: [PATCH 11/32] Fix constructor, remove KnownError since we'll need a custom handler --- .../Exception/DependentExtensionsException.php | 10 +++------- .../Exception/MissingDependenciesException.php | 16 ++++++---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Extension/Exception/DependentExtensionsException.php b/src/Extension/Exception/DependentExtensionsException.php index a906add1c3..7ac8add3ce 100644 --- a/src/Extension/Exception/DependentExtensionsException.php +++ b/src/Extension/Exception/DependentExtensionsException.php @@ -11,9 +11,8 @@ use Exception; use Flarum\Extension\Extension; -use Flarum\Foundation\KnownError; -class DependentExtensionsException extends Exception implements KnownError +class DependentExtensionsException extends Exception { public $extension; public $dependent_extensions; @@ -25,11 +24,8 @@ class DependentExtensionsException extends Exception implements KnownError public function __construct(Extension $extension, array $dependent_extensions = []) { $this->extension = $extension; - $this->$dependent_extensions = $dependent_extensions; - } + $this->dependent_extensions = $dependent_extensions; - public function getType(): string - { - return 'dependent_extensions'; + parent::__construct(implode("\n", $dependent_extensions)); } } diff --git a/src/Extension/Exception/MissingDependenciesException.php b/src/Extension/Exception/MissingDependenciesException.php index f3216df8a1..836775ad1a 100644 --- a/src/Extension/Exception/MissingDependenciesException.php +++ b/src/Extension/Exception/MissingDependenciesException.php @@ -11,25 +11,21 @@ use Exception; use Flarum\Extension\Extension; -use Flarum\Foundation\KnownError; -class MissingDependenciesException extends Exception implements KnownError +class MissingDependenciesException extends Exception { public $extension; - public $flarum_dependencies; + public $missing_dependencies; /** * @param $extension: The extension we are attempting to activate. - * @param $flarum_dependencies: Extension IDs of the missing flarum extension dependencies for this extension + * @param $missing_dependencies: Extension IDs of the missing flarum extension dependencies for this extension */ - public function __construct(Extension $extension, array $flarum_dependencies = []) + public function __construct(Extension $extension, array $missing_dependencies = null) { $this->extension = $extension; - $this->$flarum_dependencies = $flarum_dependencies; - } + $this->missing_dependencies = $missing_dependencies; - public function getType(): string - { - return 'missing_dependencies'; + parent::__construct(implode("\n", $missing_dependencies)); } } From 54a35b4173b726bb12f8a790e281dc789a410362 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sun, 6 Sep 2020 17:26:58 -0400 Subject: [PATCH 12/32] Add custom handlers for dependency exceptions --- .../DependentExtensionsExceptionHandler.php | 34 +++++++++++++++++++ .../MissingDependenciesExceptionHandler.php | 34 +++++++++++++++++++ src/Foundation/ErrorServiceProvider.php | 6 ++++ 3 files changed, 74 insertions(+) create mode 100644 src/Extension/Exception/DependentExtensionsExceptionHandler.php create mode 100644 src/Extension/Exception/MissingDependenciesExceptionHandler.php diff --git a/src/Extension/Exception/DependentExtensionsExceptionHandler.php b/src/Extension/Exception/DependentExtensionsExceptionHandler.php new file mode 100644 index 0000000000..4dd3e0e583 --- /dev/null +++ b/src/Extension/Exception/DependentExtensionsExceptionHandler.php @@ -0,0 +1,34 @@ +withDetails($this->errorDetails($e)); + } + + protected function errorDetails(DependentExtensionsException $e): array + { + return [ + [ + 'extension' => $e->extension->getId(), + 'dependent_dependencies' => $e->dependent_dependencies + ] + ]; + } +} diff --git a/src/Extension/Exception/MissingDependenciesExceptionHandler.php b/src/Extension/Exception/MissingDependenciesExceptionHandler.php new file mode 100644 index 0000000000..e0c28a78e3 --- /dev/null +++ b/src/Extension/Exception/MissingDependenciesExceptionHandler.php @@ -0,0 +1,34 @@ +withDetails($this->errorDetails($e)); + } + + protected function errorDetails(MissingDependenciesException $e): array + { + return [ + [ + 'extension' => $e->extension->getId(), + 'missing_dependencies' => $e->missing_dependencies + ] + ]; + } +} diff --git a/src/Foundation/ErrorServiceProvider.php b/src/Foundation/ErrorServiceProvider.php index 58c827f7fe..02483841de 100644 --- a/src/Foundation/ErrorServiceProvider.php +++ b/src/Foundation/ErrorServiceProvider.php @@ -9,6 +9,10 @@ namespace Flarum\Foundation; +use Flarum\Extension\Exception\DependentExtensionsException; +use Flarum\Extension\Exception\DependentExtensionsExceptionHandler; +use Flarum\Extension\Exception\MissingDependenciesException; +use Flarum\Extension\Exception\MissingDependenciesExceptionHandler; use Flarum\Foundation\ErrorHandling\ExceptionHandler; use Flarum\Foundation\ErrorHandling\LogReporter; use Flarum\Foundation\ErrorHandling\Registry; @@ -57,6 +61,8 @@ public function register() return [ IlluminateValidationException::class => ExceptionHandler\IlluminateValidationExceptionHandler::class, ValidationException::class => ExceptionHandler\ValidationExceptionHandler::class, + DependentExtensionsException::class => DependentExtensionsExceptionHandler::class, + MissingDependenciesException::class => MissingDependenciesExceptionHandler::class, ]; }); From 2e4ebefd2a089e1e2140f8615a73412c394ac743 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sun, 6 Sep 2020 17:49:16 -0400 Subject: [PATCH 13/32] Standardize return format --- src/Extension/Exception/DependentExtensionsExceptionHandler.php | 2 +- src/Extension/Exception/MissingDependenciesExceptionHandler.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Extension/Exception/DependentExtensionsExceptionHandler.php b/src/Extension/Exception/DependentExtensionsExceptionHandler.php index 4dd3e0e583..55241ccc71 100644 --- a/src/Extension/Exception/DependentExtensionsExceptionHandler.php +++ b/src/Extension/Exception/DependentExtensionsExceptionHandler.php @@ -27,7 +27,7 @@ protected function errorDetails(DependentExtensionsException $e): array return [ [ 'extension' => $e->extension->getId(), - 'dependent_dependencies' => $e->dependent_dependencies + 'extensions' => $e->dependent_extensions ] ]; } diff --git a/src/Extension/Exception/MissingDependenciesExceptionHandler.php b/src/Extension/Exception/MissingDependenciesExceptionHandler.php index e0c28a78e3..995e3de683 100644 --- a/src/Extension/Exception/MissingDependenciesExceptionHandler.php +++ b/src/Extension/Exception/MissingDependenciesExceptionHandler.php @@ -27,7 +27,7 @@ protected function errorDetails(MissingDependenciesException $e): array return [ [ 'extension' => $e->extension->getId(), - 'missing_dependencies' => $e->missing_dependencies + 'extensions' => $e->missing_dependencies ] ]; } From d12fe5bd2b07ef5a40f11df0c2aebcaa9c8c66fc Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sun, 6 Sep 2020 17:49:31 -0400 Subject: [PATCH 14/32] Add frontend error handler for admin dashboard --- js/src/admin/components/ExtensionsPage.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/js/src/admin/components/ExtensionsPage.js b/js/src/admin/components/ExtensionsPage.js index 5a50fea6ea..d4523f4378 100644 --- a/js/src/admin/components/ExtensionsPage.js +++ b/js/src/admin/components/ExtensionsPage.js @@ -123,6 +123,7 @@ export default class ExtensionsPage extends Page { url: app.forum.attribute('apiUrl') + '/extensions/' + id, method: 'PATCH', body: { enabled: !enabled }, + errorHandler: this.onerror.bind(this), }) .then(() => { if (!enabled) localStorage.setItem('enabledExtension', id); @@ -131,4 +132,18 @@ export default class ExtensionsPage extends Page { app.modal.show(LoadingModal); } + + onerror(e) { + app.modal.close(); + + const error = JSON.parse(e.responseText).errors[0]; + + app.alerts.show( + app.translator.trans(`core.lib.error.${error.code}_message`, { + extension: error.extension, + extensions: error.extensions.join(', '), + }), + { type: 'error' } + ); + } } From 6e4b8674c4b8c14d5da7003a4c7f28824d380b70 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sun, 6 Sep 2020 17:58:38 -0400 Subject: [PATCH 15/32] Fix dependentExtensions saving the wrong thing --- src/Extension/ExtensionManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 9416bff583..155c598bf9 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -196,7 +196,7 @@ public function disable($name) foreach ($this->getEnabledExtensions() as $possibleDependent) { foreach ($possibleDependent->extensionDependencies as $dependency) { if ($dependency === $extension->getId()) { - $dependentExtensions[] = $dependency; + $dependentExtensions[] = $possibleDependent->getId(); // No need to cycle through the rest of this extension's dependencies. break; } From 7b405f0708a54140341d1cecac40d16c9cc614a5 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com> Date: Sun, 6 Sep 2020 18:37:15 -0400 Subject: [PATCH 16/32] Update src/Extension/Exception/DependentExtensionsException.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniƫl Klabbers --- src/Extension/Exception/DependentExtensionsException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/Exception/DependentExtensionsException.php b/src/Extension/Exception/DependentExtensionsException.php index 7ac8add3ce..471a8000ed 100644 --- a/src/Extension/Exception/DependentExtensionsException.php +++ b/src/Extension/Exception/DependentExtensionsException.php @@ -21,7 +21,7 @@ class DependentExtensionsException extends Exception * @param $extension: The extension we are attempting to activate. * @param $dependent_extensions: Extension IDs of the Flarum extensions that depend on this extension */ - public function __construct(Extension $extension, array $dependent_extensions = []) + public function __construct(Extension $extension, array $dependent_extensions) { $this->extension = $extension; $this->dependent_extensions = $dependent_extensions; From 39ca931632a055ec7d8c0c823251d13d84dca01b Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Thu, 24 Sep 2020 17:54:49 -0400 Subject: [PATCH 17/32] Update for new alerts signature --- js/src/admin/components/ExtensionsPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/admin/components/ExtensionsPage.js b/js/src/admin/components/ExtensionsPage.js index d4523f4378..6f92f385b8 100644 --- a/js/src/admin/components/ExtensionsPage.js +++ b/js/src/admin/components/ExtensionsPage.js @@ -139,11 +139,11 @@ export default class ExtensionsPage extends Page { const error = JSON.parse(e.responseText).errors[0]; app.alerts.show( + { type: 'error' }, app.translator.trans(`core.lib.error.${error.code}_message`, { extension: error.extension, extensions: error.extensions.join(', '), - }), - { type: 'error' } + }) ); } } From 7be3c1f593cf5d8641f1aa232543701aae6a1080 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Thu, 24 Sep 2020 17:55:08 -0400 Subject: [PATCH 18/32] Add more tests for dependency resolution --- .../ExtensionDependencyResolutionTest.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php index ef1d84a2b6..3f89b2162b 100644 --- a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php +++ b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php @@ -54,6 +54,34 @@ public function works_with_proper_data() $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); } + /** @test */ + public function works_with_proper_data_in_different_order() + { + $exts = [$this->help, $this->categories, $this->tagBackgrounds, $this->tags, $this->something]; + + $expected = [ + 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], + 'missingDependencies' => [], + 'circularDependencies' => [], + ]; + + $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); + } + + /** @test */ + public function works_with_proper_data_in_yet_another_order() + { + $exts = [$this->something, $this->tagBackgrounds, $this->help, $this->categories, $this->tags]; + + $expected = [ + 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], + 'missingDependencies' => [], + 'circularDependencies' => [], + ]; + + $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); + } + /** @test */ public function works_with_missing_dependencies() { From d32bafcdc5179b24c7a5f504f066ab989ced7f27 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sat, 26 Sep 2020 12:59:49 -0400 Subject: [PATCH 19/32] Remove dependency resolution portion --- src/Extension/ExtensionManager.php | 86 +----------- .../ExtensionDependencyResolutionTest.php | 129 ------------------ 2 files changed, 1 insertion(+), 214 deletions(-) delete mode 100644 tests/unit/Foundation/ExtensionDependencyResolutionTest.php diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 155c598bf9..6308e3f74b 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -338,9 +338,7 @@ public function getEnabledExtensions() */ public function extend(Container $app) { - $enabled = static::resolveExtensionOrder($this->getEnabledExtensions()); - - foreach ($enabled['valid'] as $extension) { + foreach ($this->getEnabledExtensions() as $extension) { $extension->extend($app); } } @@ -379,86 +377,4 @@ public function isEnabled($extension) return isset($enabled[$extension]); } - - /** - * Sort a list of extensions so that they are properly resolved in respect to order. - * Effectively just topological sorting. - * - * @param $extensionList: an array of \Flarum\Extension\Extension objects - * - * @return array with 2 keys: 'extensions' 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; - $extensionGraph[$extension->getId()] = $extension->extensionDependencies; - - foreach ($extension->extensionDependencies 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 - ]; - } } diff --git a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php b/tests/unit/Foundation/ExtensionDependencyResolutionTest.php deleted file mode 100644 index 3f89b2162b..0000000000 --- a/tests/unit/Foundation/ExtensionDependencyResolutionTest.php +++ /dev/null @@ -1,129 +0,0 @@ -tags = new FakeExtension('flarum-tags', []); - $this->categories = new FakeExtension('flarum-categories', ['flarum-tags', 'flarum-tag-backgrounds']); - $this->tagBackgrounds = new FakeExtension('flarum-tag-backgrounds', ['flarum-tags']); - $this->something = new FakeExtension('flarum-something', ['flarum-categories', 'flarum-help']); - $this->help = new FakeExtension('flarum-help', []); - $this->missing = new FakeExtension('flarum-missing', ['this-does-not-exist', 'flarum-tags', 'also-not-exists']); - $this->circular1 = new FakeExtension('circular1', ['circular2']); - $this->circular2 = new FakeExtension('circular2', ['circular1']); - } - - /** @test */ - public function works_with_empty_set() - { - $expected = [ - 'valid' => [], - 'missingDependencies' => [], - 'circularDependencies' => [], - ]; - - $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder([])); - } - - /** @test */ - public function works_with_proper_data() - { - $exts = [$this->tags, $this->categories, $this->tagBackgrounds, $this->something, $this->help]; - - $expected = [ - 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], - 'missingDependencies' => [], - 'circularDependencies' => [], - ]; - - $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); - } - - /** @test */ - public function works_with_proper_data_in_different_order() - { - $exts = [$this->help, $this->categories, $this->tagBackgrounds, $this->tags, $this->something]; - - $expected = [ - 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], - 'missingDependencies' => [], - 'circularDependencies' => [], - ]; - - $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); - } - - /** @test */ - public function works_with_proper_data_in_yet_another_order() - { - $exts = [$this->something, $this->tagBackgrounds, $this->help, $this->categories, $this->tags]; - - $expected = [ - 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], - 'missingDependencies' => [], - 'circularDependencies' => [], - ]; - - $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); - } - - /** @test */ - public function works_with_missing_dependencies() - { - $exts = [$this->tags, $this->categories, $this->tagBackgrounds, $this->something, $this->help, $this->missing]; - - $expected = [ - 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], - 'missingDependencies' => ['flarum-missing' => ['this-does-not-exist', 'also-not-exists']], - 'circularDependencies' => [], - ]; - - $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); - } - - /** @test */ - public function works_with_circular_dependencies() - { - $exts = [$this->tags, $this->categories, $this->tagBackgrounds, $this->something, $this->help, $this->circular1, $this->circular2]; - - $expected = [ - 'valid' => [$this->tags, $this->tagBackgrounds, $this->help, $this->categories, $this->something], - 'missingDependencies' => [], - 'circularDependencies' => ['circular2', 'circular1'], - ]; - - $this->assertEquals($expected, ExtensionManager::resolveExtensionOrder($exts)); - } -} - -class FakeExtension -{ - protected $id; - public $extensionDependencies; - - public function __construct($id, $extensionDependencies) - { - $this->id = $id; - $this->extensionDependencies = $extensionDependencies; - } - - public function getId() - { - return $this->id; - } -} From 8becd7ce66db4c025fb0906f5b41efe1f986d036 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Sat, 26 Sep 2020 13:54:10 -0400 Subject: [PATCH 20/32] Store a set of installed extension composer names instead of an associative array to their package.jsons to save memory --- src/Extension/ExtensionManager.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 6308e3f74b..be9099d344 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -83,19 +83,18 @@ public function getExtensions() // Composer 2.0 changes the structure of the installed.json manifest $installed = $installed['packages'] ?? $installed; - // We calculate and store an associative copy of the json manifest + // We calculate and store a set of installed extension IDs (associative array with null keys) // to avoid an O(n * m) complexity when finding extension dependencies. - // Additionally, by only including flarum extensions, we know that - // anything present in this associative copy is a flarum extension, removing - // the need to check for it's type. - $associativeInstalled = []; + // By only including flarum extensions, we know that anything present in this associative copy + // is a flarum extension, removing the need to check for it's type. + $installedSet = []; foreach ($installed as $package) { if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) { continue; } - $associativeInstalled[Arr::get($package, 'name')] = $package; + $installedSet[Arr::get($package, 'name')] = null; $path = isset($package['install-path']) ? $this->paths->vendor.'/composer/'.$package['install-path'] @@ -112,7 +111,7 @@ public function getExtensions() } foreach ($extensions as $extension) { - $extension->calculateDependencies($associativeInstalled); + $extension->calculateDependencies($installedSet); } $this->extensions = $extensions->sortBy(function ($extension, $name) { From ebe0107b20bf3bf7c7b78546fd3020368c4be12b Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Wed, 30 Sep 2020 15:45:12 -0400 Subject: [PATCH 21/32] Improve explanation for installedSet, put its values as true, not null --- src/Extension/ExtensionManager.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index be9099d344..3a71415d51 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -84,9 +84,11 @@ public function getExtensions() $installed = $installed['packages'] ?? $installed; // We calculate and store a set of installed extension IDs (associative array with null keys) - // to avoid an O(n * m) complexity when finding extension dependencies. + // to avoid O(n * m) complexity when finding extension dependencies. // By only including flarum extensions, we know that anything present in this associative copy // is a flarum extension, removing the need to check for it's type. + // We store the installed flarum extensions as keys, not values, of this array + // so that we have constant lookup time. $installedSet = []; foreach ($installed as $package) { @@ -94,7 +96,7 @@ public function getExtensions() continue; } - $installedSet[Arr::get($package, 'name')] = null; + $installedSet[Arr::get($package, 'name')] = true; $path = isset($package['install-path']) ? $this->paths->vendor.'/composer/'.$package['install-path'] @@ -110,6 +112,8 @@ public function getExtensions() $extensions->put($extension->getId(), $extension); } + $installedSet = $extensions->pluck('name')->toArray(); + foreach ($extensions as $extension) { $extension->calculateDependencies($installedSet); } From 3fa2cddd28ba08bfedd4f0e308d6aaa53e0283e0 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 15:05:12 -0400 Subject: [PATCH 22/32] Fix comments --- src/Extension/ExtensionManager.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 3a71415d51..3016a32068 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -83,8 +83,8 @@ public function getExtensions() // Composer 2.0 changes the structure of the installed.json manifest $installed = $installed['packages'] ?? $installed; - // We calculate and store a set of installed extension IDs (associative array with null keys) - // to avoid O(n * m) complexity when finding extension dependencies. + // We calculate and store a set of installed extension IDs (associative array with true keys) + // to speed up figuring what is and isn't a flarum extension later on. // By only including flarum extensions, we know that anything present in this associative copy // is a flarum extension, removing the need to check for it's type. // We store the installed flarum extensions as keys, not values, of this array @@ -198,9 +198,10 @@ public function disable($name) foreach ($this->getEnabledExtensions() as $possibleDependent) { foreach ($possibleDependent->extensionDependencies as $dependency) { + // We check all enabled extensions. For each of them, if any depend on the extension + // we're disabling, we add it to the list and move onto the next one. if ($dependency === $extension->getId()) { $dependentExtensions[] = $possibleDependent->getId(); - // No need to cycle through the rest of this extension's dependencies. break; } } From e1d63784b2f26ab9c4ba48b6bc6ecb050d51b94e Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 15:42:41 -0400 Subject: [PATCH 23/32] Get rid of the $installedSet mess, use $extensions to check what is/isn't a flarum extension instead. --- src/Extension/Extension.php | 10 ++++++---- src/Extension/ExtensionManager.php | 21 ++++++--------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index 57bac972f0..8329c5fb15 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -199,14 +199,16 @@ public function setVersion($version) /** * Get the list of flarum extensions that this extension depends on. * - * @param array $lockJson: The json manifest of all installed php extensions + * @param array $extensionSet: An associative array where keys are the composer package names + * of installed extensions. Used to figure out which dependencies + * are flarum extensions. */ - public function calculateDependencies($lockJson) + public function calculateDependencies($extensionSet) { $this->extensionDependencies = (new Collection(Arr::get($this->composerJson, 'require', []))) ->keys() - ->filter(function ($key) use ($lockJson) { - return array_key_exists($key, $lockJson); + ->filter(function ($key) use ($extensionSet) { + return array_key_exists($key, $extensionSet); })->map(function ($key) { return static::nameToId($key); })->toArray(); diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 3016a32068..988e7a46b6 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -83,21 +83,11 @@ public function getExtensions() // Composer 2.0 changes the structure of the installed.json manifest $installed = $installed['packages'] ?? $installed; - // We calculate and store a set of installed extension IDs (associative array with true keys) - // to speed up figuring what is and isn't a flarum extension later on. - // By only including flarum extensions, we know that anything present in this associative copy - // is a flarum extension, removing the need to check for it's type. - // We store the installed flarum extensions as keys, not values, of this array - // so that we have constant lookup time. - $installedSet = []; - foreach ($installed as $package) { if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) { continue; } - $installedSet[Arr::get($package, 'name')] = true; - $path = isset($package['install-path']) ? $this->paths->vendor.'/composer/'.$package['install-path'] : $this->paths->vendor.'/'.Arr::get($package, 'name'); @@ -109,16 +99,17 @@ public function getExtensions() $extension->setInstalled(true); $extension->setVersion(Arr::get($package, 'version')); - $extensions->put($extension->getId(), $extension); + // We use the composer package name as the key so that: + // 1. We don't have naming collisions + // 2. We can use it for constant time lookups in `calculateDependencies` + $extensions->put(Arr::get($package, 'name'), $extension); } - $installedSet = $extensions->pluck('name')->toArray(); - foreach ($extensions as $extension) { - $extension->calculateDependencies($installedSet); + $extension->calculateDependencies($extensions); } - $this->extensions = $extensions->sortBy(function ($extension, $name) { + $this->extensions = $extensions->sortBy(function ($extension) { return $extension->composerJsonAttribute('extra.flarum-extension.title'); }); } From 7acf52b55c0ed3b6073a8028a40cb1db4f6c176b Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 15:55:13 -0400 Subject: [PATCH 24/32] Go back to using $installedSet to store composer package names for installed extensions --- src/Extension/ExtensionManager.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 988e7a46b6..5e0b3e5644 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -83,11 +83,18 @@ public function getExtensions() // Composer 2.0 changes the structure of the installed.json manifest $installed = $installed['packages'] ?? $installed; + // 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. + $installedSet = []; + foreach ($installed as $package) { if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) { continue; } + $installedSet[Arr::get($package, 'name')] = true; + $path = isset($package['install-path']) ? $this->paths->vendor.'/composer/'.$package['install-path'] : $this->paths->vendor.'/'.Arr::get($package, 'name'); @@ -99,14 +106,11 @@ public function getExtensions() $extension->setInstalled(true); $extension->setVersion(Arr::get($package, 'version')); - // We use the composer package name as the key so that: - // 1. We don't have naming collisions - // 2. We can use it for constant time lookups in `calculateDependencies` - $extensions->put(Arr::get($package, 'name'), $extension); + $extensions->put($extension->getId(), $extension); } foreach ($extensions as $extension) { - $extension->calculateDependencies($extensions); + $extension->calculateDependencies($installedSet); } $this->extensions = $extensions->sortBy(function ($extension) { From 58477b534424515c80fa0155b9ac118a6fb68242 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 16:22:06 -0400 Subject: [PATCH 25/32] Code cleanup - Make extensionDependencies protected, call through encapsulated method - The dependency exceptions now store arrays of extensions, not arrays of extension ids. - Improve documentation --- .../DependentExtensionsException.php | 21 ++++++++- .../DependentExtensionsExceptionHandler.php | 2 +- .../MissingDependenciesException.php | 19 +++++++- .../MissingDependenciesExceptionHandler.php | 2 +- src/Extension/Extension.php | 19 ++++++-- src/Extension/ExtensionManager.php | 45 ++++++++----------- 6 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src/Extension/Exception/DependentExtensionsException.php b/src/Extension/Exception/DependentExtensionsException.php index 471a8000ed..847f4e6325 100644 --- a/src/Extension/Exception/DependentExtensionsException.php +++ b/src/Extension/Exception/DependentExtensionsException.php @@ -12,14 +12,19 @@ use Exception; use Flarum\Extension\Extension; + +/** + * This exception is thrown when someone attempts to disable an extension + * that other enabled extensions depend on. + */ class DependentExtensionsException extends Exception { public $extension; public $dependent_extensions; /** - * @param $extension: The extension we are attempting to activate. - * @param $dependent_extensions: Extension IDs of the Flarum extensions that depend on this extension + * @param $extension: The extension we are attempting to disable. + * @param $dependent_extensions: Enabled Flarum extensions that depend on this extension. */ public function __construct(Extension $extension, array $dependent_extensions) { @@ -28,4 +33,16 @@ public function __construct(Extension $extension, array $dependent_extensions) parent::__construct(implode("\n", $dependent_extensions)); } + + /** + * Get array of IDs for extensions that depend on this extension. + * + * @return array + */ + public function getDependentExtensionIds() + { + return array_map(function (Extension $extension) { + return $extension->getId(); + }, $this->dependent_extensions); + } } diff --git a/src/Extension/Exception/DependentExtensionsExceptionHandler.php b/src/Extension/Exception/DependentExtensionsExceptionHandler.php index 55241ccc71..a8ad877274 100644 --- a/src/Extension/Exception/DependentExtensionsExceptionHandler.php +++ b/src/Extension/Exception/DependentExtensionsExceptionHandler.php @@ -27,7 +27,7 @@ protected function errorDetails(DependentExtensionsException $e): array return [ [ 'extension' => $e->extension->getId(), - 'extensions' => $e->dependent_extensions + 'extensions' => $e->getDependentExtensionIds(), ] ]; } diff --git a/src/Extension/Exception/MissingDependenciesException.php b/src/Extension/Exception/MissingDependenciesException.php index 836775ad1a..ea2dbfd076 100644 --- a/src/Extension/Exception/MissingDependenciesException.php +++ b/src/Extension/Exception/MissingDependenciesException.php @@ -12,14 +12,18 @@ use Exception; use Flarum\Extension\Extension; +/** + * This exception is thrown when someone attempts to enable an extension + * whose Flarum extension dependencies are not all enabled. + */ class MissingDependenciesException extends Exception { public $extension; public $missing_dependencies; /** - * @param $extension: The extension we are attempting to activate. - * @param $missing_dependencies: Extension IDs of the missing flarum extension dependencies for this extension + * @param $extension: The extension we are attempting to enable. + * @param $missing_dependencies: Extensions that this extension depends on, and are not enabled. */ public function __construct(Extension $extension, array $missing_dependencies = null) { @@ -28,4 +32,15 @@ public function __construct(Extension $extension, array $missing_dependencies = parent::__construct(implode("\n", $missing_dependencies)); } + + /** + * Get array of IDs for missing (disabled) extensions that this extension depends on. + * + * @return array + */ + public function getMissingDependencyIds() { + return array_map(function(Extension $extension) { + return $extension->getId(); + }, $this->missing_dependencies); + } } diff --git a/src/Extension/Exception/MissingDependenciesExceptionHandler.php b/src/Extension/Exception/MissingDependenciesExceptionHandler.php index 995e3de683..39f8980cc2 100644 --- a/src/Extension/Exception/MissingDependenciesExceptionHandler.php +++ b/src/Extension/Exception/MissingDependenciesExceptionHandler.php @@ -27,7 +27,7 @@ protected function errorDetails(MissingDependenciesException $e): array return [ [ 'extension' => $e->extension->getId(), - 'extensions' => $e->missing_dependencies + 'extensions' => $e->getMissingDependencyIds(), ] ]; } diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index 8329c5fb15..bf0ba5e8d1 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -69,6 +69,7 @@ protected static function nameToId($name) * @var string */ protected $id; + /** * The directory of this extension. * @@ -84,11 +85,11 @@ protected static function nameToId($name) protected $composerJson; /** - * An array of extension ids for extension dependencies. + * The IDs of all Flarum extensions that this extension depends on. * * @var string[] */ - protected $dependencies; + protected $extensionDependencies; /** * Whether the extension is installed. @@ -285,6 +286,16 @@ public function getPath() return $this->path; } + /** + * The IDs of all Flarum extensions that this extension depends on. + * + * @return array + */ + public function getExtensionDependencies() + { + return $this->extensionDependencies; + } + private function getExtenders(): array { $extenderFile = $this->getExtenderFile(); @@ -397,11 +408,11 @@ public function toArray() return (array) array_merge([ 'id' => $this->getId(), 'version' => $this->getVersion(), - 'path' => $this->path, + 'path' => $this->getPath(), 'icon' => $this->getIcon(), 'hasAssets' => $this->hasAssets(), 'hasMigrations' => $this->hasMigrations(), - 'extensionDependencies' => $this->extensionDependencies, + 'extensionDependencies' => $this->getExtensionDependencies(), ], $this->composerJson); } } diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 5e0b3e5644..aa399531c7 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -74,11 +74,11 @@ public function __construct( */ public function getExtensions() { - if (is_null($this->extensions) && $this->filesystem->exists($this->paths->vendor.'/composer/installed.json')) { + if (is_null($this->extensions) && $this->filesystem->exists($this->paths->vendor . '/composer/installed.json')) { $extensions = new Collection(); // Load all packages installed by composer. - $installed = json_decode($this->filesystem->get($this->paths->vendor.'/composer/installed.json'), true); + $installed = json_decode($this->filesystem->get($this->paths->vendor . '/composer/installed.json'), true); // Composer 2.0 changes the structure of the installed.json manifest $installed = $installed['packages'] ?? $installed; @@ -96,8 +96,8 @@ public function getExtensions() $installedSet[Arr::get($package, 'name')] = true; $path = isset($package['install-path']) - ? $this->paths->vendor.'/composer/'.$package['install-path'] - : $this->paths->vendor.'/'.Arr::get($package, 'name'); + ? $this->paths->vendor . '/composer/' . $package['install-path'] + : $this->paths->vendor . '/' . Arr::get($package, 'name'); // Instantiates an Extension object using the package path and composer.json file. $extension = new Extension($path, $package); @@ -113,7 +113,7 @@ public function getExtensions() $extension->calculateDependencies($installedSet); } - $this->extensions = $extensions->sortBy(function ($extension) { + $this->extensions = $extensions->sortBy(function ($extension, $name) { return $extension->composerJsonAttribute('extra.flarum-extension.title'); }); } @@ -146,28 +146,26 @@ public function enable($name) $extension = $this->getExtension($name); $missingDependencies = []; - $enabled = $this->getEnabled(); - foreach ($extension->extensionDependencies as $dependency) { - if (! in_array($dependency, $enabled)) { - $missingDependencies[] = $dependency; + $enabledIds = $this->getEnabled(); + foreach ($extension->getExtensionDependencies() as $dependencyId) { + if (!in_array($dependencyId, $enabledIds)) { + $missingDependencies[] = $this->getExtension($dependencyId); } } - if (! empty($missingDependencies)) { + if (!empty($missingDependencies)) { throw new Exception\MissingDependenciesException($extension, $missingDependencies); } $this->dispatcher->dispatch(new Enabling($extension)); - $enabled = $this->getEnabled(); - - $enabled[] = $name; + $enabledIds[] = $name; $this->migrate($extension); $this->publishAssets($extension); - $this->setEnabled($enabled); + $this->setEnabled($enabledIds); $extension->enable($this->container); @@ -192,17 +190,12 @@ public function disable($name) $dependentExtensions = []; foreach ($this->getEnabledExtensions() as $possibleDependent) { - foreach ($possibleDependent->extensionDependencies as $dependency) { - // We check all enabled extensions. For each of them, if any depend on the extension - // we're disabling, we add it to the list and move onto the next one. - if ($dependency === $extension->getId()) { - $dependentExtensions[] = $possibleDependent->getId(); - break; - } + if (in_array($extension->getId(), $possibleDependent->getExtensionDependencies())) { + $dependentExtensions[] = $possibleDependent; } } - if (! empty($dependentExtensions)) { + if (!empty($dependentExtensions)) { throw new Exception\DependentExtensionsException($extension, $dependentExtensions); } @@ -246,8 +239,8 @@ protected function publishAssets(Extension $extension) { if ($extension->hasAssets()) { $this->filesystem->copyDirectory( - $extension->getPath().'/assets', - $this->paths->public.'/assets/extensions/'.$extension->getId() + $extension->getPath() . '/assets', + $this->paths->public . '/assets/extensions/' . $extension->getId() ); } } @@ -259,7 +252,7 @@ protected function publishAssets(Extension $extension) */ protected function unpublishAssets(Extension $extension) { - $this->filesystem->deleteDirectory($this->paths->public.'/assets/extensions/'.$extension->getId()); + $this->filesystem->deleteDirectory($this->paths->public . '/assets/extensions/' . $extension->getId()); } /** @@ -271,7 +264,7 @@ protected function unpublishAssets(Extension $extension) */ public function getAsset(Extension $extension, $path) { - return $this->paths->public.'/assets/extensions/'.$extension->getId().$path; + return $this->paths->public . '/assets/extensions/' . $extension->getId() . $path; } /** From 8923b0cc27b0e713a1628d5bed00edc14ba5fc72 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 16:24:42 -0400 Subject: [PATCH 26/32] Rename `extensionDependencies` to `extensionDependencyIds` for clarity --- src/Extension/Extension.php | 20 ++++++++++---------- src/Extension/ExtensionManager.php | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index bf0ba5e8d1..d9f9f1e8d1 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -89,7 +89,7 @@ protected static function nameToId($name) * * @var string[] */ - protected $extensionDependencies; + protected $extensionDependencyIds; /** * Whether the extension is installed. @@ -291,9 +291,9 @@ public function getPath() * * @return array */ - public function getExtensionDependencies() + public function getExtensionDependencyIds() { - return $this->extensionDependencies; + return $this->extensionDependencyIds; } private function getExtenders(): array @@ -406,13 +406,13 @@ public function migrate(Migrator $migrator, $direction = 'up') public function toArray() { return (array) array_merge([ - 'id' => $this->getId(), - 'version' => $this->getVersion(), - 'path' => $this->getPath(), - 'icon' => $this->getIcon(), - 'hasAssets' => $this->hasAssets(), - 'hasMigrations' => $this->hasMigrations(), - 'extensionDependencies' => $this->getExtensionDependencies(), + 'id' => $this->getId(), + 'version' => $this->getVersion(), + 'path' => $this->getPath(), + 'icon' => $this->getIcon(), + 'hasAssets' => $this->hasAssets(), + 'hasMigrations' => $this->hasMigrations(), + 'extensionDependencyIds' => $this->getExtensionDependencyIds(), ], $this->composerJson); } } diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index aa399531c7..56485ae613 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -147,7 +147,7 @@ public function enable($name) $missingDependencies = []; $enabledIds = $this->getEnabled(); - foreach ($extension->getExtensionDependencies() as $dependencyId) { + foreach ($extension->getExtensionDependencyIds() as $dependencyId) { if (!in_array($dependencyId, $enabledIds)) { $missingDependencies[] = $this->getExtension($dependencyId); } @@ -190,7 +190,7 @@ public function disable($name) $dependentExtensions = []; foreach ($this->getEnabledExtensions() as $possibleDependent) { - if (in_array($extension->getId(), $possibleDependent->getExtensionDependencies())) { + if (in_array($extension->getId(), $possibleDependent->getExtensionDependencyIds())) { $dependentExtensions[] = $possibleDependent; } } From 12e60d9b0c94c73bae254d42db958b051cb28367 Mon Sep 17 00:00:00 2001 From: luceos Date: Fri, 2 Oct 2020 20:25:00 +0000 Subject: [PATCH 27/32] Apply fixes from StyleCI [ci skip] [skip ci] --- .../DependentExtensionsException.php | 1 - .../MissingDependenciesException.php | 5 +++-- src/Extension/ExtensionManager.php | 22 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Extension/Exception/DependentExtensionsException.php b/src/Extension/Exception/DependentExtensionsException.php index 847f4e6325..b6219c3d48 100644 --- a/src/Extension/Exception/DependentExtensionsException.php +++ b/src/Extension/Exception/DependentExtensionsException.php @@ -12,7 +12,6 @@ use Exception; use Flarum\Extension\Extension; - /** * This exception is thrown when someone attempts to disable an extension * that other enabled extensions depend on. diff --git a/src/Extension/Exception/MissingDependenciesException.php b/src/Extension/Exception/MissingDependenciesException.php index ea2dbfd076..4fa1b86225 100644 --- a/src/Extension/Exception/MissingDependenciesException.php +++ b/src/Extension/Exception/MissingDependenciesException.php @@ -38,8 +38,9 @@ public function __construct(Extension $extension, array $missing_dependencies = * * @return array */ - public function getMissingDependencyIds() { - return array_map(function(Extension $extension) { + public function getMissingDependencyIds() + { + return array_map(function (Extension $extension) { return $extension->getId(); }, $this->missing_dependencies); } diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 56485ae613..7b393dc5aa 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -74,11 +74,11 @@ public function __construct( */ public function getExtensions() { - if (is_null($this->extensions) && $this->filesystem->exists($this->paths->vendor . '/composer/installed.json')) { + if (is_null($this->extensions) && $this->filesystem->exists($this->paths->vendor.'/composer/installed.json')) { $extensions = new Collection(); // Load all packages installed by composer. - $installed = json_decode($this->filesystem->get($this->paths->vendor . '/composer/installed.json'), true); + $installed = json_decode($this->filesystem->get($this->paths->vendor.'/composer/installed.json'), true); // Composer 2.0 changes the structure of the installed.json manifest $installed = $installed['packages'] ?? $installed; @@ -96,8 +96,8 @@ public function getExtensions() $installedSet[Arr::get($package, 'name')] = true; $path = isset($package['install-path']) - ? $this->paths->vendor . '/composer/' . $package['install-path'] - : $this->paths->vendor . '/' . Arr::get($package, 'name'); + ? $this->paths->vendor.'/composer/'.$package['install-path'] + : $this->paths->vendor.'/'.Arr::get($package, 'name'); // Instantiates an Extension object using the package path and composer.json file. $extension = new Extension($path, $package); @@ -148,12 +148,12 @@ public function enable($name) $missingDependencies = []; $enabledIds = $this->getEnabled(); foreach ($extension->getExtensionDependencyIds() as $dependencyId) { - if (!in_array($dependencyId, $enabledIds)) { + if (! in_array($dependencyId, $enabledIds)) { $missingDependencies[] = $this->getExtension($dependencyId); } } - if (!empty($missingDependencies)) { + if (! empty($missingDependencies)) { throw new Exception\MissingDependenciesException($extension, $missingDependencies); } @@ -195,7 +195,7 @@ public function disable($name) } } - if (!empty($dependentExtensions)) { + if (! empty($dependentExtensions)) { throw new Exception\DependentExtensionsException($extension, $dependentExtensions); } @@ -239,8 +239,8 @@ protected function publishAssets(Extension $extension) { if ($extension->hasAssets()) { $this->filesystem->copyDirectory( - $extension->getPath() . '/assets', - $this->paths->public . '/assets/extensions/' . $extension->getId() + $extension->getPath().'/assets', + $this->paths->public.'/assets/extensions/'.$extension->getId() ); } } @@ -252,7 +252,7 @@ protected function publishAssets(Extension $extension) */ protected function unpublishAssets(Extension $extension) { - $this->filesystem->deleteDirectory($this->paths->public . '/assets/extensions/' . $extension->getId()); + $this->filesystem->deleteDirectory($this->paths->public.'/assets/extensions/'.$extension->getId()); } /** @@ -264,7 +264,7 @@ protected function unpublishAssets(Extension $extension) */ public function getAsset(Extension $extension, $path) { - return $this->paths->public . '/assets/extensions/' . $extension->getId() . $path; + return $this->paths->public.'/assets/extensions/'.$extension->getId().$path; } /** From 4ba8aba77228b5ec9b09994683223e1cd0c37827 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 16:31:35 -0400 Subject: [PATCH 28/32] Improve extension dependency error messages, even though they aren't shown to users --- src/Extension/Exception/DependentExtensionsException.php | 2 +- src/Extension/Exception/MissingDependenciesException.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Extension/Exception/DependentExtensionsException.php b/src/Extension/Exception/DependentExtensionsException.php index b6219c3d48..c888ddd901 100644 --- a/src/Extension/Exception/DependentExtensionsException.php +++ b/src/Extension/Exception/DependentExtensionsException.php @@ -30,7 +30,7 @@ public function __construct(Extension $extension, array $dependent_extensions) $this->extension = $extension; $this->dependent_extensions = $dependent_extensions; - parent::__construct(implode("\n", $dependent_extensions)); + parent::__construct($extension->getId().' could not be disabled, because it is a dependency of: '.implode(", ", $this->getDependentExtensionIds())); } /** diff --git a/src/Extension/Exception/MissingDependenciesException.php b/src/Extension/Exception/MissingDependenciesException.php index 4fa1b86225..d5594c59c0 100644 --- a/src/Extension/Exception/MissingDependenciesException.php +++ b/src/Extension/Exception/MissingDependenciesException.php @@ -30,7 +30,7 @@ public function __construct(Extension $extension, array $missing_dependencies = $this->extension = $extension; $this->missing_dependencies = $missing_dependencies; - parent::__construct(implode("\n", $missing_dependencies)); + parent::__construct($extension->getId().' could not be enabled, because it depends on: '.implode(", ", $this->getMissingDependencyIds())); } /** From 294a8bca1a7f58075baf9498208f28cae57f3c2f Mon Sep 17 00:00:00 2001 From: luceos Date: Fri, 2 Oct 2020 20:31:52 +0000 Subject: [PATCH 29/32] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Extension/Exception/DependentExtensionsException.php | 2 +- src/Extension/Exception/MissingDependenciesException.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Extension/Exception/DependentExtensionsException.php b/src/Extension/Exception/DependentExtensionsException.php index c888ddd901..ab26347061 100644 --- a/src/Extension/Exception/DependentExtensionsException.php +++ b/src/Extension/Exception/DependentExtensionsException.php @@ -30,7 +30,7 @@ public function __construct(Extension $extension, array $dependent_extensions) $this->extension = $extension; $this->dependent_extensions = $dependent_extensions; - parent::__construct($extension->getId().' could not be disabled, because it is a dependency of: '.implode(", ", $this->getDependentExtensionIds())); + parent::__construct($extension->getId().' could not be disabled, because it is a dependency of: '.implode(', ', $this->getDependentExtensionIds())); } /** diff --git a/src/Extension/Exception/MissingDependenciesException.php b/src/Extension/Exception/MissingDependenciesException.php index d5594c59c0..114348ca40 100644 --- a/src/Extension/Exception/MissingDependenciesException.php +++ b/src/Extension/Exception/MissingDependenciesException.php @@ -30,7 +30,7 @@ public function __construct(Extension $extension, array $missing_dependencies = $this->extension = $extension; $this->missing_dependencies = $missing_dependencies; - parent::__construct($extension->getId().' could not be enabled, because it depends on: '.implode(", ", $this->getMissingDependencyIds())); + parent::__construct($extension->getId().' could not be enabled, because it depends on: '.implode(', ', $this->getMissingDependencyIds())); } /** From 36f8fa0d86156bc4b353d5cc9bb51bcc5d8f50d6 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 16:40:33 -0400 Subject: [PATCH 30/32] Fix leftover `extensionDependencies` --- src/Extension/Extension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index d9f9f1e8d1..ea7b90ed04 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -206,7 +206,7 @@ public function setVersion($version) */ public function calculateDependencies($extensionSet) { - $this->extensionDependencies = (new Collection(Arr::get($this->composerJson, 'require', []))) + $this->extensionDependencyIds = (new Collection(Arr::get($this->composerJson, 'require', []))) ->keys() ->filter(function ($key) use ($extensionSet) { return array_key_exists($key, $extensionSet); From fef0d485c69cf983e55961b289df6aeff2d2aa57 Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 17:04:45 -0400 Subject: [PATCH 31/32] Add workaround to modal bug caused by bootstrap JS --- js/src/admin/components/ExtensionsPage.js | 31 +++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/js/src/admin/components/ExtensionsPage.js b/js/src/admin/components/ExtensionsPage.js index 6f92f385b8..08c9615d24 100644 --- a/js/src/admin/components/ExtensionsPage.js +++ b/js/src/admin/components/ExtensionsPage.js @@ -116,6 +116,8 @@ export default class ExtensionsPage extends Page { } toggle(id) { + app.modal.show(LoadingModal); + const enabled = this.isEnabled(id); app @@ -129,21 +131,24 @@ export default class ExtensionsPage extends Page { if (!enabled) localStorage.setItem('enabledExtension', id); window.location.reload(); }); - - app.modal.show(LoadingModal); } onerror(e) { - app.modal.close(); - - const error = JSON.parse(e.responseText).errors[0]; - - app.alerts.show( - { type: 'error' }, - app.translator.trans(`core.lib.error.${error.code}_message`, { - extension: error.extension, - extensions: error.extensions.join(', '), - }) - ); + // We need to give the modal animation time to start; if we close the modal too early, + // it breaks the bootstrap modal library. + // TODO: This workaround should be removed when we move away from bootstrap JS for modals. + setTimeout(() => { + app.modal.close(); + + const error = JSON.parse(e.responseText).errors[0]; + + app.alerts.show( + { type: 'error' }, + app.translator.trans(`core.lib.error.${error.code}_message`, { + extension: error.extension, + extensions: error.extensions.join(', '), + }) + ); + }, 250); } } From d65b767e6b495052220e6d02df9c604e9e32741c Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov Date: Fri, 2 Oct 2020 17:11:40 -0400 Subject: [PATCH 32/32] Remove unnecessary changes --- js/src/admin/components/ExtensionsPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/admin/components/ExtensionsPage.js b/js/src/admin/components/ExtensionsPage.js index 08c9615d24..0ef4e9f9c4 100644 --- a/js/src/admin/components/ExtensionsPage.js +++ b/js/src/admin/components/ExtensionsPage.js @@ -116,8 +116,6 @@ export default class ExtensionsPage extends Page { } toggle(id) { - app.modal.show(LoadingModal); - const enabled = this.isEnabled(id); app @@ -131,6 +129,8 @@ export default class ExtensionsPage extends Page { if (!enabled) localStorage.setItem('enabledExtension', id); window.location.reload(); }); + + app.modal.show(LoadingModal); } onerror(e) {