diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-10-27 19:23:45 +0100 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-11-15 12:41:43 +0100 |
commit | a6e9c08499f9f79dad88cb3ae9eacda0e0c34c96 (patch) | |
tree | 41f70d7dc478e70a4a3ce4a578839316f5578765 /application | |
parent | 6f9e0609f4c118142504234ebcc7d93456b5e588 (diff) | |
download | Shaarli-a6e9c08499f9f79dad88cb3ae9eacda0e0c34c96.tar.gz Shaarli-a6e9c08499f9f79dad88cb3ae9eacda0e0c34c96.tar.zst Shaarli-a6e9c08499f9f79dad88cb3ae9eacda0e0c34c96.zip |
Plugin system: allow plugins to provide custom routes
- each route will be prefixed by `/plugin/<plugin_name>`
- add a new template for plugins rendering
- add a live example in the demo_plugin
Check out the "Plugin System" documentation for more detail.
Related to #143
Diffstat (limited to 'application')
-rw-r--r-- | application/container/ContainerBuilder.php | 17 | ||||
-rw-r--r-- | application/plugin/PluginManager.php | 64 | ||||
-rw-r--r-- | application/plugin/exception/PluginInvalidRouteException.php | 26 |
3 files changed, 96 insertions, 11 deletions
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index f0234eca..6d69a880 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php | |||
@@ -50,6 +50,9 @@ class ContainerBuilder | |||
50 | /** @var LoginManager */ | 50 | /** @var LoginManager */ |
51 | protected $login; | 51 | protected $login; |
52 | 52 | ||
53 | /** @var PluginManager */ | ||
54 | protected $pluginManager; | ||
55 | |||
53 | /** @var LoggerInterface */ | 56 | /** @var LoggerInterface */ |
54 | protected $logger; | 57 | protected $logger; |
55 | 58 | ||
@@ -61,12 +64,14 @@ class ContainerBuilder | |||
61 | SessionManager $session, | 64 | SessionManager $session, |
62 | CookieManager $cookieManager, | 65 | CookieManager $cookieManager, |
63 | LoginManager $login, | 66 | LoginManager $login, |
67 | PluginManager $pluginManager, | ||
64 | LoggerInterface $logger | 68 | LoggerInterface $logger |
65 | ) { | 69 | ) { |
66 | $this->conf = $conf; | 70 | $this->conf = $conf; |
67 | $this->session = $session; | 71 | $this->session = $session; |
68 | $this->login = $login; | 72 | $this->login = $login; |
69 | $this->cookieManager = $cookieManager; | 73 | $this->cookieManager = $cookieManager; |
74 | $this->pluginManager = $pluginManager; | ||
70 | $this->logger = $logger; | 75 | $this->logger = $logger; |
71 | } | 76 | } |
72 | 77 | ||
@@ -78,12 +83,10 @@ class ContainerBuilder | |||
78 | $container['sessionManager'] = $this->session; | 83 | $container['sessionManager'] = $this->session; |
79 | $container['cookieManager'] = $this->cookieManager; | 84 | $container['cookieManager'] = $this->cookieManager; |
80 | $container['loginManager'] = $this->login; | 85 | $container['loginManager'] = $this->login; |
86 | $container['pluginManager'] = $this->pluginManager; | ||
81 | $container['logger'] = $this->logger; | 87 | $container['logger'] = $this->logger; |
82 | $container['basePath'] = $this->basePath; | 88 | $container['basePath'] = $this->basePath; |
83 | 89 | ||
84 | $container['plugins'] = function (ShaarliContainer $container): PluginManager { | ||
85 | return new PluginManager($container->conf); | ||
86 | }; | ||
87 | 90 | ||
88 | $container['history'] = function (ShaarliContainer $container): History { | 91 | $container['history'] = function (ShaarliContainer $container): History { |
89 | return new History($container->conf->get('resource.history')); | 92 | return new History($container->conf->get('resource.history')); |
@@ -113,14 +116,6 @@ class ContainerBuilder | |||
113 | ); | 116 | ); |
114 | }; | 117 | }; |
115 | 118 | ||
116 | $container['pluginManager'] = function (ShaarliContainer $container): PluginManager { | ||
117 | $pluginManager = new PluginManager($container->conf); | ||
118 | |||
119 | $pluginManager->load($container->conf->get('general.enabled_plugins')); | ||
120 | |||
121 | return $pluginManager; | ||
122 | }; | ||
123 | |||
124 | $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { | 119 | $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { |
125 | return new FormatterFactory( | 120 | return new FormatterFactory( |
126 | $container->conf, | 121 | $container->conf, |
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index 3ea55728..7fc0cb04 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php | |||
@@ -4,6 +4,7 @@ namespace Shaarli\Plugin; | |||
4 | 4 | ||
5 | use Shaarli\Config\ConfigManager; | 5 | use Shaarli\Config\ConfigManager; |
6 | use Shaarli\Plugin\Exception\PluginFileNotFoundException; | 6 | use Shaarli\Plugin\Exception\PluginFileNotFoundException; |
7 | use Shaarli\Plugin\Exception\PluginInvalidRouteException; | ||
7 | 8 | ||
8 | /** | 9 | /** |
9 | * Class PluginManager | 10 | * Class PluginManager |
@@ -26,6 +27,14 @@ class PluginManager | |||
26 | */ | 27 | */ |
27 | private $loadedPlugins = []; | 28 | private $loadedPlugins = []; |
28 | 29 | ||
30 | /** @var array List of registered routes. Contains keys: | ||
31 | * - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE | ||
32 | * - `route` (path): without prefix, e.g. `/up/{variable}` | ||
33 | * It will be later prefixed by `/plugin/<plugin name>/`. | ||
34 | * - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`. | ||
35 | */ | ||
36 | protected $registeredRoutes = []; | ||
37 | |||
29 | /** | 38 | /** |
30 | * @var ConfigManager Configuration Manager instance. | 39 | * @var ConfigManager Configuration Manager instance. |
31 | */ | 40 | */ |
@@ -86,6 +95,9 @@ class PluginManager | |||
86 | $this->loadPlugin($dirs[$index], $plugin); | 95 | $this->loadPlugin($dirs[$index], $plugin); |
87 | } catch (PluginFileNotFoundException $e) { | 96 | } catch (PluginFileNotFoundException $e) { |
88 | error_log($e->getMessage()); | 97 | error_log($e->getMessage()); |
98 | } catch (\Throwable $e) { | ||
99 | $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage(); | ||
100 | $this->errors = array_unique(array_merge($this->errors, [$error])); | ||
89 | } | 101 | } |
90 | } | 102 | } |
91 | } | 103 | } |
@@ -166,6 +178,22 @@ class PluginManager | |||
166 | } | 178 | } |
167 | } | 179 | } |
168 | 180 | ||
181 | $registerRouteFunction = $pluginName . '_register_routes'; | ||
182 | $routes = null; | ||
183 | if (function_exists($registerRouteFunction)) { | ||
184 | $routes = call_user_func($registerRouteFunction); | ||
185 | } | ||
186 | |||
187 | if ($routes !== null) { | ||
188 | foreach ($routes as $route) { | ||
189 | if (static::validateRouteRegistration($route)) { | ||
190 | $this->registeredRoutes[$pluginName][] = $route; | ||
191 | } else { | ||
192 | throw new PluginInvalidRouteException($pluginName); | ||
193 | } | ||
194 | } | ||
195 | } | ||
196 | |||
169 | $this->loadedPlugins[] = $pluginName; | 197 | $this->loadedPlugins[] = $pluginName; |
170 | } | 198 | } |
171 | 199 | ||
@@ -238,6 +266,14 @@ class PluginManager | |||
238 | } | 266 | } |
239 | 267 | ||
240 | /** | 268 | /** |
269 | * @return array List of registered custom routes by plugins. | ||
270 | */ | ||
271 | public function getRegisteredRoutes(): array | ||
272 | { | ||
273 | return $this->registeredRoutes; | ||
274 | } | ||
275 | |||
276 | /** | ||
241 | * Return the list of encountered errors. | 277 | * Return the list of encountered errors. |
242 | * | 278 | * |
243 | * @return array List of errors (empty array if none exists). | 279 | * @return array List of errors (empty array if none exists). |
@@ -246,4 +282,32 @@ class PluginManager | |||
246 | { | 282 | { |
247 | return $this->errors; | 283 | return $this->errors; |
248 | } | 284 | } |
285 | |||
286 | /** | ||
287 | * Checks whether provided input is valid to register a new route. | ||
288 | * It must contain keys `method`, `route`, `callable` (all strings). | ||
289 | * | ||
290 | * @param string[] $input | ||
291 | * | ||
292 | * @return bool | ||
293 | */ | ||
294 | protected static function validateRouteRegistration(array $input): bool | ||
295 | { | ||
296 | if ( | ||
297 | !array_key_exists('method', $input) | ||
298 | || !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) | ||
299 | ) { | ||
300 | return false; | ||
301 | } | ||
302 | |||
303 | if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) { | ||
304 | return false; | ||
305 | } | ||
306 | |||
307 | if (!array_key_exists('callable', $input)) { | ||
308 | return false; | ||
309 | } | ||
310 | |||
311 | return true; | ||
312 | } | ||
249 | } | 313 | } |
diff --git a/application/plugin/exception/PluginInvalidRouteException.php b/application/plugin/exception/PluginInvalidRouteException.php new file mode 100644 index 00000000..6ba9bc43 --- /dev/null +++ b/application/plugin/exception/PluginInvalidRouteException.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Plugin\Exception; | ||
6 | |||
7 | use Exception; | ||
8 | |||
9 | /** | ||
10 | * Class PluginFileNotFoundException | ||
11 | * | ||
12 | * Raise when plugin files can't be found. | ||
13 | */ | ||
14 | class PluginInvalidRouteException extends Exception | ||
15 | { | ||
16 | /** | ||
17 | * Construct exception with plugin name. | ||
18 | * Generate message. | ||
19 | * | ||
20 | * @param string $pluginName name of the plugin not found | ||
21 | */ | ||
22 | public function __construct() | ||
23 | { | ||
24 | $this->message = 'trying to register invalid route.'; | ||
25 | } | ||
26 | } | ||