Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Plugin management Composer support #967

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ composer.lock
.env.php
selenium.php
/bootstrap/compiled.php
/storage/framework/packages.json
.phpunit.result.cache

# Hosting ignores
Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
"winter/wn-backend-module": "dev-develop",
"winter/wn-cms-module": "dev-develop",
"laravel/framework": "^9.1",
"wikimedia/composer-merge-plugin": "~2.1.0"
"winter/packager": "*",
"wikimedia/composer-merge-plugin": "~2.1.0",
"jaxwilko/composer-winter-plugin": "dev-main"
},
"require-dev": {
"phpunit/phpunit": "^9.5.8",
Expand Down Expand Up @@ -82,7 +84,8 @@
"config": {
"allow-plugins": {
"composer/installers": true,
"wikimedia/composer-merge-plugin": true
"wikimedia/composer-merge-plugin": true,
"jaxwilko/composer-winter-plugin": true
}
}
}
29 changes: 29 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 ?array The composer package details for this plugin.
*/
protected ?array $composerPackage = null;

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

/**
* Set the composer package property for the plugin
*/
public function setComposerPackage(?array $package): void
{
$this->composerPackage = $package;
}

/**
* Get the composer package details
*/
public function getComposerPackage(): ?array
{
return $this->composerPackage;
}

/**
* Get the composer package name
*/
public function getComposerPackageName(): ?string
{
return $this->composerPackage['name'] ?? null;
}

/**
* Verifies the plugin's dependencies are present and enabled
*/
Expand Down
52 changes: 52 additions & 0 deletions modules/system/classes/PluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
use Cache;
use Config;
use Schema;
use Storage;
use SystemException;
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 +63,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 +115,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 +129,35 @@ protected function init(): void
$this->registerPluginReplacements();
}

public function loadComposer(): array
{
return $this->composerPackages = Cache::rememberForever(Composer::COMPOSER_CACHE_KEY, function () {
$packageFile = Storage::path('../framework/packages.json');
if (!is_file($packageFile)) {
return [];
}
$packages = [];
$installed = json_decode(file_get_contents($packageFile), JSON_OBJECT_AS_ARRAY);
foreach ($installed as $package) {
switch ($package['type']) {
case 'winter-plugin':
$packages['plugins'][$package['path']] = $package;
break;
case 'winter-module':
$packages['modules'][$package['path']] = $package;
break;
case 'winter-theme':
$packages['themes'][$package['path']] = $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 +219,8 @@ public function loadPlugin(string $namespace, string $path): ?PluginBase
$this->plugins[$lowerClassId] = $pluginObj;
$this->normalizedMap[$lowerClassId] = $classId;

$pluginObj->setComposerPackage($this->composerPackages['plugins'][$path] ?? null);

$replaces = $pluginObj->getReplaces();
if ($replaces) {
foreach ($replaces as $replace) {
Expand Down Expand Up @@ -471,6 +512,17 @@ public function findByIdentifier(PluginBase|string $identifier, bool $ignoreRepl
return $this->plugins[$identifier] ?? null;
}

public function findByComposerPackage(string $identifier): ?PluginBase
{
foreach ($this->getAllPlugins() as $plugin) {
if ($plugin->getComposerPackageName() === $identifier) {
return $plugin;
}
}

return null;
}

/**
* Checks to see if a plugin has been registered.
*/
Expand Down
55 changes: 55 additions & 0 deletions modules/system/classes/packager/Composer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace System\Classes\Packager;

use System\Classes\Packager\Commands\RemoveCommand;
use System\Classes\Packager\Commands\SearchCommand;
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('remove', new RemoveCommand(static::$composer));
static::$composer->setCommand('require', new RequireCommand(static::$composer));
static::$composer->setCommand('search', new SearchCommand(static::$composer));
static::$composer->setCommand('show', new ShowCommand(static::$composer));
static::$composer->setCommand('update', new UpdateCommand(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\Composer;
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(Composer::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\Composer;
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(Composer::COMPOSER_CACHE_KEY);

return $message;
}

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

namespace System\Classes\Packager\Commands;

use Cache;
use Winter\Packager\Commands\Search;
use Winter\Packager\Exceptions\CommandException;

class SearchCommand extends Search
{
public function execute()
{
$output = $this->runComposerCommand();

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

$this->results = json_decode(implode(PHP_EOL, $output['output']), true) ?? [];

return $this;
}
}
Loading