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.
10 KiB
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
{
"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:
{
"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
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.
// 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
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:
- Selected language —
language/<selected>/admin.php(orsite.php) - Plugin default_language —
plugin.jsondefault_language(e.g.nl) - CMS site default —
config.language.default - 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 inlanguage/<lang>/admin.php. - Content plugins (
type: "content") follow the selected content language (from the URL orconfig.language.default). Translations live inlanguage/<lang>/site.php.
Fetching translations in a plugin
// 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:
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():
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:
{
"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 loadonBeforeRender- Before renderingonAfterRender- After renderingonSearch- On searchonMenuBuild- On menu build
Filters (return modified value):
onContentFilter- Filter contentonTitleFilter- Filter titleonMenuFilter- Filter menu
Admin integration
Plugins can add custom admin pages via getAdminMenu() and handleAdminRoute():
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):
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:
$config = $this->pluginManager->getPluginConfig('MyPlugin');
Adding CSS
Plugins can provide a CSS URL via getCssUrl():
public function getCssUrl(): string
{
return '/plugins/MyPlugin/assets/css/style.css';
}
The URL is passed to the front-end template via plugin_css_urls.