# 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//admin.php` for a translated label | | `help_key` | Key in the plugin's `language//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 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 '

' . htmlspecialchars($label) . ': ' . htmlspecialchars($title) . '

'; } } ``` 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//`. System plugins use `admin.php`, content plugins use `site.php`. ### Language file format `language/en/admin.php`: ```php '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//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//admin.php`. - **Content plugins** (`type: "content"`) follow the selected **content language** (from the URL or `config.language.default`). Translations live in `language//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//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 '

' . htmlspecialchars($tr('page_title')) . '

'; } ``` 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`.