Plugin-Anatomie
Diese Seite erklaert den Aufbau eines Plugins im Detail: welche Ordner es gibt, welche Pflicht-Methoden implementiert werden muessen und wie die Klasse mit dem Core verzahnt ist.
Ordner-Struktur
_public/extensions/core/backend/{plugin-name}/
├── bootstrap.php # Plugin-Klasse + Registrierung (PFLICHT)
├── layout/ # Vue-Admin-Komponenten (optional)
│ ├── index.vue # Listen-/Template-Ansicht
│ ├── index_detail.vue # Detail/Edit-Ansicht
│ └── {subview}.vue # Zeilen-Renderer fuer show_module_list
├── script/ # PHP-Action-Scripts (optional)
│ ├── csvExport.php
│ └── {pfad}/{name}.php
├── api/ # Plugin-eigene API-Models (optional)
│ ├── backend/{name}/model.php
│ └── {public}/{name}/model.php
├── widgets/ # Pagebuilder-Widgets (optional)
│ └── {widget-name}/
│ ├── bootstrap.php
│ └── template/index.vue
├── migrations/ # Plugin-eigene SQL-Migrationen (optional)
│ └── 001_create_tables.sql
├── searchIndex/ # Elasticsearch-Index-Scripts (optional)
├── scheduledTasks/ # Cron-Script-Dateien (optional)
├── src/ # Frontend-Assets (optional)
│ └── css/
└── config/ # JSON-Configs (optional)Pflicht ist nur bootstrap.php. Alle anderen Ordner sind bedarfsabhaengig.
bootstrap.php — Plugin-Klasse
Die Hauptdatei enthaelt genau eine Klasse mit dem Muster {plugin-name}_BackendPlugin und extends install_controller:
<?php
class menueditor_BackendPlugin extends install_controller
{
public static function getVersion(): string
{
return "1.0.0";
}
public static function getName(): string
{
return "Menu Editor";
}
public function install()
{
$this->url_rewrites_backend = ["menueditor"];
$this->content_construct = ["template"];
$this->content_table = [""];
$this->backend_widget_base = ["core/backend/menueditor"];
$this->content_titles = ["Menu Editor"];
$this->content_columns = [""];
$this->content_button = ['[]'];
$this->action_button = ['[]'];
$this->content_replace = ['{}'];
$this->header_buttons = ['[]'];
$this->modal_edits = ['{}'];
$this->icon = "fa-light fa-bars-staggered";
$this->menu_group = 0;
$this->apiEndpoints = [
'{"modelPath": "backend/menueditor/model.php", "endpoint": "backend/menueditor"}',
'{"modelPath": "menueditor/model.php", "endpoint": "menueditor"}'
];
}
public function uninstall()
{
// Leer lassen — Core-installController entfernt plugin_backend-Eintraege
}
public static function update()
{
// Nur befuellen, wenn Datenmigrationen in plugin_backend noetig sind.
// Schema-Migrationen gehoeren in migrations/ (siehe Plugins › Migrations).
}
public function getDirectory()
{
return $this->getDirectoryClass(__DIR__);
}
}Beispiel aus dem echten Repo: _public/extensions/core/backend/menueditor/bootstrap.php.
Klassen-Namens-Konvention
Der Klassenname ist zwingend {root_folder}_BackendPlugin (Root-Folder = Ordnername unter _public/extensions/core/backend/). Der Plugin-Manager instanziiert die Klasse ueber new {root_folder}_BackendPlugin() — bei Abweichung kommt ein Class not found-Fatal und die Installation schlaegt fehl.
Pflicht- und empfohlene Methoden
Die Basis-Klasse install_controller gibt fuenf Pflicht-Methoden vor, die jedes Plugin ueberschreiben muss, plus eine empfohlene Methode fuer Asset-/Layout-Aufloesung.
Pflicht (5)
| Methode | Signatur | Zweck |
|---|---|---|
getName() | public static function getName(): string | UI-Label im Plugin-Manager |
getVersion() | public static function getVersion(): string | Semver, erscheint in plugins.version |
install() | public function install() | Properties setzen, vom Core nach new Plugin() aufgerufen |
uninstall() | public function uninstall() | Cleanup-Hook (meist leer — Core entfernt plugin_backend-Eintraege automatisch) |
update() | public static function update() | Daten-Migrationen bei System-Update. Schema-Aenderungen gehoeren in Migrations, typisch leer |
Empfohlen (1)
| Methode | Signatur | Zweck |
|---|---|---|
getDirectory() | public function getDirectory() | Loest den Plugin-Ordner ueber __DIR__ auf — wichtig fuer Asset-Pfade und Layout-Resolving |
Das empfohlene Muster ist ein Einzeiler:
public function getDirectory()
{
return $this->getDirectoryClass(__DIR__);
}Ohne diese Methode fallen getDirectory()-Aufrufe des Cores auf den install_controller-Default zurueck — fuer minimalistische Plugins ohne eigene Assets ist das meistens ok, fuer Plugins mit Widgets oder Custom-Layouts ist die Implementierung praktisch Pflicht.
Registrierungs-Properties
In install() werden Properties der Basis-Klasse belegt. Alle sind public und werden vom Installer gelesen:
| Property | Typ | Zweck |
|---|---|---|
$url_rewrites_backend | array<string> | URL-Slug pro Content-Construct → /admin/{slug} |
$content_construct | array<string> | table, template oder formonly — siehe Content Constructs |
$content_table | array<string> | DB-Tabelle bei table-Construct |
$content_titles | array<string> | UI-Label |
$content_columns | array<string> | Sichtbare Spalten in Liste (comma-separated) |
$backend_widget_base | array<string> | Pfad zum Widget-Einstiegspunkt pro Construct (z. B. core/backend/{plugin}) |
$content_button | array<string> | Zeilen-Buttons (JSON-String) |
$action_button | array<string> | Header-Action-Button (JSON-String) |
$header_buttons | array<string> | Weitere Header-Buttons |
$content_replace | array<string> | Spalten-Umbenennungen |
$modal_edits | array<string> | Modal-Feld-Konfig |
$icon | string | Font-Awesome-Klasse fuer Navigation |
$menu_group | int | Gruppierung in der Admin-Sidebar |
$apiEndpoints | array<string> | JSON-Strings, je ein Endpoint — siehe API-Endpunkte |
$scheduledTasks | array<array> | Cron-Jobs — siehe Scheduled Tasks |
$webhookEvents | array<string> | Manuelle Events — siehe Webhook-Events |
$pagebuilder_widgets | array<string> | Pagebuilder-Widgets des Plugins |
Arrays, nicht Skalare
Alle content_*-Properties sind Arrays — ein Eintrag pro content_construct. Ein Plugin kann mehrere Einstiegspunkte haben (z. B. eine Tabellen-Ansicht und eine Template-Ansicht). Auch bei nur einem Eintrag muss das Array-Format eingehalten werden, sonst zerfaellt die for-Iteration in installBackend() und es entstehen leere Eintraege in plugin_backend.
Der Installer-Fluss
Nach install() ruft der Core nacheinander diese Installer auf (alle in install_controller.php):
installPlugin($root_folder, $location)
└─ install() # Dein Code, setzt Properties
└─ installBackend($pluginID) # plugin_backend-Eintraege
└─ installWidgets($pluginID) # pagebuilder_widgets + page_widgets
└─ installDatabase() # Plugin-eigene SQL via MigrationRunner
└─ installInjections($pluginID) # Script-Hooks (PreInjection/MidInjection/PostInjection/CacheInjection) registrieren
└─ installSources($pluginID, …) # CSS/JS-Assets
└─ installAPI($pluginID) # pluginAPI-Tabelle
└─ installScheduledTasks($pluginID)
└─ installWebhookEvents($pluginID)Dein Plugin schreibt also keine Inserts von Hand — die Core-Installer uebernehmen das. Dein Job ist nur, die Properties richtig zu setzen.
onLoad() — Template-Plugins
Bei content_construct=template kann das Plugin eine onLoad()-Methode implementieren, die echtes JSON zurueckgibt. Der Rueckgabewert wird als content in der Response des Admin-Page-Endpoints ausgeliefert und steht im Frontend-Template zur Verfuegung:
public function onLoad()
{
parent::onLoad();
$menus = [];
$q = query("SELECT * FROM menueditor_menus ORDER BY id ASC");
while ($row = fetch_assoc($q)) {
$menus[] = $row;
}
if ($GLOBALS['isApiCall']) {
return json_encode(['menus' => $menus]);
}
return "[]";
}Das Original im menueditor-Plugin laedt zusaetzlich die Sprachen-Liste und belegt im else-Pfad $GLOBALS['smarty'] fuer das Legacy-Smarty-Rendering. Das obige Snippet ist fuer die Nuxt-Admin-API gekuerzt — nur der $GLOBALS['isApiCall']-Pfad ist relevant, wenn keine Smarty-Kompatibilitaet benoetigt wird.
onLoad() muss gueltiges JSON liefern
Leere Rueckgabe "" oder String "[]" sind okay, aber niemals PHP-Arrays oder Objekte direkt zurueckgeben — der Core echot den Wert ungefiltert, das Frontend erwartet parsebares JSON.
getContent() — Custom-Content-Buttons
Fuer Buttons mit type="show_custom_content" liefert die Methode getContent() Daten fuer einen beliebig gestalteten Detail-Bereich:
public function getContent(): array
{
return [
'stats' => $this->loadStats(),
'recent_logs' => $this->loadRecentLogs(),
];
}Details dazu in Buttons.
Datenbank-Helper
Plugins nutzen die Helper aus include/mysql.php — niemals mysqli_* oder PDO direkt:
query("SELECT ..."); // laeuft gegen aktive Connection
fetch_assoc($q); // ein Record
fetch_all($q); // alle Records
num_rows($q); // Zeilen zaehlen
insert_id(); // ID nach INSERT — NICHT last_insert_id()!
real_escape_string($value); // Escape fuer Stringsinsert_id() statt last_insert_id()
last_insert_id() existiert im CMS nicht. Bei INSERT immer insert_id() nutzen.
Haeufige Fehler
Klassenname passt nicht zum Ordner
Ordner shop → Klasse shop_BackendPlugin. Bei Abweichung (z. B. Shop_BackendPlugin oder shopPlugin) schlaegt installPlugin() fehl — das Plugin ist im Manager sichtbar, laesst sich aber nicht installieren.
content_*-Properties nicht als Array
Auch bei nur einem Eintrag muessen content_construct, content_table, url_rewrites_backend etc. Arrays sein. Skalare Werte fuehren zu leeren Eintraegen in plugin_backend.
onLoad() gibt PHP-Array zurueck
Core echot die Rueckgabe unveraendert. PHP-Objekte/Arrays werden als Array-String ausgeliefert → Frontend-JSON-Parse-Error. Immer json_encode() verwenden.
Siehe auch
- Plugins-Uebersicht — Was ist ein Plugin, Lebenszyklus
- Content Constructs —
table/template/formonly - Buttons — Alle Button-Typen
- API-Endpunkte —
$this->apiEndpointsund Klassen-Naming - Beispiel: Hello World — komplettes Minimal-Plugin