mirror of
https://github.com/flarum/framework.git
synced 2025-02-15 01:02:45 +08:00
Basic Extension Dependency Support (#2188)
- Don't enable an extension if its dependencies are not enabled - Don't disable an extension if its dependencies are not disabled
This commit is contained in:
parent
9251aa925f
commit
eb717bb034
|
@ -123,6 +123,7 @@ export default class ExtensionsPage extends Page {
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: { enabled: !enabled },
|
body: { enabled: !enabled },
|
||||||
|
errorHandler: this.onerror.bind(this),
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!enabled) localStorage.setItem('enabledExtension', id);
|
if (!enabled) localStorage.setItem('enabledExtension', id);
|
||||||
|
@ -131,4 +132,23 @@ export default class ExtensionsPage extends Page {
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
app.modal.show(LoadingModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onerror(e) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Extension\Exception;
|
||||||
|
|
||||||
|
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 disable.
|
||||||
|
* @param $dependent_extensions: Enabled Flarum extensions that depend on this extension.
|
||||||
|
*/
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Extension\Exception;
|
||||||
|
|
||||||
|
use Flarum\Foundation\ErrorHandling\HandledError;
|
||||||
|
|
||||||
|
class DependentExtensionsExceptionHandler
|
||||||
|
{
|
||||||
|
public function handle(DependentExtensionsException $e): HandledError
|
||||||
|
{
|
||||||
|
return (new HandledError(
|
||||||
|
$e,
|
||||||
|
'dependent_extensions',
|
||||||
|
409
|
||||||
|
))->withDetails($this->errorDetails($e));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function errorDetails(DependentExtensionsException $e): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'extension' => $e->extension->getId(),
|
||||||
|
'extensions' => $e->getDependentExtensionIds(),
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Extension\Exception;
|
||||||
|
|
||||||
|
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 enable.
|
||||||
|
* @param $missing_dependencies: Extensions that this extension depends on, and are not enabled.
|
||||||
|
*/
|
||||||
|
public function __construct(Extension $extension, array $missing_dependencies = null)
|
||||||
|
{
|
||||||
|
$this->extension = $extension;
|
||||||
|
$this->missing_dependencies = $missing_dependencies;
|
||||||
|
|
||||||
|
parent::__construct($extension->getId().' could not be enabled, because it depends on: '.implode(', ', $this->getMissingDependencyIds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Extension\Exception;
|
||||||
|
|
||||||
|
use Flarum\Foundation\ErrorHandling\HandledError;
|
||||||
|
|
||||||
|
class MissingDependenciesExceptionHandler
|
||||||
|
{
|
||||||
|
public function handle(MissingDependenciesException $e): HandledError
|
||||||
|
{
|
||||||
|
return (new HandledError(
|
||||||
|
$e,
|
||||||
|
'missing_dependencies',
|
||||||
|
409
|
||||||
|
))->withDetails($this->errorDetails($e));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function errorDetails(MissingDependenciesException $e): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'extension' => $e->extension->getId(),
|
||||||
|
'extensions' => $e->getMissingDependencyIds(),
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ use Flarum\Extend\LifecycleInterface;
|
||||||
use Illuminate\Contracts\Container\Container;
|
use Illuminate\Contracts\Container\Container;
|
||||||
use Illuminate\Contracts\Support\Arrayable;
|
use Illuminate\Contracts\Support\Arrayable;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use League\Flysystem\Adapter\Local;
|
use League\Flysystem\Adapter\Local;
|
||||||
use League\Flysystem\Filesystem;
|
use League\Flysystem\Filesystem;
|
||||||
|
@ -51,6 +52,14 @@ class Extension implements Arrayable
|
||||||
'jpg' => 'image/jpeg',
|
'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.
|
* Unique Id of the extension.
|
||||||
*
|
*
|
||||||
|
@ -60,6 +69,7 @@ class Extension implements Arrayable
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $id;
|
protected $id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The directory of this extension.
|
* The directory of this extension.
|
||||||
*
|
*
|
||||||
|
@ -74,6 +84,13 @@ class Extension implements Arrayable
|
||||||
*/
|
*/
|
||||||
protected $composerJson;
|
protected $composerJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IDs of all Flarum extensions that this extension depends on.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $extensionDependencyIds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the extension is installed.
|
* Whether the extension is installed.
|
||||||
*
|
*
|
||||||
|
@ -104,9 +121,7 @@ class Extension implements Arrayable
|
||||||
*/
|
*/
|
||||||
protected function assignId()
|
protected function assignId()
|
||||||
{
|
{
|
||||||
list($vendor, $package) = explode('/', $this->name);
|
$this->id = static::nameToId($this->name);
|
||||||
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
|
|
||||||
$this->id = "$vendor-$package";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function extend(Container $app)
|
public function extend(Container $app)
|
||||||
|
@ -182,6 +197,24 @@ class Extension implements Arrayable
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of flarum extensions that this extension depends on.
|
||||||
|
*
|
||||||
|
* @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($extensionSet)
|
||||||
|
{
|
||||||
|
$this->extensionDependencyIds = (new Collection(Arr::get($this->composerJson, 'require', [])))
|
||||||
|
->keys()
|
||||||
|
->filter(function ($key) use ($extensionSet) {
|
||||||
|
return array_key_exists($key, $extensionSet);
|
||||||
|
})->map(function ($key) {
|
||||||
|
return static::nameToId($key);
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
@ -253,6 +286,16 @@ class Extension implements Arrayable
|
||||||
return $this->path;
|
return $this->path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IDs of all Flarum extensions that this extension depends on.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getExtensionDependencyIds()
|
||||||
|
{
|
||||||
|
return $this->extensionDependencyIds;
|
||||||
|
}
|
||||||
|
|
||||||
private function getExtenders(): array
|
private function getExtenders(): array
|
||||||
{
|
{
|
||||||
$extenderFile = $this->getExtenderFile();
|
$extenderFile = $this->getExtenderFile();
|
||||||
|
@ -365,10 +408,11 @@ class Extension implements Arrayable
|
||||||
return (array) array_merge([
|
return (array) array_merge([
|
||||||
'id' => $this->getId(),
|
'id' => $this->getId(),
|
||||||
'version' => $this->getVersion(),
|
'version' => $this->getVersion(),
|
||||||
'path' => $this->path,
|
'path' => $this->getPath(),
|
||||||
'icon' => $this->getIcon(),
|
'icon' => $this->getIcon(),
|
||||||
'hasAssets' => $this->hasAssets(),
|
'hasAssets' => $this->hasAssets(),
|
||||||
'hasMigrations' => $this->hasMigrations(),
|
'hasMigrations' => $this->hasMigrations(),
|
||||||
|
'extensionDependencyIds' => $this->getExtensionDependencyIds(),
|
||||||
], $this->composerJson);
|
], $this->composerJson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,11 +83,18 @@ class ExtensionManager
|
||||||
// Composer 2.0 changes the structure of the installed.json manifest
|
// Composer 2.0 changes the structure of the installed.json manifest
|
||||||
$installed = $installed['packages'] ?? $installed;
|
$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) {
|
foreach ($installed as $package) {
|
||||||
if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) {
|
if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$installedSet[Arr::get($package, 'name')] = true;
|
||||||
|
|
||||||
$path = isset($package['install-path'])
|
$path = isset($package['install-path'])
|
||||||
? $this->paths->vendor.'/composer/'.$package['install-path']
|
? $this->paths->vendor.'/composer/'.$package['install-path']
|
||||||
: $this->paths->vendor.'/'.Arr::get($package, 'name');
|
: $this->paths->vendor.'/'.Arr::get($package, 'name');
|
||||||
|
@ -101,6 +108,11 @@ class ExtensionManager
|
||||||
|
|
||||||
$extensions->put($extension->getId(), $extension);
|
$extensions->put($extension->getId(), $extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($extensions as $extension) {
|
||||||
|
$extension->calculateDependencies($installedSet);
|
||||||
|
}
|
||||||
|
|
||||||
$this->extensions = $extensions->sortBy(function ($extension, $name) {
|
$this->extensions = $extensions->sortBy(function ($extension, $name) {
|
||||||
return $extension->composerJsonAttribute('extra.flarum-extension.title');
|
return $extension->composerJsonAttribute('extra.flarum-extension.title');
|
||||||
});
|
});
|
||||||
|
@ -133,17 +145,27 @@ class ExtensionManager
|
||||||
|
|
||||||
$extension = $this->getExtension($name);
|
$extension = $this->getExtension($name);
|
||||||
|
|
||||||
|
$missingDependencies = [];
|
||||||
|
$enabledIds = $this->getEnabled();
|
||||||
|
foreach ($extension->getExtensionDependencyIds() as $dependencyId) {
|
||||||
|
if (! in_array($dependencyId, $enabledIds)) {
|
||||||
|
$missingDependencies[] = $this->getExtension($dependencyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($missingDependencies)) {
|
||||||
|
throw new Exception\MissingDependenciesException($extension, $missingDependencies);
|
||||||
|
}
|
||||||
|
|
||||||
$this->dispatcher->dispatch(new Enabling($extension));
|
$this->dispatcher->dispatch(new Enabling($extension));
|
||||||
|
|
||||||
$enabled = $this->getEnabled();
|
$enabledIds[] = $name;
|
||||||
|
|
||||||
$enabled[] = $name;
|
|
||||||
|
|
||||||
$this->migrate($extension);
|
$this->migrate($extension);
|
||||||
|
|
||||||
$this->publishAssets($extension);
|
$this->publishAssets($extension);
|
||||||
|
|
||||||
$this->setEnabled($enabled);
|
$this->setEnabled($enabledIds);
|
||||||
|
|
||||||
$extension->enable($this->container);
|
$extension->enable($this->container);
|
||||||
|
|
||||||
|
@ -165,6 +187,18 @@ class ExtensionManager
|
||||||
|
|
||||||
$extension = $this->getExtension($name);
|
$extension = $this->getExtension($name);
|
||||||
|
|
||||||
|
$dependentExtensions = [];
|
||||||
|
|
||||||
|
foreach ($this->getEnabledExtensions() as $possibleDependent) {
|
||||||
|
if (in_array($extension->getId(), $possibleDependent->getExtensionDependencyIds())) {
|
||||||
|
$dependentExtensions[] = $possibleDependent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($dependentExtensions)) {
|
||||||
|
throw new Exception\DependentExtensionsException($extension, $dependentExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
$this->dispatcher->dispatch(new Disabling($extension));
|
$this->dispatcher->dispatch(new Disabling($extension));
|
||||||
|
|
||||||
unset($enabled[$k]);
|
unset($enabled[$k]);
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
|
|
||||||
namespace Flarum\Foundation;
|
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\ExceptionHandler;
|
||||||
use Flarum\Foundation\ErrorHandling\LogReporter;
|
use Flarum\Foundation\ErrorHandling\LogReporter;
|
||||||
use Flarum\Foundation\ErrorHandling\Registry;
|
use Flarum\Foundation\ErrorHandling\Registry;
|
||||||
|
@ -57,6 +61,8 @@ class ErrorServiceProvider extends AbstractServiceProvider
|
||||||
return [
|
return [
|
||||||
IlluminateValidationException::class => ExceptionHandler\IlluminateValidationExceptionHandler::class,
|
IlluminateValidationException::class => ExceptionHandler\IlluminateValidationExceptionHandler::class,
|
||||||
ValidationException::class => ExceptionHandler\ValidationExceptionHandler::class,
|
ValidationException::class => ExceptionHandler\ValidationExceptionHandler::class,
|
||||||
|
DependentExtensionsException::class => DependentExtensionsExceptionHandler::class,
|
||||||
|
MissingDependenciesException::class => MissingDependenciesExceptionHandler::class,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user