Skip to content

Commit 242a75b

Browse files
authored
Plugin system (#1866)
1 parent 2ab4c81 commit 242a75b

33 files changed

+1875
-11
lines changed

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,14 @@ RUN chown root:www-data ./ \
7676
# Files should not have execute set, but directories need it
7777
&& find ./ -type d -exec chmod 750 {} \; \
7878
# Create necessary directories
79-
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
80-
# Symlinks for env, database, and avatars
79+
&& mkdir -p /pelican-data/storage /pelican-data/plugins /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
80+
# Symlinks for env, database, storage, and plugins
8181
&& ln -s /pelican-data/.env ./.env \
8282
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
8383
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
8484
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
8585
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
86+
&& ln -s /pelican-data/plugins /var/www/html/plugins \
8687
# Allow www-data write permissions where necessary
8788
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
8889
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \

Dockerfile.dev

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,14 @@ RUN chown root:www-data ./ \
8080
# Files should not have execute set, but directories need it
8181
&& find ./ -type d -exec chmod 750 {} \; \
8282
# Create necessary directories
83-
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
84-
# Symlinks for env, database, and avatars
83+
&& mkdir -p /pelican-data/storage /pelican-data/plugins /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
84+
# Symlinks for env, database, storage, and plugins
8585
&& ln -s /pelican-data/.env ./.env \
8686
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
8787
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
8888
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
8989
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
90+
&& ln -s /pelican-data/plugins /var/www/html/plugins \
9091
# Allow www-data write permissions where necessary
9192
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
9293
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
use App\Services\Helpers\PluginService;
6+
use Exception;
7+
use Illuminate\Console\Command;
8+
9+
class ComposerPluginsCommand extends Command
10+
{
11+
protected $signature = 'p:plugin:composer';
12+
13+
protected $description = 'Makes sure the needed composer packages for all installed plugins are available.';
14+
15+
public function handle(PluginService $pluginService): void
16+
{
17+
try {
18+
$pluginService->manageComposerPackages();
19+
} catch (Exception $exception) {
20+
report($exception);
21+
22+
$this->error($exception->getMessage());
23+
}
24+
}
25+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
use App\Models\Plugin;
6+
use App\Services\Helpers\PluginService;
7+
use Illuminate\Console\Command;
8+
9+
class DisablePluginCommand extends Command
10+
{
11+
protected $signature = 'p:plugin:disable {id?}';
12+
13+
protected $description = 'Disables a plugin';
14+
15+
public function handle(PluginService $pluginService): void
16+
{
17+
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
18+
19+
$plugin = Plugin::find($id);
20+
21+
if (!$plugin) {
22+
$this->error('Plugin does not exist!');
23+
24+
return;
25+
}
26+
27+
if (!$plugin->canDisable()) {
28+
$this->error("Plugin can't be disabled!");
29+
30+
return;
31+
}
32+
33+
$pluginService->disablePlugin($plugin);
34+
35+
$this->info('Plugin disabled.');
36+
}
37+
}
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+
use App\Enums\PluginStatus;
6+
use App\Models\Plugin;
7+
use App\Services\Helpers\PluginService;
8+
use Illuminate\Console\Command;
9+
10+
class InstallPluginCommand extends Command
11+
{
12+
protected $signature = 'p:plugin:install {id?}';
13+
14+
protected $description = 'Installs a plugin';
15+
16+
public function handle(PluginService $pluginService): void
17+
{
18+
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
19+
20+
$plugin = Plugin::find($id);
21+
22+
if (!$plugin) {
23+
$this->error('Plugin does not exist!');
24+
25+
return;
26+
}
27+
28+
if ($plugin->status !== PluginStatus::NotInstalled) {
29+
$this->error('Plugin is already installed!');
30+
31+
return;
32+
}
33+
34+
$pluginService->installPlugin($plugin);
35+
36+
$this->info('Plugin installed and enabled.');
37+
}
38+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
use App\Models\Plugin;
6+
use Illuminate\Console\Command;
7+
8+
class ListPluginsCommand extends Command
9+
{
10+
protected $signature = 'p:plugin:list';
11+
12+
protected $description = 'List all installed plugins';
13+
14+
public function handle(): void
15+
{
16+
$plugins = Plugin::query()->get(['name', 'author', 'status', 'version', 'panels', 'category']);
17+
18+
if (count($plugins) < 1) {
19+
$this->warn('No plugins installed');
20+
21+
return;
22+
}
23+
24+
$this->table(['Name', 'Author', 'Status', 'Version', 'Panels', 'Category'], $plugins->toArray());
25+
26+
$this->output->newLine();
27+
}
28+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Plugin;
4+
5+
use App\Enums\PluginCategory;
6+
use App\Enums\PluginStatus;
7+
use Illuminate\Console\Command;
8+
use Illuminate\Filesystem\Filesystem;
9+
use Illuminate\Support\Str;
10+
11+
class MakePluginCommand extends Command
12+
{
13+
protected $signature = 'p:plugin:make
14+
{--name=}
15+
{--author=}
16+
{--description=}
17+
{--category=}
18+
{--url=}
19+
{--updateUrl=}
20+
{--panels=}
21+
{--panelVersion=}';
22+
23+
protected $description = 'Create a new plugin';
24+
25+
public function __construct(private Filesystem $filesystem)
26+
{
27+
parent::__construct();
28+
}
29+
30+
public function handle(): void
31+
{
32+
$name = $this->option('name') ?? $this->ask('Name');
33+
$name = preg_replace('/[^A-Za-z0-9 ]/', '', Str::ascii($name));
34+
35+
$id = Str::slug($name);
36+
37+
if ($this->filesystem->exists(plugin_path($id))) {
38+
$this->error('Plugin with that name already exists!');
39+
40+
return;
41+
}
42+
43+
$author = $this->option('author') ?? $this->ask('Author', cache('plugin.author'));
44+
$author = preg_replace('/[^A-Za-z0-9 ]/', '', Str::ascii($author));
45+
cache()->forever('plugin.author', $author);
46+
47+
$namespace = Str::studly($author) . '\\' . Str::studly($name);
48+
$class = Str::studly($name . 'Plugin');
49+
50+
if (class_exists('\\' . $namespace . '\\' . $class)) {
51+
$this->error('Plugin class with that name already exists!');
52+
53+
return;
54+
}
55+
56+
$this->info('Creating Plugin "' . $name . '" (' . $id . ') by ' . $author);
57+
58+
$description = $this->option('description') ?? $this->ask('Description (can be empty)');
59+
60+
$category = $this->option('category') ?? $this->choice('Category', collect(PluginCategory::cases())->mapWithKeys(fn (PluginCategory $category) => [$category->value => $category->getLabel()])->toArray(), PluginCategory::Plugin->value);
61+
62+
if (!PluginCategory::tryFrom($category)) {
63+
$this->error('Unknown plugin category!');
64+
65+
return;
66+
}
67+
68+
$url = $this->option('url') ?? $this->ask('URL (can be empty)');
69+
$updateUrl = $this->option('updateUrl') ?? $this->ask('Update URL (can be empty)');
70+
71+
$panels = $this->option('panels');
72+
if (!$panels) {
73+
if ($this->confirm('Should the plugin be available on all panels?', true)) {
74+
$panels = null;
75+
} else {
76+
$panels = $this->choice('Panels (comma separated list)', [
77+
'admin' => 'Admin Area',
78+
'server' => 'Client Area',
79+
'app' => 'Server List',
80+
], multiple: true);
81+
}
82+
}
83+
$panels = is_string($panels) ? explode(',', $panels) : $panels;
84+
85+
$panelVersion = $this->option('panelVersion');
86+
if (!$panelVersion) {
87+
$panelVersion = $this->ask('Required panel version (leave empty for no constraint)', config('app.version') === 'canary' ? null : config('app.version'));
88+
89+
if ($panelVersion && $this->confirm("Should the version constraint be minimal instead of strict? ($panelVersion or higher instead of only $panelVersion)")) {
90+
$panelVersion = "^$panelVersion";
91+
}
92+
}
93+
94+
$composerPackages = null;
95+
// TODO: ask for composer packages?
96+
97+
// Create base directory
98+
$this->filesystem->makeDirectory(plugin_path($id));
99+
100+
// Write plugin.json
101+
$this->filesystem->put(plugin_path($id, 'plugin.json'), json_encode([
102+
'id' => $id,
103+
'name' => $name,
104+
'author' => $author,
105+
'version' => '1.0.0',
106+
'description' => $description,
107+
'category' => $category,
108+
'url' => $url,
109+
'update_url' => $updateUrl,
110+
'namespace' => $namespace,
111+
'class' => $class,
112+
'panels' => $panels,
113+
'panel_version' => $panelVersion,
114+
'composer_packages' => $composerPackages,
115+
'meta' => [
116+
'status' => PluginStatus::Enabled,
117+
'status_message' => null,
118+
],
119+
], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
120+
121+
// Create src directory and create main class
122+
$this->filesystem->makeDirectory(plugin_path($id, 'src'));
123+
$this->filesystem->put(plugin_path($id, 'src', $class . '.php'), Str::replace(['$namespace$', '$class$', '$id$'], [$namespace, $class, $id], file_get_contents(__DIR__ . '/Plugin.stub')));
124+
125+
// Create Providers directory and create service provider
126+
$this->filesystem->makeDirectory(plugin_path($id, 'src', 'Providers'));
127+
$this->filesystem->put(plugin_path($id, 'src', 'Providers', $class . 'Provider.php'), Str::replace(['$namespace$', '$class$'], [$namespace, $class], file_get_contents(__DIR__ . '/PluginProvider.stub')));
128+
129+
// Create config directory and create config file
130+
$this->filesystem->makeDirectory(plugin_path($id, 'config'));
131+
$this->filesystem->put(plugin_path($id, 'config', $id . '.php'), Str::replace(['$name$'], [$name], file_get_contents(__DIR__ . '/PluginConfig.stub')));
132+
133+
$this->info('Plugin created.');
134+
}
135+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace $namespace$;
4+
5+
use Filament\Contracts\Plugin;
6+
use Filament\Panel;
7+
8+
class $class$ implements Plugin
9+
{
10+
public function getId(): string
11+
{
12+
return '$id$';
13+
}
14+
15+
public function register(Panel $panel): void
16+
{
17+
// Allows you to use any configuration option that is available to the panel.
18+
// This includes registering resources, custom pages, themes, render hooks and more.
19+
}
20+
21+
public function boot(Panel $panel): void
22+
{
23+
// Is run only when the panel that the plugin is being registered to is actually in-use. It is executed by a middleware class.
24+
}
25+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
return [
4+
// Config values for $name$
5+
];
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace $namespace$\Providers;
4+
5+
use Illuminate\Support\ServiceProvider;
6+
7+
class $class$Provider extends ServiceProvider
8+
{
9+
public function register(): void
10+
{
11+
//
12+
}
13+
14+
public function boot(): void
15+
{
16+
//
17+
}
18+
}

0 commit comments

Comments
 (0)