Bug: beschermde/essentiële plugins (Dashboard, Navigation) konden niet meer geactiveerd worden als ze uit enabled_plugins raakten. Core forceert nu laden van essential plugins (plugin.json essential: true); admin-UI toont ze altijd als Actief; toggle-handler staat aanzetten wél toe, uitzetten blijft geblokkeerd; isProtectedPlugin() dekt nu ook essential.
360 lines
10 KiB
Markdown
360 lines
10 KiB
Markdown
# Plugin Development
|
|
|
|
## Plugin structure
|
|
|
|
Each plugin has its own folder under `plugins/`. The folder name must match the main plugin class name.
|
|
|
|
```
|
|
plugins/MyPlugin/
|
|
├── MyPlugin.php # Main plugin class (name = folder name)
|
|
├── plugin.json # Plugin metadata + settings schema
|
|
├── README.md # Plugin documentation
|
|
├── config.json # Optional runtime configuration (overrides defaults)
|
|
├── assets/ # Optional CSS/JS/SCSS
|
|
│ ├── css/
|
|
│ └── scss/
|
|
└── language/ # Translations
|
|
├── nl/
|
|
│ ├── admin.php # Admin labels (system plugins)
|
|
│ └── site.php # Front-end labels (content plugins)
|
|
└── en/
|
|
├── admin.php
|
|
└── site.php
|
|
```
|
|
|
|
## plugin.json
|
|
|
|
```json
|
|
{
|
|
"name": "My Plugin",
|
|
"version": "1.0.0",
|
|
"author": "Your Name",
|
|
"description": "Description",
|
|
"type": "content",
|
|
"essential": false,
|
|
"hasConfig": false,
|
|
"default_language": "nl",
|
|
"settings": []
|
|
}
|
|
```
|
|
|
|
### Fields
|
|
|
|
| Field | Value | Description |
|
|
|-------|-------|-------------|
|
|
| `name` | string | Display name in admin |
|
|
| `version` | string | Version number |
|
|
| `author` | string | Author |
|
|
| `description` | string | Short description |
|
|
| `type` | `"system"` or `"content"` | System (blue badge) or content (green badge) |
|
|
| `essential` | boolean | Essential plugins are always loaded (regardless of `enabled_plugins`) and cannot be disabled, edited or deleted |
|
|
| `hasConfig` | boolean | Shows a Config button in admin |
|
|
| `default_language` | string | Fallback language for plugin translations (e.g. `nl`) |
|
|
| `settings` | array | Settings schema (see below) |
|
|
|
|
## Settings schema
|
|
|
|
When `hasConfig: true`, define settings in the `settings` array:
|
|
|
|
```json
|
|
{
|
|
"settings": [
|
|
{
|
|
"key": "max_items",
|
|
"type": "number",
|
|
"default": 10,
|
|
"label_key": "setting_max_items",
|
|
"help_key": "setting_max_items_help"
|
|
},
|
|
{
|
|
"key": "required_roles",
|
|
"type": "multi-select",
|
|
"default": ["admin"],
|
|
"options": {
|
|
"admin": "Admin",
|
|
"content-manager": "Content Manager"
|
|
},
|
|
"label_key": "setting_required_roles",
|
|
"help_key": "setting_required_roles_help",
|
|
"option_label_key": "role_options"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Per-setting fields
|
|
|
|
| Field | Description |
|
|
|-------|-------------|
|
|
| `key` | Setting key (stored in `config.json`) |
|
|
| `type` | `text`, `checkbox`, `number`, `select`, `multi-select` |
|
|
| `default` | Default value |
|
|
| `label` | Hardcoded label (fallback when no `label_key` or when translation missing) |
|
|
| `help` | Hardcoded help text (fallback when no `help_key`) |
|
|
| `label_key` | Key in the plugin's `language/<lang>/admin.php` for a translated label |
|
|
| `help_key` | Key in the plugin's `language/<lang>/admin.php` for translated help text |
|
|
| `option_label_key` | Key pointing to an array of translated option labels for `select`/`multi-select` |
|
|
| `options` | Options for `select`/`multi-select` (`{value: label}`) |
|
|
|
|
The admin loads defaults from `plugin.json` and overrides them with values from `config.json`. The plugin reads the resolved values via `PluginManager::getPluginConfig()` or its own `getPluginConfig()` method.
|
|
|
|
## Plugin class example
|
|
|
|
```php
|
|
<?php
|
|
|
|
class MyPlugin
|
|
{
|
|
private ?PluginAPIInterface $api = null;
|
|
|
|
public function setAPI(PluginAPIInterface $api): void
|
|
{
|
|
$this->api = $api;
|
|
}
|
|
|
|
public function getConfig(): array
|
|
{
|
|
return [
|
|
'title' => 'My Plugin',
|
|
'type' => 'content',
|
|
'viewable' => true,
|
|
];
|
|
}
|
|
|
|
public function getSidebarContent(): string
|
|
{
|
|
// Fetch plugin translations (content plugin = front-end language)
|
|
$t = $this->api ? $this->api->getPluginTranslations('MyPlugin') : [];
|
|
$title = $this->api ? $this->api->getCurrentPageTitle() : '';
|
|
$label = $t['current_page'] ?? 'Current page';
|
|
return '<p>' . htmlspecialchars($label) . ': ' . htmlspecialchars($title) . '</p>';
|
|
}
|
|
}
|
|
```
|
|
|
|
The plugin class is automatically loaded by `PluginManager` when the plugin is listed in `enabled_plugins` in `config.json`.
|
|
|
|
## Using the API
|
|
|
|
The API is injected via `setAPI()`, not via a static method. In the front-end context this is a `CMSAPI` instance, in the admin context an `AdminPluginAPI` instance. Both implement `PluginAPIInterface`.
|
|
|
|
```php
|
|
// Front-end API (CMSAPI) - for content plugins
|
|
$this->api->getCurrentPageTitle();
|
|
$this->api->getMenu();
|
|
$this->api->getConfig('site_title');
|
|
$this->api->getCurrentLanguage();
|
|
$this->api->isHomepage();
|
|
$this->api->createUrl('about-us');
|
|
$this->api->getPluginTranslations('MyPlugin'); // front-end language
|
|
$this->api->t('current_page', 'MyPlugin'); // translation helper
|
|
|
|
// Admin API (AdminPluginAPI) - for system plugins
|
|
$this->api->getConfig('analytics.enabled');
|
|
$this->api->getContentDir();
|
|
$this->api->getEnabledPlugins();
|
|
$this->api->getAdminLanguage(); // active admin language
|
|
$this->api->getPluginTranslations('MyPlugin'); // admin language
|
|
$this->api->t('menu_label', 'MyPlugin'); // translation helper
|
|
```
|
|
|
|
## Language support (i18n)
|
|
|
|
Each plugin can provide translations in `language/<lang>/`. System plugins use `admin.php`, content plugins use `site.php`.
|
|
|
|
### Language file format
|
|
|
|
`language/en/admin.php`:
|
|
|
|
```php
|
|
<?php
|
|
return [
|
|
// Menu
|
|
'menu_label' => 'My Plugin',
|
|
|
|
// Page
|
|
'page_title' => 'My Plugin',
|
|
'current_page' => 'Current page',
|
|
|
|
// Settings (label_key/help_key point here)
|
|
'setting_max_items' => 'Maximum number of items',
|
|
'setting_max_items_help' => 'Number of items shown.',
|
|
'role_options' => [
|
|
'admin' => 'Admin',
|
|
'content-manager' => 'Content Manager',
|
|
],
|
|
];
|
|
```
|
|
|
|
### Fallback chain
|
|
|
|
Plugin translations are resolved in this order:
|
|
|
|
1. **Selected language** — `language/<selected>/admin.php` (or `site.php`)
|
|
2. **Plugin default_language** — `plugin.json` `default_language` (e.g. `nl`)
|
|
3. **CMS site default** — `config.language.default`
|
|
4. **Empty array** — the key is shown unchanged
|
|
|
|
### System vs content plugins
|
|
|
|
- **System plugins** (`type: "system"`) follow the selected **admin language** (`config.admin_language`). Translations live in `language/<lang>/admin.php`.
|
|
- **Content plugins** (`type: "content"`) follow the selected **content language** (from the URL or `config.language.default`). Translations live in `language/<lang>/site.php`.
|
|
|
|
### Fetching translations in a plugin
|
|
|
|
```php
|
|
// In handleAdminRoute() or getSidebarContent():
|
|
$t = $this->api->getPluginTranslations('MyPlugin');
|
|
$tr = function (string $key) use ($t): string {
|
|
return $t[$key] ?? $key;
|
|
};
|
|
|
|
echo htmlspecialchars($tr('page_title'));
|
|
```
|
|
|
|
Or use the single-key helper:
|
|
|
|
```php
|
|
echo htmlspecialchars($this->api->t('page_title', 'MyPlugin'));
|
|
```
|
|
|
|
### Translating the menu label
|
|
|
|
In `getAdminMenu()`, add `label_key`. The admin sidebar shows the translation via `plugin_menu_label()`:
|
|
|
|
```php
|
|
public function getAdminMenu(): array
|
|
{
|
|
return [
|
|
[
|
|
'plugin' => 'MyPlugin',
|
|
'route' => 'my-plugin',
|
|
'label' => 'My Plugin', // fallback
|
|
'label_key' => 'menu_label', // points to language/<lang>/admin.php
|
|
'icon' => 'bi-puzzle',
|
|
'section' => 'general', // or 'system'
|
|
'permission' => 'my-plugin',
|
|
],
|
|
];
|
|
}
|
|
```
|
|
|
|
### Translating setting labels
|
|
|
|
In `plugin.json` `settings`, use `label_key`/`help_key`/`option_label_key` instead of hardcoded `label`/`help`:
|
|
|
|
```json
|
|
{
|
|
"key": "max_items",
|
|
"label_key": "setting_max_items",
|
|
"help_key": "setting_max_items_help",
|
|
"type": "number",
|
|
"default": 10
|
|
}
|
|
```
|
|
|
|
The admin's `handlePluginsConfig()` resolves these via the plugin translations; if a translation is missing it falls back to `label`/`help` from `plugin.json`.
|
|
|
|
## Hooks
|
|
|
|
Plugins can implement the following methods for automatic hook registration:
|
|
|
|
**Actions** (no return value):
|
|
|
|
- `onPageLoad` - On page load
|
|
- `onBeforeRender` - Before rendering
|
|
- `onAfterRender` - After rendering
|
|
- `onSearch` - On search
|
|
- `onMenuBuild` - On menu build
|
|
|
|
**Filters** (return modified value):
|
|
|
|
- `onContentFilter` - Filter content
|
|
- `onTitleFilter` - Filter title
|
|
- `onMenuFilter` - Filter menu
|
|
|
|
## Admin integration
|
|
|
|
Plugins can add custom admin pages via `getAdminMenu()` and `handleAdminRoute()`:
|
|
|
|
```php
|
|
public function getAdminMenu(): array
|
|
{
|
|
$config = $this->getPluginConfig();
|
|
$requiredRoles = $config['required_roles'] ?? ['admin'];
|
|
|
|
return [
|
|
[
|
|
'plugin' => 'MyPlugin',
|
|
'route' => 'my-plugin',
|
|
'label' => 'My Plugin',
|
|
'label_key' => 'menu_label',
|
|
'icon' => 'bi-puzzle',
|
|
'section' => 'general', // or 'system'
|
|
'permission' => 'my-plugin',
|
|
'required_roles' => $requiredRoles,
|
|
],
|
|
];
|
|
}
|
|
|
|
public function handleAdminRoute(string $action): ?string
|
|
{
|
|
$t = $this->api ? $this->api->getPluginTranslations('MyPlugin') : [];
|
|
$tr = function (string $key) use ($t): string {
|
|
return $t[$key] ?? $key;
|
|
};
|
|
|
|
return '<h2>' . htmlspecialchars($tr('page_title')) . '</h2>';
|
|
}
|
|
```
|
|
|
|
Only plugins listed in `enabled_plugins` are shown in the admin sidebar.
|
|
|
|
### Runtime config in a plugin
|
|
|
|
Plugins can read their runtime config (defaults from `plugin.json` `settings` + overrides from `config.json`):
|
|
|
|
```php
|
|
private function getPluginConfig(): array
|
|
{
|
|
$pluginDir = dirname(__DIR__);
|
|
$pluginJsonFile = $pluginDir . '/plugin.json';
|
|
$configJsonFile = $pluginDir . '/config.json';
|
|
|
|
$defaults = [];
|
|
if (file_exists($pluginJsonFile)) {
|
|
$pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? [];
|
|
foreach ($pluginJson['settings'] ?? [] as $setting) {
|
|
if (isset($setting['key'])) {
|
|
$defaults[$setting['key']] = $setting['default'] ?? null;
|
|
}
|
|
}
|
|
}
|
|
|
|
$overrides = [];
|
|
if (file_exists($configJsonFile)) {
|
|
$overrides = json_decode(file_get_contents($configJsonFile), true) ?? [];
|
|
}
|
|
|
|
return array_merge($defaults, $overrides);
|
|
}
|
|
```
|
|
|
|
Or via the shared method on PluginManager:
|
|
|
|
```php
|
|
$config = $this->pluginManager->getPluginConfig('MyPlugin');
|
|
```
|
|
|
|
## Adding CSS
|
|
|
|
Plugins can provide a CSS URL via `getCssUrl()`:
|
|
|
|
```php
|
|
public function getCssUrl(): string
|
|
{
|
|
return '/plugins/MyPlugin/assets/css/style.css';
|
|
}
|
|
```
|
|
|
|
The URL is passed to the front-end template via `plugin_css_urls`. |