Files
E.Noorlander 6d5ca7cab4 v2.6.1d (Lyra): Plugin i18n, plugin editor vernieuwd, media invoegen
Plugin internationalisatie: plugins hebben eigen language/ mappen. Systeem
plugins volgen admin taal (admin.php), content plugins volgen content taal
(site.php). Fallback chain: geselecteerd -> plugin default_language -> CMS
default. PluginManager/AdminPluginAPI/CMSAPI uitgebreid met
getPluginTranslations()/t(). plugin.json settings ondersteunen
label_key/help_key/option_label_key.

Plugin uniformiteit: alle 6 plugins hebben uniforme structuur (README.md,
assets/.gitkeep, language/nl|en/). plugins/README.md herschreven.
guide plugin-development.md (NL+EN) volledig herschreven.

Plugin editor vernieuwd: geneste bestandsbrowser zijbalk, nieuw bestand
aanmaken, uploaden naar assets/, verwijderen en verplaatsen. Nieuwe routes:
plugins-file-upload, plugins-file-delete, plugins-file-move. Path-traversal
bescherming + protected plugins geblokkeerd.

Media invoegen in editor: nieuw /admin/media-list JSON endpoint + herbruikbare
_media-modal.twig include. Plugin-context scant assets/ map. editor-toolbar.js
modeMap uitgebreid voor css/scss/js/json.

Plugin overzicht knoppen: alleen iconen met title/aria-label.

Tests: pentest 30/30, WCAG 2.1 AA 25/25.
2026-08-18 13:49:52 +00:00

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 cannot be edited/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:

  1. Selected languagelanguage/<selected>/admin.php (or site.php)
  2. Plugin default_languageplugin.json default_language (e.g. nl)
  3. CMS site defaultconfig.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

// 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 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():

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.