Implemented extensions as an object, usable by backend and frontend.

This commit is contained in:
Daniel Klabbers 2016-02-10 15:12:24 +01:00 committed by Franz Liedke
parent 10f3846646
commit b11b952aff
10 changed files with 430 additions and 117 deletions

View File

@ -11,7 +11,6 @@ import listItems from 'flarum/helpers/listItems';
export default class ExtensionsPage extends Component {
view() {
const extensions = Object.keys(app.extensions).map(id => app.extensions[id]);
return (
<div className="ExtensionsPage">
@ -29,15 +28,15 @@ export default class ExtensionsPage extends Component {
<div className="ExtensionsPage-list">
<div className="container">
<ul className="ExtensionList">
{extensions
.sort((a, b) => a.extra['flarum-extension'].title.localeCompare(b.extra['flarum-extension'].title))
.map(extension => {
{Object.keys(app.extensions)
.map(id => {
const extension = app.extensions[id];
const controls = this.controlItems(extension.id).toArray();
return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
<div className="ExtensionListItem-content">
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.extra['flarum-extension'].icon}>
{extension.extra['flarum-extension'].icon ? icon(extension.extra['flarum-extension'].icon.name) : ''}
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
</span>
{controls.length ? (
<Dropdown

View File

@ -72,7 +72,7 @@ class ClientController extends BaseClientController
$view->setVariable('settings', $settings);
$view->setVariable('permissions', Permission::map());
$view->setVariable('extensions', $this->extensions->getInfo());
$view->setVariable('extensions', $this->extensions->getExtensions()->toArray());
return $view;
}

View File

@ -39,9 +39,8 @@ class DatabaseMigrationRepository implements MigrationRepositoryInterface
/**
* Create a new database migration repository instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param string $table
* @return void
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param string $table
*/
public function __construct(Resolver $resolver, $table)
{

View File

@ -11,6 +11,7 @@
namespace Flarum\Database;
use Flarum\Extension\Extension;
use Illuminate\Database\ConnectionResolverInterface as Resolver;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
@ -55,35 +56,34 @@ class Migrator
/**
* Create a new migrator instance.
*
* @param \Flarum\Database\MigrationRepositoryInterface $repository
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
* @param \Flarum\Database\MigrationRepositoryInterface $repository
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param \Illuminate\Filesystem\Filesystem $files
*/
public function __construct(
MigrationRepositoryInterface $repository,
Resolver $resolver,
Filesystem $files
) {
$this->files = $files;
$this->resolver = $resolver;
$this->files = $files;
$this->resolver = $resolver;
$this->repository = $repository;
}
/**
* Run the outstanding migrations at a given path.
*
* @param string $path
* @param string $extension
* @param string $path
* @param Extension $extension
* @return void
*/
public function run($path, $extension = null)
public function run($path, Extension $extension = null)
{
$this->notes = [];
$files = $this->getMigrationFiles($path);
$ran = $this->repository->getRan($extension);
$ran = $this->repository->getRan($extension ? $extension->getId() : null);
$migrations = array_diff($files, $ran);
@ -95,11 +95,10 @@ class Migrator
/**
* Run an array of migrations.
*
* @param array $migrations
* @param bool $pretend
* @return void
* @param array $migrations
* @param Extension $extension
*/
public function runMigrationList($migrations, $extension)
public function runMigrationList($migrations, Extension $extension = null)
{
// First we will just make sure that there are any migrations to run. If there
// aren't, we will just make a note of it to the developer so they're aware
@ -121,11 +120,11 @@ class Migrator
/**
* Run "up" a migration instance.
*
* @param string $file
* @param string $extension
* @param string $file
* @param Extension $extension
* @return void
*/
protected function runUp($file, $extension)
protected function runUp($file, Extension $extension = null)
{
// First we will resolve a "real" instance of the migration class from this
// migration file name. Once we have the instances we can run the actual
@ -137,7 +136,7 @@ class Migrator
// Once we have run a migrations class, we will log that it was run in this
// repository so that we don't try to run it next time we do a migration
// in the application. A migration repository keeps the migrate order.
$this->repository->log($file, $extension);
$this->repository->log($file, $extension ? $extension->getId() : null);
$this->note("<info>Migrated:</info> $file");
}
@ -145,14 +144,15 @@ class Migrator
/**
* Rolls all of the currently applied migrations back.
*
* @param bool $pretend
* @param string $path
* @param Extension $extension
* @return int
*/
public function reset($path, $extension = null)
public function reset($path, Extension $extension = null)
{
$this->notes = [];
$migrations = array_reverse($this->repository->getRan($extension));
$migrations = array_reverse($this->repository->getRan($extension->getId()));
$this->requireFiles($path, $migrations);
@ -172,11 +172,11 @@ class Migrator
/**
* Run "down" a migration instance.
*
* @param string $file
* @param string $extension
* @param string $file
* @param Extension $extension
* @return void
*/
protected function runDown($file, $extension = null)
protected function runDown($file, Extension $extension = null)
{
// First we will get the file name of the migration so we can resolve out an
// instance of the migration. Once we get an instance we can either run a
@ -188,7 +188,7 @@ class Migrator
// Once we have successfully run the migration "down" we will remove it from
// the migration repository so it will be considered to have not been run
// by the application then will be able to fire by any later operation.
$this->repository->delete($file, $extension);
$this->repository->delete($file, $extension ? $extension->getId() : null);
$this->note("<info>Rolled back:</info> $file");
}
@ -196,12 +196,12 @@ class Migrator
/**
* Get all of the migration files in a given path.
*
* @param string $path
* @param string $path
* @return array
*/
public function getMigrationFiles($path)
{
$files = $this->files->glob($path.'/*_*.php');
$files = $this->files->glob($path . '/*_*.php');
// Once we have the array of files in the directory we will just remove the
// extension and take the basename of the file which is all we need when
@ -225,28 +225,36 @@ class Migrator
/**
* Require in all the migration files in a given path.
*
* @param string $path
* @param array $files
* @param string $path
* @param array $files
* @return void
*/
public function requireFiles($path, array $files)
{
foreach ($files as $file) {
$this->files->requireOnce($path.'/'.$file.'.php');
$this->files->requireOnce($path . '/' . $file . '.php');
}
}
/**
* Resolve a migration instance from a file.
*
* @param string $file
* @param string $file
* @param Extension $extension
* @return object
*/
public function resolve($file, $extension = null)
public function resolve($file, Extension $extension = null)
{
$file = implode('_', array_slice(explode('_', $file), 4));
$class = ($extension ? str_replace('-', '\\', $extension) : 'Flarum\\Core') . '\\Migration\\';
// flagrow/image-upload
if ($extension) {
$class = str_replace('/', '\\', $extension->name);
} else {
$class = 'Flarum\\Core';
}
$class .= '\\Migration\\';
$class .= Str::studly($file);
@ -256,7 +264,7 @@ class Migrator
/**
* Raise a note event for the migrator.
*
* @param string $message
* @param string $message
* @return void
*/
protected function note($message)
@ -277,7 +285,7 @@ class Migrator
/**
* Resolve the database connection instance.
*
* @param string $connection
* @param string $connection
* @return \Illuminate\Database\Connection
*/
public function resolveConnection($connection)
@ -288,7 +296,7 @@ class Migrator
/**
* Set the default connection name.
*
* @param string $name
* @param string $name
* @return void
*/
public function setConnection($name)

View File

@ -10,6 +10,8 @@
namespace Flarum\Event;
use Flarum\Extension\Extension;
class ExtensionWasDisabled
{
/**
@ -18,9 +20,9 @@ class ExtensionWasDisabled
protected $extension;
/**
* @param string $extension
* @param Extension $extension
*/
public function __construct($extension)
public function __construct(Extension $extension)
{
$this->extension = $extension;
}

View File

@ -10,6 +10,8 @@
namespace Flarum\Event;
use Flarum\Extension\Extension;
class ExtensionWasEnabled
{
/**
@ -18,9 +20,9 @@ class ExtensionWasEnabled
protected $extension;
/**
* @param string $extension
* @param Extension $extension
*/
public function __construct($extension)
public function __construct(Extension $extension)
{
$this->extension = $extension;
}

View File

@ -0,0 +1,252 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Extension;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
/**
* @property string $name
* @property string $description
* @property string $type
* @property array $keywords
* @property string $homepage
* @property string $time
* @property string $license
* @property array $authors
* @property array $support
* @property array $require
* @property array $requireDev
* @property array $autoload
* @property array $autoloadDev
* @property array $conflict
* @property array $replace
* @property array $provide
* @property array $suggest
* @property array $extra
*/
class Extension implements Arrayable
{
/**
* Unique Id of the extension.
*
* @info Identical to the directory in the extensions directory.
* @example flarum_suspend
*
* @var string
*/
protected $id;
/**
* The directory of this extension.
*
* @var string
*/
protected $path;
/**
* Composer json of the package.
*
* @var array
*/
protected $composerJson;
/**
* Whether the extension is installed.
*
* @var bool
*/
protected $installed = true;
/**
* The installed version of the extension.
*
* @var string
*/
protected $version;
/**
* Whether the extension is enabled.
*
* @var bool
*/
protected $enabled = false;
/**
* @param $path
* @param array $composerJson
*/
public function __construct($path, $composerJson)
{
$this->id = end(explode('/', $path));
$this->path = $path;
$this->composerJson = $composerJson;
}
/**
* {@inheritdoc}
*/
public function __get($name)
{
return $this->composerJsonAttribute(Str::snake($name, '-'));
}
/**
* {@inheritdoc}
*/
public function __isset($name)
{
return isset($this->{$name}) || $this->composerJsonAttribute(Str::snake($name, '-'));
}
/**
* Dot notation getter for composer.json attributes.
*
* @see https://laravel.com/docs/5.1/helpers#arrays
*
* @param $name
* @return mixed
*/
public function composerJsonAttribute($name)
{
return Arr::get($this->composerJson, $name);
}
/**
* @param boolean $installed
* @return Extension
*/
public function setInstalled($installed)
{
$this->installed = $installed;
return $this;
}
/**
* @return boolean
*/
public function isInstalled()
{
return $this->installed;
}
/**
* @param string $version
* @return Extension
*/
public function setVersion($version)
{
$this->version = $version;
return $this;
}
/**
* @return string
*/
public function getVersion()
{
return $this->version;
}
/**
* Loads the icon information from the composer.json.
*
* @return array|null
*/
public function getIcon()
{
if (($icon = $this->composerJsonAttribute('extra.flarum-extension.icon'))) {
if ($file = Arr::get($icon, 'image')) {
$file = $this->path . '/' . $file;
if (file_exists($file)) {
$mimetype = pathinfo($file, PATHINFO_EXTENSION) === 'svg'
? 'image/svg+xml'
: finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file);
$data = file_get_contents($file);
$icon['backgroundImage'] = 'url(\'data:' . $mimetype . ';base64,' . base64_encode($data) . '\')';
}
}
return $icon;
}
}
/**
* @param boolean $enabled
* @return Extension
*/
public function setEnabled($enabled)
{
$this->enabled = $enabled;
return $this;
}
/**
* @return boolean
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* The raw path of the directory under extensions.
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Tests whether the extension has assets.
*
* @return bool
*/
public function hasAssets()
{
return realpath($this->path . '/assets/') !== false;
}
/**
* Tests whether the extension has migrations.
*
* @return bool
*/
public function hasMigrations()
{
return realpath($this->path . '/migrations/') !== false;
}
/**
* Generates an array result for the object.
*
* @return array
*/
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(),
], $this->composerJson);
}
}

View File

@ -19,6 +19,8 @@ use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class ExtensionManager
{
@ -38,70 +40,84 @@ class ExtensionManager
*/
protected $filesystem;
public function __construct(SettingsRepositoryInterface $config, Application $app, Migrator $migrator, Dispatcher $dispatcher, Filesystem $filesystem)
{
$this->config = $config;
$this->app = $app;
$this->migrator = $migrator;
public function __construct(
SettingsRepositoryInterface $config,
Application $app,
Migrator $migrator,
Dispatcher $dispatcher,
Filesystem $filesystem
) {
$this->config = $config;
$this->app = $app;
$this->migrator = $migrator;
$this->dispatcher = $dispatcher;
$this->filesystem = $filesystem;
}
public function getInfo()
/**
* @return Collection
*/
public function getExtensions()
{
$extensionsDir = $this->getExtensionsDir();
$dirs = array_diff(scandir($extensionsDir), ['.', '..']);
$extensions = [];
$dirs = array_diff(scandir($extensionsDir), ['.', '..']);
$extensions = new Collection();
$installed = json_decode(file_get_contents(public_path('vendor/composer/installed.json')), true);
foreach ($dirs as $dir) {
if (file_exists($manifest = $extensionsDir . '/' . $dir . '/composer.json')) {
$extension = json_decode(file_get_contents($manifest), true);
$extension = new Extension(
$extensionsDir . '/' . $dir,
json_decode(file_get_contents($manifest), true)
);
if (empty($extension['name'])) {
if (empty($extension->name)) {
continue;
}
if (isset($extension['extra']['flarum-extension']['icon'])) {
$icon = &$extension['extra']['flarum-extension']['icon'];
if ($file = array_get($icon, 'image')) {
$file = $extensionsDir . '/' . $dir . '/' . $file;
if (file_exists($file)) {
$mimetype = pathinfo($file, PATHINFO_EXTENSION) === 'svg'
? 'image/svg+xml'
: finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file);
$data = file_get_contents($file);
$icon['backgroundImage'] = 'url(\'data:' . $mimetype . ';base64,' . base64_encode($data) . '\')';
}
}
}
foreach ($installed as $package) {
if ($package['name'] === $extension['name']) {
$extension['version'] = $package['version'];
if ($package['name'] === $extension->name) {
$extension->setInstalled(true);
$extension->setVersion($package['version']);
$extension->setEnabled($this->isEnabled($dir));
}
}
$extension['id'] = $dir;
$extensions[$dir] = $extension;
$extensions->put($dir, $extension);
}
}
return $extensions;
return $extensions->sortBy(function ($extension, $name) {
return $extension->composerJsonAttribute('extra.flarum-extension.title');
});
}
public function enable($extension)
/**
* Loads an Extension with all information.
*
* @param string $name
* @return Extension|null
*/
public function getExtension($name)
{
if (! $this->isEnabled($extension)) {
return $this->getExtensions()->get($name);
}
/**
* Enables the extension.
*
* @param string $name
*/
public function enable($name)
{
if (!$this->isEnabled($name)) {
$extension = $this->getExtension($name);
$enabled = $this->getEnabled();
$enabled[] = $extension;
$enabled[] = $name;
$this->migrate($extension);
@ -109,87 +125,122 @@ class ExtensionManager
$this->setEnabled($enabled);
$extension->setEnabled(true);
$this->dispatcher->fire(new ExtensionWasEnabled($extension));
}
}
public function disable($extension)
/**
* Disables an extension.
*
* @param string $name
*/
public function disable($name)
{
$enabled = $this->getEnabled();
if (($k = array_search($extension, $enabled)) !== false) {
if (($k = array_search($name, $enabled)) !== false) {
unset($enabled[$k]);
$extension = $this->getExtension($name);
$this->setEnabled($enabled);
$extension->setEnabled(false);
$this->dispatcher->fire(new ExtensionWasDisabled($extension));
}
}
public function uninstall($extension)
/**
* Uninstalls an extension.
*
* @param string $name
*/
public function uninstall($name)
{
$this->disable($extension);
$extension = $this->getExtension($name);
$this->disable($name);
$this->migrateDown($extension);
$this->unpublishAssets($extension);
$extension->setInstalled(false);
$this->dispatcher->fire(new ExtensionWasUninstalled($extension));
}
/**
* Copy the assets from an extension's assets directory into public view.
*
* @param string $extension
* @param Extension $extension
*/
protected function publishAssets($extension)
protected function publishAssets(Extension $extension)
{
$this->filesystem->copyDirectory(
$this->app->basePath().'/extensions/'.$extension.'/assets',
$this->app->basePath().'/assets/extensions/'.$extension
);
if ($extension->hasAssets()) {
$this->filesystem->copyDirectory(
$this->app->basePath() . '/extensions/' . $extension->getId() . '/assets',
$this->app->basePath() . '/assets/extensions/' . $extension->getId()
);
}
}
/**
* Delete an extension's assets from public view.
*
* @param string $extension
* @param Extension $extension
*/
protected function unpublishAssets($extension)
protected function unpublishAssets(Extension $extension)
{
$this->filesystem->deleteDirectory($this->app->basePath().'/assets/extensions/'.$extension);
$this->filesystem->deleteDirectory($this->app->basePath() . '/assets/extensions/' . $extension);
}
/**
* Get the path to an extension's published asset.
*
* @param string $extension
* @param string $path
* @param Extension $extension
* @param string $path
* @return string
*/
public function getAsset($extension, $path)
public function getAsset(Extension $extension, $path)
{
return $this->app->basePath().'/assets/extensions/'.$extension.$path;
return $this->app->basePath() . '/assets/extensions/' . $extension->getId() . $path;
}
public function migrate($extension, $up = true)
/**
* Runs the database migrations for the extension.
*
* @param Extension $extension
* @param bool|true $up
*/
public function migrate(Extension $extension, $up = true)
{
$migrationDir = public_path('extensions/' . $extension . '/migrations');
if ($extension->hasMigrations()) {
$migrationDir = public_path('extensions/' . $extension->getId() . '/migrations');
$this->app->bind('Illuminate\Database\Schema\Builder', function ($container) {
return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder();
});
$this->app->bind('Illuminate\Database\Schema\Builder', function ($container) {
return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder();
});
if ($up) {
$this->migrator->run($migrationDir, $extension);
} else {
$this->migrator->reset($migrationDir, $extension);
if ($up) {
$this->migrator->run($migrationDir, $extension);
} else {
$this->migrator->reset($migrationDir, $extension);
}
}
}
public function migrateDown($extension)
/**
* Runs the database migrations to reset the database to its old state.
*
* @param Extension $extension
*/
public function migrateDown(Extension $extension)
{
$this->migrate($extension, false);
$this->migrate($extension->getId(), false);
}
public function getMigrator()

View File

@ -343,7 +343,7 @@ class InstallCommand extends AbstractCommand
'flarum-pusher',
];
foreach ($extensions->getInfo() as $name => $extension) {
foreach ($extensions->getExtensions() as $name => $extension) {
if (in_array($name, $disabled)) {
continue;
}

View File

@ -74,8 +74,8 @@ class MigrateCommand extends AbstractCommand
$migrator = $extensions->getMigrator();
foreach ($extensions->getInfo() as $name => $extension) {
if (! $extensions->isEnabled($name)) {
foreach ($extensions->getExtensions() as $name => $extension) {
if (! $extension->isEnabled()) {
continue;
}