1
0
mirror of https://github.com/flarum/core.git synced 2025-10-15 00:44:40 +02:00
Files
php-flarum/src/Extension/Extension.php
Franz Liedke 790d5beee5 Split up the installer logic
This is probably the most complicated way I could find to fix #1587.

Jokes aside, this was done with a few goals in mind:
- Reduce coupling between the installer and the rest of Flarum's
  "Application", which we are building during installation.
- Move the installer logic to several smaller classes, which can then
  be used by the web frontend and the console task, instead of the
  former hacking its way into the latter to be "DRY".
- Separate installer infrastructure (the "pipeline", with the ability
  to revert steps upon failure) from the actual steps being taken.

The problem was conceptual, and would certainly re-occur in a similar
fashion if we wouldn't tackle it at its roots.

It is fixed now, because we no longer use the ExtensionManager for
enabling extensions, but instead duplicate some of its logic. That is
fine because we don't want to do everything it does, e.g. omit
extenders' lifecycle hooks (which depend on the Application instance
being complete).

> for each desired change, make the change easy (warning: this may be
> hard), then make the easy change

- Kent Beck, https://twitter.com/kentbeck/status/250733358307500032

Fixes #1587.
2019-01-31 21:52:04 +01:00

375 lines
8.7 KiB
PHP

<?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 Flarum\Database\Migrator;
use Flarum\Extend\Compat;
use Flarum\Extend\LifecycleInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\MountManager;
use League\Flysystem\Plugin\ListFiles;
/**
* @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
{
const LOGO_MIMETYPES = [
'svg' => 'image/svg+xml',
'png' => 'image/png',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
];
/**
* 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;
/**
* @param $path
* @param array $composerJson
*/
public function __construct($path, $composerJson)
{
$this->path = $path;
$this->composerJson = $composerJson;
$this->assignId();
}
/**
* Assigns the id for the extension used globally.
*/
protected function assignId()
{
list($vendor, $package) = explode('/', $this->name);
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
$this->id = "$vendor-$package";
}
public function extend(Container $app)
{
foreach ($this->getExtenders() as $extender) {
// If an extension has not yet switched to the new extend.php
// format, it might return a function (or more of them). We wrap
// these in a Compat extender to enjoy an unique interface.
if ($extender instanceof \Closure || is_string($extender)) {
$extender = new Compat($extender);
}
$extender->extend($app, $this);
}
}
/**
* {@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 bool $installed
* @return Extension
*/
public function setInstalled($installed)
{
$this->installed = $installed;
return $this;
}
/**
* @return bool
*/
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()
{
$icon = $this->composerJsonAttribute('extra.flarum-extension.icon');
$file = Arr::get($icon, 'image');
if (is_null($icon) || is_null($file)) {
return $icon;
}
$file = $this->path.'/'.$file;
if (file_exists($file)) {
$extension = pathinfo($file, PATHINFO_EXTENSION);
if (! array_key_exists($extension, self::LOGO_MIMETYPES)) {
throw new \RuntimeException('Invalid image type');
}
$mimetype = self::LOGO_MIMETYPES[$extension];
$data = base64_encode(file_get_contents($file));
$icon['backgroundImage'] = "url('data:$mimetype;base64,$data')";
}
return $icon;
}
public function enable(Container $container)
{
foreach ($this->getLifecycleExtenders() as $extender) {
$extender->onEnable($container, $this);
}
}
public function disable(Container $container)
{
foreach ($this->getLifecycleExtenders() as $extender) {
$extender->onDisable($container, $this);
}
}
/**
* The raw path of the directory under extensions.
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
private function getExtenders(): array
{
$extenderFile = $this->getExtenderFile();
if (! $extenderFile) {
return [];
}
$extenders = require $extenderFile;
if (! is_array($extenders)) {
$extenders = [$extenders];
}
return array_flatten($extenders);
}
/**
* @return LifecycleInterface[]
*/
private function getLifecycleExtenders(): array
{
return array_filter(
$this->getExtenders(),
function ($extender) {
return $extender instanceof LifecycleInterface;
}
);
}
private function getExtenderFile(): ?string
{
$filename = "{$this->path}/extend.php";
if (file_exists($filename)) {
return $filename;
}
// To give extension authors some time to migrate to the new extension
// format, we will also fallback to the old bootstrap.php name. Consider
// this feature deprecated.
$deprecatedFilename = "{$this->path}/bootstrap.php";
if (file_exists($deprecatedFilename)) {
return $deprecatedFilename;
}
}
/**
* Tests whether the extension has assets.
*
* @return bool
*/
public function hasAssets()
{
return realpath($this->path.'/assets/') !== false;
}
public function copyAssetsTo(FilesystemInterface $target)
{
if (! $this->hasAssets()) {
return;
}
$mount = new MountManager([
'source' => $source = new Filesystem(new Local($this->getPath().'/assets')),
'target' => $target,
]);
$source->addPlugin(new ListFiles);
$assetFiles = $source->listFiles('/', true);
foreach ($assetFiles as $file) {
$mount->copy("source://$file[path]", "target://extensions/$this->id/$file[path]");
}
}
/**
* Tests whether the extension has migrations.
*
* @return bool
*/
public function hasMigrations()
{
return realpath($this->path.'/migrations/') !== false;
}
public function migrate(Migrator $migrator, $direction = 'up')
{
if (! $this->hasMigrations()) {
return;
}
if ($direction == 'up') {
return $migrator->run($this->getPath().'/migrations', $this);
} else {
return $migrator->reset($this->getPath().'/migrations', $this);
}
}
/**
* 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);
}
}