Skip to content

Commit 2606f40

Browse files
authored
Merge pull request #61 from trakli/feature/plugin-system
feat(plugins): Implement plugin system
2 parents 5b613eb + 4594cd2 commit 2606f40

20 files changed

+1456
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ yarn-error.log
1717
/.fleet
1818
/.idea
1919
/.vscode
20+
plugins
21+
!plugins/example
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
class DisableCommand extends PluginCommand
6+
{
7+
protected $signature = 'plugin:disable
8+
{id : The ID of the plugin to disable}';
9+
10+
protected $description = 'Disable a plugin';
11+
12+
public function handle()
13+
{
14+
try {
15+
$plugin = $this->resolvePlugin($this->argument('id'));
16+
$pluginId = $plugin['id'];
17+
18+
if (! $this->pluginManager->isPluginEnabled($pluginId)) {
19+
$this->info("Plugin [{$plugin['name']}] is already disabled.");
20+
21+
return 0;
22+
}
23+
24+
$this->pluginManager->disablePlugin($pluginId);
25+
$this->info("Plugin [{$plugin['name']}] disabled successfully.");
26+
27+
// Clear caches to ensure the plugin is immediately unavailable
28+
$this->call('config:clear');
29+
$this->call('route:clear');
30+
31+
return 0;
32+
} catch (\RuntimeException $e) {
33+
$this->error($e->getMessage());
34+
35+
return 1;
36+
}
37+
}
38+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
class DiscoverCommand extends PluginCommand
6+
{
7+
protected $signature = 'plugin:discover';
8+
9+
protected $description = 'Discover all available plugins';
10+
11+
public function handle()
12+
{
13+
$count = $this->pluginManager->discover()->count();
14+
$this->info("Discovered {$count} plugins.");
15+
16+
return $this->call('plugin:list');
17+
}
18+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
class EnableCommand extends PluginCommand
6+
{
7+
protected $signature = 'plugin:enable
8+
{id : The ID of the plugin to enable}';
9+
10+
protected $description = 'Enable a plugin';
11+
12+
public function handle()
13+
{
14+
try {
15+
$plugin = $this->resolvePlugin($this->argument('id'));
16+
$pluginId = $plugin['id'];
17+
18+
if ($this->pluginManager->isPluginEnabled($pluginId)) {
19+
$this->info("Plugin [{$plugin['name']}] is already enabled.");
20+
21+
return 0;
22+
}
23+
24+
$this->pluginManager->enablePlugin($pluginId);
25+
$this->info("Plugin [{$plugin['name']}] enabled successfully.");
26+
27+
// Clear caches to ensure the plugin is immediately available
28+
$this->call('config:clear');
29+
$this->call('route:clear');
30+
31+
return 0;
32+
} catch (\RuntimeException $e) {
33+
$this->error($e->getMessage());
34+
35+
return 1;
36+
}
37+
}
38+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
class InfoCommand extends PluginCommand
6+
{
7+
protected $signature = 'plugin:info
8+
{id : The ID of the plugin to show state for}';
9+
10+
protected $description = 'Show detailed information about a plugin';
11+
12+
public function handle()
13+
{
14+
try {
15+
$pluginId = $this->argument('id');
16+
$plugin = $this->resolvePlugin($pluginId);
17+
18+
$manifest = $plugin['manifest'] ?? [];
19+
20+
$this->info('<fg=cyan>Plugin State:</>');
21+
$this->line('"'.$plugin['name'].'" (ID: '.$plugin['id'].')');
22+
$this->line(str_repeat('-', 60));
23+
24+
// Basic Info
25+
$this->info("\n<fg=yellow>Basic Information:</>");
26+
$this->line('Name: '.($plugin['name'] ?? 'N/A'));
27+
$this->line('Version: '.($manifest['version'] ?? 'N/A'));
28+
$this->line('Namespace: '.$plugin['namespace']);
29+
$this->line('Path: '.$plugin['path']);
30+
$this->line('Status: '.($plugin['enabled'] ? '<fg=green>Enabled</>' : '<fg=red>Disabled</>'));
31+
32+
// Dependencies
33+
$this->info("\n<fg=yellow>Dependencies:</>");
34+
if (file_exists($plugin['path'].'/composer.json')) {
35+
$composerJson = json_decode(file_get_contents($plugin['path'].'/composer.json'), true);
36+
$this->line('Requires PHP: '.($composerJson['require']['php'] ?? 'Not specified'));
37+
38+
if (! empty($composerJson['require'])) {
39+
unset($composerJson['require']['php']);
40+
$this->line('Package Dependencies:');
41+
foreach ($composerJson['require'] as $pkg => $version) {
42+
$this->line(" - {$pkg}: {$version}");
43+
}
44+
} else {
45+
$this->line('No package dependencies');
46+
}
47+
} else {
48+
$this->line('No composer.json found');
49+
}
50+
51+
// Service Provider
52+
$this->info("\n<fg=yellow>Service Provider:</>");
53+
$this->line($plugin['provider']);
54+
55+
// Routes
56+
$this->info("\n<fg=yellow>Routes:</>");
57+
$routesPath = $plugin['path'].'/routes';
58+
if (is_dir($routesPath)) {
59+
$routeFiles = glob($routesPath.'/*.php');
60+
if (! empty($routeFiles)) {
61+
foreach ($routeFiles as $file) {
62+
$this->line('- '.basename($file).' ('.
63+
number_format(filesize($file)).' bytes)');
64+
}
65+
} else {
66+
$this->line('No route files found');
67+
}
68+
} else {
69+
$this->line('No routes directory found');
70+
}
71+
72+
// Configuration
73+
$this->info("\n<fg=yellow>Configuration:</>");
74+
$configPath = $plugin['path'].'/config';
75+
if (is_dir($configPath)) {
76+
$configFiles = glob($configPath.'/*.php');
77+
if (! empty($configFiles)) {
78+
foreach ($configFiles as $file) {
79+
$this->line('- '.basename($file).' ('.
80+
number_format(filesize($file)).' bytes)');
81+
}
82+
} else {
83+
$this->line('No configuration files found');
84+
}
85+
} else {
86+
$this->line('No config directory found');
87+
}
88+
89+
// Last modified
90+
$this->info("\n<fg=yellow>Last Modified:</>");
91+
$this->line('Manifest: '.date('Y-m-d H:i:s', filemtime($plugin['path'].'/plugin.json')));
92+
93+
return 0;
94+
} catch (\RuntimeException $e) {
95+
$this->error($e->getMessage());
96+
97+
return 1;
98+
}
99+
}
100+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
class InstallCommand extends PluginCommand
6+
{
7+
protected $signature = 'plugin:install
8+
{id : The ID of the plugin to install}
9+
{--no-deps : Skip installing dependencies}';
10+
11+
protected $description = 'Install a plugin and its dependencies';
12+
13+
public function handle()
14+
{
15+
try {
16+
$plugin = $this->resolvePlugin($this->argument('id'));
17+
$pluginId = $plugin['id'];
18+
19+
$this->info("Installing plugin: {$plugin['name']}");
20+
21+
if (file_exists("{$plugin['path']}/composer.json") && ! $this->option('no-deps')) {
22+
$this->info("Installing dependencies for plugin [{$plugin['name']}]...");
23+
24+
if ($this->pluginManager->installDependencies($pluginId)) {
25+
$this->info("Dependencies installed successfully for [{$plugin['name']}].");
26+
27+
// Enable the plugin after installation if not already enabled
28+
if (! $this->pluginManager->isPluginEnabled($pluginId)) {
29+
$this->info("Enabling plugin [{$plugin['name']}]...");
30+
$this->pluginManager->enablePlugin($pluginId);
31+
$this->info("Plugin [{$plugin['name']}] enabled successfully.");
32+
33+
// Clear caches to ensure the plugin is immediately available
34+
$this->call('config:clear');
35+
$this->call('route:clear');
36+
}
37+
38+
$this->info("Plugin [{$plugin['name']}] installed successfully.");
39+
40+
return 0;
41+
}
42+
43+
$this->error("Failed to install dependencies for [{$plugin['name']}].");
44+
45+
return 1;
46+
47+
} else {
48+
$this->info("Plugin [{$plugin['name']}] installed successfully.");
49+
50+
return 0;
51+
}
52+
53+
} catch (\RuntimeException $e) {
54+
$this->error($e->getMessage());
55+
56+
return 1;
57+
}
58+
}
59+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
class ListCommand extends PluginCommand
6+
{
7+
protected $signature = 'plugin:list';
8+
9+
protected $description = 'List all available plugins';
10+
11+
public function handle()
12+
{
13+
$plugins = $this->pluginManager->discover();
14+
15+
if ($plugins->isEmpty()) {
16+
$this->info('No plugins found.');
17+
18+
return 0;
19+
}
20+
21+
$rows = $plugins->map(function ($plugin) {
22+
return [
23+
'id' => $plugin['id'] ?? '',
24+
'name' => $plugin['name'],
25+
'version' => $plugin['manifest']['version'] ?? '1.0.0',
26+
'status' => $plugin['enabled'] ? '<fg=green>Enabled</>' : '<fg=red>Disabled</>',
27+
'description' => $plugin['manifest']['description'] ?? 'No description',
28+
];
29+
})->toArray();
30+
31+
$this->table(
32+
['ID', 'Name', 'Version', 'Status', 'Description'],
33+
$rows
34+
);
35+
36+
return 0;
37+
}
38+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
use App\Services\PluginManager;
6+
use Illuminate\Console\Command;
7+
8+
abstract class PluginCommand extends Command
9+
{
10+
protected PluginManager $pluginManager;
11+
12+
public function __construct(PluginManager $pluginManager)
13+
{
14+
parent::__construct();
15+
$this->pluginManager = $pluginManager;
16+
}
17+
18+
/**
19+
* Get a plugin by ID or short name
20+
*
21+
* @param string $pluginId Plugin ID or short name
22+
*
23+
* @throws \RuntimeException If plugin is not found or multiple plugins match
24+
*/
25+
protected function resolvePlugin(string $pluginId): array
26+
{
27+
$plugin = $this->pluginManager->findPlugin($pluginId);
28+
29+
if (! $plugin) {
30+
$suggestions = $this->getPluginSuggestions($pluginId);
31+
$message = "Plugin '{$pluginId}' not found.";
32+
33+
if (! empty($suggestions)) {
34+
$message .= " Did you mean one of these?\n - ".implode("\n - ", $suggestions);
35+
} else {
36+
$message .= ' Use `plugin:list` to see available plugins.';
37+
}
38+
39+
throw new \RuntimeException($message);
40+
}
41+
42+
return $plugin;
43+
}
44+
45+
/**
46+
* Get suggested plugin IDs based on input
47+
*/
48+
protected function getPluginSuggestions(string $input): array
49+
{
50+
$allPlugins = $this->pluginManager->discover();
51+
$input = strtolower($input);
52+
$suggestions = [];
53+
54+
foreach ($allPlugins as $plugin) {
55+
$id = strtolower($plugin['id']);
56+
if (str_contains($id, $input)) {
57+
$suggestions[] = $plugin['id'];
58+
}
59+
}
60+
61+
return array_slice($suggestions, 0, 5); // Return max 5 suggestions
62+
}
63+
}

0 commit comments

Comments
 (0)