Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
95 / 95
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
PluginsController
100.00% covered (success)
100.00%
95 / 95
100.00% covered (success)
100.00%
10 / 10
24
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 list
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 updateState
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
10
 getAllPlugins
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 registerPlugin
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 handleInstall
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 handleUninstall
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 handleUpdate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 handleEnable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 handleDisable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Controllers\Admin;
6
7use Engelsystem\Application;
8use Engelsystem\Database\Migration\Direction;
9use Engelsystem\Database\Migration\Migrate;
10use Engelsystem\Http\Exceptions\HttpNotFound;
11use Engelsystem\Http\Redirector;
12use Engelsystem\Http\Request;
13use Engelsystem\Http\Response;
14use Engelsystem\Models\Plugin;
15use Engelsystem\Plugins\Plugin as AbstractPlugin;
16use Engelsystem\Plugins\PluginServiceProvider;
17use Illuminate\Support\Collection;
18use Illuminate\Support\Str;
19use Psr\Log\LoggerInterface;
20
21class PluginsController extends BaseConfigController
22{
23    /** @var array<string> */
24    protected array $permissions = [
25        'plugin.edit',
26    ];
27
28    public function __construct(
29        protected Application $app,
30        protected LoggerInterface $log,
31        protected Migrate $migration,
32        protected Plugin $plugin,
33        protected Redirector $redirect,
34        protected Response $response,
35    ) {
36        parent::__construct();
37        $this->parseOptions();
38    }
39
40    public function list(): Response
41    {
42        $plugins = $this->getAllPlugins();
43
44        $installedPlugins = $this->plugin->all()->keyBy('name');
45
46        return $this->response->withView(
47            'admin/plugins.twig',
48            [
49                ...$this->getPageData('plugins'),
50                'plugins' => $plugins,
51                'installedPlugins' => $installedPlugins,
52            ]
53        );
54    }
55
56    public function updateState(Request $request): Response
57    {
58        $name = (string) $request->getAttribute('plugin');
59        $plugins = $this->getAllPlugins();
60        /** @var AbstractPlugin $plugin */
61        $plugin = $plugins->get($name);
62        if (!$plugin) {
63            throw new HttpNotFound();
64        }
65
66        $data = $this->validate($request, [
67            'action' => 'required|in:install,uninstall,update,enable,disable',
68        ]);
69        /** @var Plugin $model */
70        $model = $this->plugin->firstOrNew(
71            ['name' => $plugin->getPluginName()],
72            ['version' => $plugin->getVersion(), 'enabled' => true],
73        );
74        $baseDir = $plugin->getNamespacePath() . 'Migrations';
75        if (is_dir($baseDir)) {
76            $this->migration->setNamespace($plugin->getNamespace() . 'Migrations\\');
77            $this->migration->setOutput(function (string $message): void {
78                if (Str::startsWith($message, 'Skipping ')) {
79                    return;
80                }
81                $this->log->info($message);
82            });
83        } else {
84            $baseDir = null;
85        }
86
87        switch ($data['action']) {
88            case 'install':
89                $plugin = $this->registerPlugin($plugin);
90                $this->handleInstall($plugin, $model, $baseDir);
91                break;
92
93            case 'uninstall':
94                $this->handleUninstall($plugin, $model, $baseDir);
95                break;
96
97            case 'update':
98                $this->handleUpdate($plugin, $model, $baseDir);
99                break;
100
101            case 'enable':
102                $plugin = $this->registerPlugin($plugin);
103                $this->handleEnable($plugin, $model);
104                break;
105
106            case 'disable':
107                $this->handleDisable($plugin, $model);
108                break;
109        }
110
111        $routesCacheFile = $this->app->get('path.cache.routes');
112        if (file_exists($routesCacheFile)) {
113            unlink($routesCacheFile);
114        }
115
116        return $this->redirect->to('/admin/plugins');
117    }
118
119    protected function getAllPlugins(): Collection
120    {
121        $plugins = collect();
122        /** @var AbstractPlugin $plugin */
123        foreach ($this->app->tagged('plugin') as $plugin) {
124            $plugins[] = $plugin;
125        }
126        foreach ($this->app->tagged('plugin.disabled') as $plugin) {
127            $plugins[] = $plugin;
128        }
129        return $plugins
130            ->keyBy(fn(AbstractPlugin $p) => $p->getPluginName())
131            ->sortKeys();
132    }
133
134    protected function registerPlugin(AbstractPlugin $plugin): mixed
135    {
136        /** @var PluginServiceProvider $psp */
137        $psp = $this->app->get(PluginServiceProvider::class);
138        $psp->addPlugin($plugin->getPluginName(), $plugin->getPath(), true);
139        return $this->app->get($plugin->getPluginName());
140    }
141
142    protected function handleInstall(AbstractPlugin $plugin, Plugin $model, ?string $baseDir): void
143    {
144        if ($baseDir) {
145            $this->migration->run($baseDir, Direction::UP);
146        }
147
148        $model->save();
149
150        $plugin->install();
151
152        $this->log->info('Installed plugin "{name}"', ['name' => $plugin->getPluginName()]);
153        $this->addNotification('plugin.install.success');
154    }
155
156    protected function handleUninstall(AbstractPlugin $plugin, Plugin $model, ?string $baseDir): void
157    {
158        $plugin->uninstall();
159
160        if ($baseDir) {
161            $this->migration->run($baseDir, Direction::DOWN);
162        }
163
164        $model->delete();
165
166        $this->log->info('Uninstalled plugin "{name}"', ['name' => $plugin->getPluginName()]);
167        $this->addNotification('plugin.uninstall.success');
168    }
169
170    protected function handleUpdate(AbstractPlugin $plugin, Plugin $model, ?string $baseDir): void
171    {
172        if ($baseDir) {
173            $this->migration->run($baseDir, Direction::UP);
174        }
175
176        $plugin->update($model->version);
177
178        $model->version = $plugin->getVersion();
179        $model->save();
180
181        $this->log->info('Updated plugin "{name}"', ['name' => $plugin->getPluginName()]);
182        $this->addNotification('plugin.update.success');
183    }
184
185    protected function handleEnable(AbstractPlugin $plugin, Plugin $model): void
186    {
187        $model->enabled = true;
188        $model->save();
189
190        $plugin->enable();
191
192        $this->log->info('Enabled plugin "{name}"', ['name' => $plugin->getPluginName()]);
193        $this->addNotification('plugin.enable.success');
194    }
195
196    protected function handleDisable(AbstractPlugin $plugin, Plugin $model): void
197    {
198        $plugin->disable();
199
200        $model->enabled = false;
201        $model->save();
202
203        $this->log->info('Disabled plugin "{name}"', ['name' => $plugin->getPluginName()]);
204        $this->addNotification('plugin.disable.success');
205    }
206}