Tutorial: Ein Bludit-Plugin bauen, das ein eigenes Dashboard-Widget registriert
Vom leeren Pluginordner bis zum registrierten Widget mit Callback, Rollen, Einstellungen, Escaping und Tests.
Dieses Tutorial baut ein eigenständiges Bludit-Plugin, das keine Dashboard-Datei patcht und kein eigenes Drag-&-Drop-JavaScript mitbringt. Es registriert lediglich ein Widget bei MD Dashboard Layout 1.1.1. Genau dieses Muster ist für weitere Fachplugins gedacht.
Das fertige Beispiel liegt als md-dashboard-example-widget-1.0.0.zip im Projekt.
1. Voraussetzungen
- Bludit 3.22.x
- MD Dashboard Layout 1.1.1 oder neuer im Ordner
bl-plugins/md-dashboard-layout/ - MD Dashboard Layout im Administrationsbereich aktiviert
- Grundverständnis der Bludit-Pluginstruktur:
plugin.php,metadata.jsonundlanguages/
2. Verzeichnisstruktur anlegen
bl-plugins/md-dashboard-example-widget/
├── plugin.php
├── metadata.json
└── languages/
├── de.json
├── de_DE.json
└── en.json
3. metadata.json
{
"author": "MD Bludit Praxis",
"email": "",
"website": "",
"version": "1.0.0",
"releaseDate": "2026-09-18",
"license": "MIT",
"compatible": "3.22",
"notes": "Beispielplugin fuer die Widget-Registry von MD Dashboard Layout 1.1.1+."
}
Das Feld compatible sagt Bludit, für welche Haupt-/Nebenversion das Beispiel gedacht ist. Die Widget-Schnittstelle selbst ist eine Funktion von MD Dashboard Layout und keine native Bludit-Core-API.
4. Sprachdatei
{
"plugin-data": {
"name": "MD Dashboard Beispiel-Widget",
"description": "Beispielplugin, das ein eigenes Widget bei MD Dashboard Layout registriert."
}
}
5. Plugin-Klasse und Einstellungen
Wir speichern einen frei änderbaren Widget-Titel und eine Nachricht über Bludits normale Plugin-Datenbank. Das ist absichtlich Teil des Beispiels: Ein registriertes Widget darf weiterhin ein vollwertiges Plugin mit eigener Konfiguration sein.
public function init()
{
$this->dbFields = array(
'widgetTitle' => 'Hallo aus einem Plugin',
'message' => 'Dieses Widget wurde von einem eigenständigen Bludit-Plugin registriert.'
);
}
6. Warum beforeAdminLoad()?
Bludit lädt zuerst die plugin.php-Dateien aller vorhandenen Plugins und ermittelt daraus seine Plugin-Klassen. MD Dashboard Layout 1.1.1 lädt die Hilfsklasse MDDashboardWidgetRegistry deshalb bewusst erst in seiner init()-Methode aus lib/widget-registry.php. So steht die Registry anschließend in beforeAdminLoad() zur Verfügung, ohne von Bludit selbst als Plugin-Klasse missverstanden zu werden.
Wichtig: Eigene Hilfsklassen sollten bei Bludit 3.22.x nicht einfach zusätzlich auf oberster Ebene derselben plugin.php deklariert werden. Bludits Loader verwendet get_declared_classes() und kann solche Klassen sonst als Plugin behandeln. Hilfsklassen daher aus einer separaten Datei erst in init(), prepare() oder einem späteren Hook laden.
public function beforeAdminLoad()
{
if (!class_exists('MDDashboardWidgetRegistry')) {
return '';
}
MDDashboardWidgetRegistry::register('example-plugin-widget', array(
'title' => $this->getValue('widgetTitle'),
'icon' => 'puzzle-piece',
'defaultZone' => 'side',
'priority' => 220,
'roles' => array('admin', 'editor', 'author'),
'description' => 'Referenzwidget aus einem separaten Plugin.',
'source' => __CLASS__,
'renderer' => array($this, 'renderDashboardWidget')
));
return '';
}
Das return '' ist absichtlich leer. Admin-Hooks sollten keinen Wahrheitswert wie true zurückgeben, wenn Bludit die Hook-Rückgabe ausgibt; sonst kann daraus eine sichtbare „1“ werden.
7. Die Widget-ID richtig wählen
- stabil halten: Eine spätere Änderung der ID sieht für das Layout wie ein komplett neues Widget aus.
- Namensraum verwenden, zum Beispiel
kontakt-neue-nachrichtenstatt nurstatus. - nur Kleinbuchstaben, Zahlen, Bindestrich und Unterstrich verwenden.
- keine benutzerspezifischen Werte in die ID einbauen; dafür existiert die Layoutdatei pro Benutzer.
8. Der Renderer
Der Callback erhält Kontextdaten vom Dashboard und gibt ausschließlich den inneren HTML-Inhalt der Karte zurück. Titel, Rahmen, Drag-Handle und Einklappfunktion erzeugt MD Dashboard Layout.
public function renderDashboardWidget($context)
{
global $pages;
$published = count($pages->getPublishedDB());
$username = isset($context['username']) ? $context['username'] : '';
$role = isset($context['role']) ? $context['role'] : '';
$html = '<p>'.$this->esc($this->getValue('message')).'</p>';
$html .= '<dl class="row md-dash-definition mb-0">';
$html .= '<dt class="col-5">Benutzer</dt><dd class="col-7">'.$this->esc($username).'</dd>';
$html .= '<dt class="col-5">Rolle</dt><dd class="col-7">'.$this->esc($role).'</dd>';
$html .= '<dt class="col-5">Veröffentlicht</dt><dd class="col-7">'.$published.'</dd>';
$html .= '</dl>';
return $html;
}
9. Escaping ist Aufgabe des Fachplugins
MD Dashboard Layout kann nicht wissen, welche Teile eines fremden Widget-HTMLs vertrauenswürdig sind. Deshalb escaped das Beispiel alle konfigurierbaren Texte selbst mit htmlspecialchars(). Zahlen aus intern gezählten Datensätzen werden als Integer ausgegeben.
private function esc($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
10. Rollen einschränken
Mit roles kann ein Widget bereits auf Registry-Ebene eingeschränkt werden. Für rein informative Widgets reicht das oft aus. Für schreibende Aktionen muss das Fachplugin trotzdem serverseitig erneut Berechtigungen prüfen; die Sichtbarkeit eines Widgets ist keine Autorisierung.
'roles' => array('admin')
11. Standardzone und Priorität
Die Standardzone wird nur dann benutzt, wenn ein Widget in der gespeicherten Benutzeranordnung noch nicht vorkommt. Sobald der Benutzer es verschiebt, gewinnt sein persönliches JSON-Layout.
main: große Hauptspalteside: rechte schmale Spaltebottom: breite Zone unter beiden Spalten
priority ordnet neue Widgets innerhalb ihrer Standardzone. Sie überschreibt niemals eine bereits gespeicherte Drag-&-Drop-Reihenfolge.
12. Vollständige plugin.php
<?php defined('BLUDIT') or die('Bludit CMS.');
/**
* MD Dashboard Example Widget
*
* Minimal reference plugin for the public widget registry provided by
* MD Dashboard Layout 1.1.1+.
*/
class pluginMDDashboardExampleWidget extends Plugin
{
public function init()
{
$this->dbFields = array(
'widgetTitle' => 'Hallo aus einem Plugin',
'message' => 'Dieses Widget wurde von einem eigenständigen Bludit-Plugin registriert.'
);
}
private function esc($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
public function form()
{
$html = '<div class="form-group">';
$html .= '<label>Widget-Titel</label>';
$html .= '<input class="form-control" name="widgetTitle" value="'.$this->esc($this->getValue('widgetTitle')).'">';
$html .= '</div>';
$html .= '<div class="form-group">';
$html .= '<label>Nachricht</label>';
$html .= '<textarea class="form-control" rows="4" name="message">'.$this->esc($this->getValue('message')).'</textarea>';
$html .= '</div>';
$html .= '<p class="text-muted">Voraussetzung: MD Dashboard Layout 1.1.1 oder neuer muss installiert und aktiviert sein.</p>';
return $html;
}
public function post()
{
$this->db['widgetTitle'] = isset($_POST['widgetTitle']) ? trim((string)$_POST['widgetTitle']) : 'Hallo aus einem Plugin';
$this->db['message'] = isset($_POST['message']) ? trim((string)$_POST['message']) : '';
return $this->save();
}
/**
* All plugin.php files are loaded before admin hooks run. That means the
* registry class is available here regardless of the directory order.
*/
public function beforeAdminLoad()
{
if (!class_exists('MDDashboardWidgetRegistry')) {
return '';
}
MDDashboardWidgetRegistry::register('example-plugin-widget', array(
'title' => $this->getValue('widgetTitle'),
'icon' => 'puzzle-piece',
'defaultZone' => 'side',
'priority' => 220,
'roles' => array('admin', 'editor', 'author'),
'description' => 'Referenzwidget aus einem separaten Plugin.',
'source' => __CLASS__,
'renderer' => array($this, 'renderDashboardWidget')
));
return '';
}
/**
* MD Dashboard Layout calls this callback when the dashboard is rendered.
* The callback returns only the BODY of the widget. Header, drag handle,
* collapse button and persistence are supplied by MD Dashboard Layout.
*/
public function renderDashboardWidget($context)
{
global $pages;
$published = 0;
if (isset($pages) && is_object($pages)) {
$published = count($pages->getPublishedDB());
}
$username = isset($context['username']) ? $context['username'] : '';
$role = isset($context['role']) ? $context['role'] : '';
$html = '<p>'.$this->esc($this->getValue('message')).'</p>';
$html .= '<dl class="row md-dash-definition mb-0">';
$html .= '<dt class="col-5">Benutzer</dt><dd class="col-7">'.$this->esc($username).'</dd>';
$html .= '<dt class="col-5">Rolle</dt><dd class="col-7">'.$this->esc($role).'</dd>';
$html .= '<dt class="col-5">Veröffentlicht</dt><dd class="col-7">'.$published.'</dd>';
$html .= '<dt class="col-5">Quelle</dt><dd class="col-7"><code>md-dashboard-example-widget</code></dd>';
$html .= '</dl>';
return $html;
}
}
13. Aktivieren und prüfen
- MD Dashboard Layout 1.1.1 aktivieren.
- MD Dashboard Beispiel-Widget aktivieren.
- Dashboard neu öffnen.
- Das Widget „Hallo aus einem Plugin“ muss in der Seitenzone erscheinen.
- Es in den Hauptbereich ziehen und Browser neu laden.
- Unter „Ansicht“ ausblenden und wieder einblenden.
- In den Plugin-Einstellungen Titel/Nachricht ändern und Dashboard erneut öffnen.
- Beispielplugin deaktivieren: Das Widget muss verschwinden, der Rest des Layouts muss intakt bleiben.
- Plugin wieder aktivieren: Es wird als neues verfügbares Widget in seiner Standardzone ergänzt, sofern die alte Position nicht mehr gültig ist.
14. Ein eigenes Fachwidget daraus machen
Für ein reales Plugin ersetzt du im Wesentlichen nur ID, Metadaten und Renderer. Die folgenden Beispiele zeigen typische Kandidaten:
- Kontakt/Postfach: Anzahl ungelesener Nachrichten, letzte Betreffzeilen und Link zum Postfach.
- Backup: Zeitpunkt der letzten Sicherung, Größe und Warnung bei überfälligem Backup.
- Besucherstatistik: heutige Besuche, 7-Tage-Trend und Bot-Anteil.
- Radio: aktueller Titel, nächste Sendung und ungeplante Tracks.
- Hotel/Reservierung: heutige Anreisen, offene Reservierungen und Auslastung.
- Arztpraxis: heutige Termine, unbestätigte Buchungen und neue Nachrichten.
15. Wenn das Widget Daten ändern soll
Dann sollte der Renderer nur Buttons/Formulare anzeigen. Die eigentliche Mutation gehört in eine eigene Plugin-Adminroute beziehungsweise adminController(). Dort erneut Rolle prüfen, CSRF validieren, Eingaben sanitizen und erst danach schreiben. Das Dashboard ist Präsentation und Einstiegspunkt, nicht die einzige Sicherheitsgrenze.
16. Fehlerdiagnose
- Registry-Klasse fehlt: MD Dashboard Layout 1.1.1 liegt nicht im Pluginordner oder wurde entfernt.
- Widget erscheint nicht: Beispielplugin nicht aktiviert, Rolle nicht in
rolesoder Registrierung wird nicht ausgeführt. - Widget erscheint, Inhalt leer: Renderer gibt nichts zurück oder löst eine Exception aus.
- Widget springt zurück: Speichern des Layouts schlägt fehl; Netzwerk-Tab und Schreibrechte des Workspace prüfen.
- sichtbare „1“: ein Hook gibt
truezurück und seine Rückgabe wird von Bludit ausgegeben; bei reinen Side-Effect-Hooks leer zurückgeben.
17. Das Wesentliche als Muster
public function beforeAdminLoad()
{
if (class_exists('MDDashboardWidgetRegistry')) {
MDDashboardWidgetRegistry::register('mein-plugin-status', array(
'title' => 'Mein Status',
'defaultZone' => 'side',
'renderer' => array($this, 'renderWidget')
));
}
return '';
}
public function renderWidget($context)
{
return '<p>Mein Dashboard-Inhalt</p>';
}
Damit ist die Kopplung bewusst klein: Dein Fachplugin kennt nur die Registry. MD Dashboard Layout kennt umgekehrt nicht die internen Daten deines Fachplugins.