Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
104 / 104
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
PluginServiceProvider
100.00% covered (success)
100.00%
104 / 104
100.00% covered (success)
100.00%
10 / 10
37
100.00% covered (success)
100.00%
1 / 1
 register
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 boot
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addPlugin
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
8
 getNamespace
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 registerNamespaces
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 registerPluginInstance
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 registerPluginProviders
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 registerPluginMiddleware
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 registerPluginEventHandlers
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 registerPluginConfigOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Plugins;
6
7use DirectoryIterator;
8use Engelsystem\Config\Config;
9use Engelsystem\Container\ServiceProvider;
10use Engelsystem\Events\EventDispatcher;
11use Engelsystem\Models\Plugin as PluginModel;
12use Illuminate\Database\QueryException;
13use Illuminate\Support\Str;
14
15class PluginServiceProvider extends ServiceProvider
16{
17    public function register(): void
18    {
19        $pluginPath = $this->app->get('path.plugins');
20        try {
21            $enabledPlugins = PluginModel::whereEnabled(true)->get();
22        } catch (QueryException) {
23            return;
24        }
25        /** @var DirectoryIterator $fileInfo */
26        foreach (new DirectoryIterator($pluginPath) as $fileInfo) {
27            if ($fileInfo->isDot() || !$fileInfo->isDir()) {
28                continue;
29            }
30
31            $pluginName = $fileInfo->getFilename();
32            $pluginPath = $fileInfo->getPath() . '/' . $pluginName . '/';
33            $isEnabled = $enabledPlugins->filter(fn(PluginModel $p) => $p->name === $pluginName)->isNotEmpty();
34
35            $this->addPlugin($pluginName, $pluginPath, $isEnabled);
36        }
37
38        /** @var Config $config */
39        $config = $this->app->get('config');
40        $config->set('config_options', [
41            ...$config->get('config_options', []),
42            'plugins' => [
43                'title' => 'plugin.admin',
44                'icon' => 'puzzle-fill',
45                'order' => 80,
46                'url' => '/admin/plugins',
47                'permission' => ['plugin.edit'],
48                'config' => [],
49            ],
50        ]);
51    }
52
53    public function boot(): void
54    {
55        /** @var Plugin $plugin */
56        foreach ($this->app->tagged('plugin') as $plugin) {
57            foreach ($plugin->getProviders() as $providerName) {
58                /** @var ServiceProvider $provider */
59                $provider = $this->app->get($providerName);
60                $provider->boot();
61            }
62
63            $plugin->boot();
64        }
65    }
66
67    public function addPlugin(string $pluginName, string $pluginPath, bool $isEnabled): void
68    {
69        $composerFile = $pluginPath . 'composer.json';
70
71        if (!is_readable($composerFile)) {
72            return;
73        }
74
75        $pluginInfo = json_decode(file_get_contents($composerFile), true);
76        if (!$pluginInfo || empty($pluginInfo['name']) || ($pluginInfo['type'] ?? null) !== 'engelsystem-plugin') {
77            return;
78        }
79
80        list($pluginNamespace, $pluginNamespacePath) = $this->getNamespace(
81            $pluginInfo['autoload'] ?? [],
82            $pluginPath
83        );
84        if (!$pluginNamespace) {
85            $pluginNamespace = $pluginName . '\\';
86            $pluginNamespacePath = $pluginPath;
87        }
88
89        $pluginInfo['plugin_name'] = $pluginName;
90        $pluginInfo['path'] = $pluginPath;
91        $pluginInfo['namespace'] = $pluginNamespace;
92        $pluginInfo['namespace_path'] = $pluginNamespacePath;
93        if ($isEnabled) {
94            $this->registerNamespaces($pluginInfo['autoload'] ?? [], $pluginPath);
95        }
96
97        $plugin = $this->registerPluginInstance($pluginNamespace, $pluginName, $pluginInfo, $pluginPath, $isEnabled);
98
99        if ($isEnabled) {
100            $this->registerPluginProviders($plugin);
101            $this->registerPluginMiddleware($plugin);
102            $this->registerPluginEventHandlers($plugin);
103            $this->registerPluginConfigOptions($plugin);
104        }
105    }
106
107    protected function getNamespace(array $autoload, string $pluginPath): array
108    {
109        foreach ($autoload['psr-4'] ?? [] as $namespace => $path) {
110            $namespace = rtrim($namespace, '\\') . '\\';
111            $path = rtrim($pluginPath . $path, '/') . '/';
112            return [$namespace, $path];
113        }
114
115        return [null, null];
116    }
117
118    protected function registerNamespaces(array $autoload, string $pluginPath): void
119    {
120        foreach ($autoload['psr-4'] ?? [] as $namespace => $path) {
121            $namespace = rtrim($namespace, '\\') . '\\';
122            $path = $pluginPath . rtrim($path, '/') . '/';
123
124            spl_autoload_register(function (string $className) use ($namespace, $path): void {
125                if (!Str::startsWith($className, $namespace)) {
126                    return;
127                }
128
129                $className = Str::substr($className, Str::length($namespace));
130                $file = $path . str_replace('\\', '/', $className) . '.php';
131                if (!file_exists($file)) {
132                    return;
133                }
134
135                require_once $file;
136            });
137        }
138    }
139
140    protected function registerPluginInstance(
141        string $pluginNamespace,
142        string $pluginName,
143        array $pluginInfo,
144        string $pluginPath,
145        bool $isEnabled
146    ): Plugin {
147        $namespacedPlugin = $pluginNamespace . $pluginName;
148        if ($this->app->bound($namespacedPlugin)) {
149            // This is a rebind, reset previous singleton instance
150            $this->app->singleton($namespacedPlugin);
151        }
152
153        if (!$isEnabled) {
154            $this->app->alias(DisabledPlugin::class, $namespacedPlugin);
155        }
156
157        $plugin = $this->app->make(
158            class_exists($namespacedPlugin) ? $namespacedPlugin : Plugin::class,
159            ['pluginInfo' => $pluginInfo],
160        );
161
162        // Set and tag plugin instance
163        $this->app->singleton($namespacedPlugin, fn() => $plugin);
164        $this->app->alias($namespacedPlugin, $pluginName);
165        $this->app->tag($pluginName, $isEnabled ? 'plugin' : 'plugin.disabled');
166
167        // Set plugin path to be available by tag
168        $this->app->instance($pluginPath, $pluginPath);
169        $this->app->tag($pluginPath, $isEnabled ? 'plugin.path' : 'plugin.path.disabled');
170
171        return $plugin;
172    }
173
174    protected function registerPluginProviders(Plugin $plugin): void
175    {
176        /** @var Config $config */
177        $config = $this->app->get('config');
178        $providers = $config->get('providers');
179        foreach ($plugin->getProviders() as $providerName) {
180            $providers[] = $providerName;
181            $this->app->singleton($providerName);
182            /** @var ServiceProvider $provider */
183            $provider = $this->app->make($providerName);
184            $provider->register();
185        }
186        $config->set('providers', $providers);
187    }
188
189    protected function registerPluginMiddleware(Plugin $plugin): void
190    {
191        foreach ($plugin->getMiddleware() as $middleware) {
192            $this->app->tag($middleware, 'plugin.middleware');
193        }
194    }
195
196    protected function registerPluginEventHandlers(Plugin $plugin): void
197    {
198        /** @var EventDispatcher $dispatcher */
199        $dispatcher = $this->app->get(EventDispatcher::class);
200        foreach ($plugin->getEventHandlers() as $event => $handlers) {
201            foreach ((array) $handlers as $handler) {
202                $dispatcher->listen($event, $handler);
203            }
204        }
205    }
206
207    protected function registerPluginConfigOptions(Plugin $plugin): void
208    {
209        /** @var Config $config */
210        $config = $this->app->get('config');
211        $configOptions = $config->get('config_options');
212        foreach ($plugin->getConfigOptions() as $type => $options) {
213            $configOptions[$type] = array_merge_recursive($configOptions[$type] ?? [], $options);
214        }
215        $config->set('config_options', $configOptions);
216    }
217}