Skip to content

Commit

Permalink
Added initial composer support for plugin management
Browse files Browse the repository at this point in the history
  • Loading branch information
jaxwilko committed Sep 1, 2023
1 parent ca85e3c commit 20269e5
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 9 deletions.
10 changes: 10 additions & 0 deletions modules/system/classes/PluginBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class PluginBase extends ServiceProviderBase
*/
public $disabled = false;

/**
* @var ?string The composer package a plugin belongs to.
*/
public readonly ?string $package;

/**
* Returns information about this plugin, including plugin name and developer name.
*
Expand Down Expand Up @@ -477,6 +482,11 @@ public function getPluginVersions(bool $includeScripts = true): array
return $versions;
}

public function setComposerPackage(?string $package): void
{
$this->package = $package;
}

/**
* Verifies the plugin's dependencies are present and enabled
*/
Expand Down
66 changes: 66 additions & 0 deletions modules/system/classes/PluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use FilesystemIterator;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use System\Classes\Packager\Composer;
use System\Models\PluginVersion;
use Winter\Storm\Foundation\Application;
use Winter\Storm\Support\ClassLoader;
Expand Down Expand Up @@ -61,6 +62,11 @@ class PluginManager
*/
protected $pluginFlags = [];

/**
* @var array Array of packages installed via composer
*/
protected $composerPackages = [];

/**
* @var PluginVersion[] Local cache of loaded PluginVersion records keyed by plugin code
*/
Expand Down Expand Up @@ -108,6 +114,9 @@ protected function init(): void
{
$this->app = App::make('app');

// Load the packages registered via composer
$this->loadComposer();

// Load the plugins from the filesystem and sort them by dependencies
$this->loadPlugins();

Expand All @@ -119,6 +128,53 @@ protected function init(): void
$this->registerPluginReplacements();
}

public function loadComposer(): array
{
return $this->composerPackages = Cache::rememberForever(Composer::COMPOSER_CACHE_KEY, function () {
$outdated = Composer::show('outdated')['installed'] ?? [];
$outdated = array_combine(array_map(fn ($pack) => $pack['name'], $outdated), $outdated);

$paths = Composer::show(path: true)['installed'] ?? [];
$paths = array_combine(
array_map(fn ($pack) => $pack['name'], $paths),
array_map(fn ($pack) => $pack['path'], $paths),
);

$packages = [];
foreach (Composer::show()['installed'] as $package) {
if ($package['direct-dependency']) {
if (isset($outdated[$package['name']])) {
$package['outdated'] = $outdated[$package['name']];
}
if (isset($paths[$package['name']])) {
$package['path'] = $paths[$package['name']];
if (is_file($paths[$package['name']] . '/composer.json')) {
$json = json_decode(file_get_contents($paths[$package['name']] . '/composer.json'));
if (isset($json->type)) {
$package['type'] = $json->type;
switch ($package['type']) {
case 'winter-plugin':
$packages['plugins'][$package['name']] = $package;
break;
case 'winter-module':
$packages['modules'][$package['name']] = $package;
break;
case 'winter-theme':
$packages['themes'][$package['name']] = $package;
break;
default:
break;
}
}
}
}
}
}

return $packages;
});
}

/**
* Finds all available plugins and loads them in to the $this->plugins array.
*/
Expand Down Expand Up @@ -180,6 +236,16 @@ public function loadPlugin(string $namespace, string $path): ?PluginBase
$this->plugins[$lowerClassId] = $pluginObj;
$this->normalizedMap[$lowerClassId] = $classId;

$pluginObj->setComposerPackage(Cache::rememberForever($lowerClassId . '.composerPackage', function () use ($path) {
foreach ($this->composerPackages['plugins'] ?? [] as $name => $package) {
if (($package['path'] ?? '') === $path) {
return $name;
}
}

return null;
}));

$replaces = $pluginObj->getReplaces();
if ($replaces) {
foreach ($replaces as $replace) {
Expand Down
53 changes: 53 additions & 0 deletions modules/system/classes/packager/Composer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace System\Classes\Packager;

use System\Classes\Packager\Commands\RemoveCommand;
use System\Classes\Packager\Commands\ShowCommand;
use System\Classes\Packager\Commands\UpdateCommand;
use System\Classes\Packager\Commands\RequireCommand;
use Winter\Packager\Composer as PackagerComposer;

/**
* @class Composer
* @method static i(): array
* @method static install(): array
* @method static search(string $query, ?string $type = null, bool $onlyNames = false, bool $onlyVendors = false): \Winter\Packager\Commands\Search
* @method static show(?string $mode = 'installed', string $package = null, bool $noDev = false, bool $path = false): object
* @method static update(bool $includeDev = true, bool $lockFileOnly = false, bool $ignorePlatformReqs = false, string $installPreference = 'none', bool $ignoreScripts = false, bool $dryRun = false, ?string $package = null): \Winter\Packager\Commands\Update
* @method static remove(?string $package = null, bool $dryRun = false): array
* @method static require(?string $package = null, bool $dryRun = false, bool $dev = false): array
* @method static version(string $detail = 'version'): array<string, string>|string
*/
class Composer
{
public const COMPOSER_CACHE_KEY = 'winter.system.composer';

protected static PackagerComposer $composer;

public static function make(bool $fresh = false): PackagerComposer
{
if (!$fresh && isset(static::$composer)) {
return static::$composer;
}

static::$composer = new PackagerComposer();
static::$composer->setWorkDir(base_path());

static::$composer->setCommand('show', new ShowCommand(static::$composer));
static::$composer->setCommand('update', new UpdateCommand(static::$composer));
static::$composer->setCommand('remove', new RemoveCommand(static::$composer));
static::$composer->setCommand('require', new RequireCommand(static::$composer));

return static::$composer;
}

public static function __callStatic(string $name, array $args = []): mixed
{
if (!isset(static::$composer)) {
static::make();
}

return static::$composer->{$name}(...$args);
}
}
70 changes: 70 additions & 0 deletions modules/system/classes/packager/commands/RemoveCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace System\Classes\Packager\Commands;

use Cache;
use System\Classes\Packager\ComposerFactory;
use Winter\Packager\Commands\BaseCommand;
use Winter\Packager\Exceptions\CommandException;

class RemoveCommand extends BaseCommand
{
protected ?string $package = null;
protected bool $dryRun = false;

/**
* Command handler.
*
* @param string|null $package
* @param boolean $dryRun
* @return void
* @throws CommandException
*/
public function handle(?string $package = null, bool $dryRun = false): void
{
if (!$package) {
throw new CommandException('Must provide a package');
}

$this->package = $package;
$this->dryRun = $dryRun;
}

/**
* @inheritDoc
*/
public function arguments(): array
{
$arguments = [];

if ($this->dryRun) {
$arguments['--dry-run'] = true;
}

$arguments['packages'] = [$this->package];

return $arguments;
}

public function execute()
{
$output = $this->runComposerCommand();
$message = implode(PHP_EOL, $output['output']);

if ($output['code'] !== 0) {
throw new CommandException($message);
}

Cache::forget(ComposerFactory::COMPOSER_CACHE_KEY);

return $message;
}

/**
* @inheritDoc
*/
public function getCommandName(): string
{
return 'remove';
}
}
77 changes: 77 additions & 0 deletions modules/system/classes/packager/commands/RequireCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace System\Classes\Packager\Commands;

use Cache;
use System\Classes\Packager\ComposerFactory;
use Winter\Packager\Commands\BaseCommand;
use Winter\Packager\Exceptions\CommandException;

class RequireCommand extends BaseCommand
{
protected ?string $package = null;
protected bool $dryRun = false;
protected bool $dev = false;

/**
* Command handler.
*
* @param string|null $package
* @param boolean $dryRun
* @param boolean $dev
* @return void
* @throws CommandException
*/
public function handle(?string $package = null, bool $dryRun = false, bool $dev = false): void
{
if (!$package) {
throw new CommandException('Must provide a package');
}

$this->package = $package;
$this->dryRun = $dryRun;
$this->dev = $dev;
}

/**
* @inheritDoc
*/
public function arguments(): array
{
$arguments = [];

if ($this->dryRun) {
$arguments['--dry-run'] = true;
}

if ($this->dev) {
$arguments['--dev'] = true;
}

$arguments['packages'] = [$this->package];

return $arguments;
}

public function execute()
{
$output = $this->runComposerCommand();
$message = implode(PHP_EOL, $output['output']);

if ($output['code'] !== 0) {
throw new CommandException($message);
}

Cache::forget(ComposerFactory::COMPOSER_CACHE_KEY);

return $message;
}

/**
* @inheritDoc
*/
public function getCommandName(): string
{
return 'require';
}
}
66 changes: 66 additions & 0 deletions modules/system/classes/packager/commands/ShowCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace System\Classes\Packager\Commands;

use Winter\Packager\Commands\Show;
use Winter\Packager\Exceptions\CommandException;

class ShowCommand extends Show
{
protected bool $path = false;

/**
* Command handler.
*
* The mode can be one of the following:
* - `installed`: Show installed packages
* - `locked`: Show locked packages
* - `platform`: Show platform requirements
* - `available`: Show all available packages
* - `self`: Show the current package
* - `path`: Show the package path
* - `tree`: Show packages in a dependency tree
* - `outdated`: Show only outdated packages
* - `direct`: Show only direct dependencies
*
* @param string|null $mode
* @param string|null $package
* @param boolean $noDev
* @param boolean $path
* @return void
*/
public function handle(?string $mode = 'installed', string $package = null, bool $noDev = false, bool $path = false): void
{
parent::handle($mode, $package, $noDev);

$this->path = $path;
}

/**
* @inheritDoc
*/
public function arguments(): array
{
$arguments = [];

if (!empty($this->package)) {
$arguments['package'] = $this->package;
}

if ($this->mode !== 'installed') {
$arguments['--' . $this->mode] = true;
}

if ($this->noDev) {
$arguments['--no-dev'] = true;
}

if ($this->path) {
$arguments['--path'] = true;
}

$arguments['--format'] = 'json';

return $arguments;
}
}
Loading

0 comments on commit 20269e5

Please sign in to comment.