Skip to content

getWidgetContent()

Die statische Methode getWidgetContent() auf der Widget-Klasse ist der Ort, an dem Widgets dynamische Daten laden — z. B. Blog-Artikel, Produkte, Menu-Strukturen oder user-spezifische Inhalte. Reine Formular-Widgets geben einfach null zurueck.

Signatur

php
class myWidget_PageBuilderPlugin
{
    public static function getWidgetContent($widget_array, $backend = false)
    {
        // Rueckgabe: null | array
    }
}
ParameterTypZweck
$widget_arrayarrayFormular-Werte + Meta (IDs, Ordner-Pfad, Klasse)
$backendbooltrue beim Rendern im Pagebuilder-Preview-Modus, false im oeffentlichen Frontend

Der Rueckgabewert landet im Vue-Template unter widget.content. null bedeutet: keine zusaetzlichen Daten, Vue sieht nur widget.data.

Wann implementieren?

SituationgetWidgetContent()
Widget zeigt nur Formular-Werte (Titel, Text, Bild, Link)return null;
Widget lädt Inhalte aus DB (Blog, Shop, Menu)Query + Array zurueckgeben
Widget zeigt Login-Status oder user-spezifischen InhaltSession lesen, Array zurueckgeben
Widget rendert Sub-Items (Repeater)Items aus page_widget_item laden
Widget zeigt andere Frontend-Verhalten zwischen Preview + Liveif ($backend)-Branch

$widget_array-Schluessel

Formular-Werte werden mit widget_-Prefix zur Verfuegung gestellt — nicht unter dem row-Namen direkt.

row im JSONKey in $widget_array
titlewidget_title
subtitlewidget_subtitle
textwidget_html (Legacy!)
linkwidget_link
imagewidget_image
gallerywidget_gallery
logowidget_logo
colorwidget_color
pricewidget_price
videowidget_video
locationwidget_location
vcardwidget_vcard
short_textwidget_short_text
mapwidget_map
accordionwidget_accordion
pagebuilderwidget_pagebuilder
formwidget_form

row: "text" hat Key widget_html

Das einzige Mapping, das nicht dem Schema widget_{row} folgt: "row": "text" wird zu $widget_array['widget_html']. Wer $widget_array['widget_text'] liest, bekommt null und wundert sich. Das Remapping passiert in PageService.php — die DB-Spalte heisst text, der Key-Name widget_html macht die HTML-Content-Semantik explizit.

Zusaetzlich enthaelt $widget_array System-Keys:

KeyZweck
item_id / base_idWidget-Base-ID — fuer Item-Queries in page_widget_item
widget_idKonkrete Widget-Record-ID (eine pro Platzierung auf einer Seite)
widget_folderAbsoluter Pfad zum Widget-Ordner
widget_classWidget-Template-Name (myWidget)

sale_status und sliderrevolution existieren als DB-Spalten, sind aber nicht im $widget_array gemappt — falls gebraucht, direkt via $widget_id aus page_row_col_widget laden.

Pattern: null zurueckgeben (reines Formular-Widget)

Fuer rein statische Widgets, die nur Formular-Werte darstellen:

php
class image_PageBuilderPlugin
{
    public static function getVersion() { return "1.0.0"; }
    public static function getName()    { return "Image"; }

    public static function getWidgetContent($widget_array, $backend = false)
    {
        return null;
    }
}

Das reale image-Widget aus widgetsBase/widgets/image/bootstrap.php — kuerzer geht es nicht. Im Vue-Template steht dann ausschliesslich widget.data.* zur Verfuegung.

Pattern: DB-Daten laden

Typisches Muster: Widget hat einen select-Feld (z. B. Produkt-ID oder Kategorie-ID), daraus werden dynamisch Daten geladen.

php
class productTeaser_PageBuilderPlugin
{
    public static function getVersion() { return "1.0.0"; }
    public static function getName()    { return "Product Teaser"; }

    public static function getWidgetContent($widget_array, $backend = false)
    {
        $productId = (int) ($widget_array['widget_subtitle'] ?? 0);
        if ($productId === 0) return null;

        $currentLanguage = $GLOBALS['current_language'] ?? 'de';
        $lang = real_escape_string($currentLanguage);

        $q = query("SELECT id, title, price, image, url_rewrite
                    FROM s_products
                    WHERE id = $productId AND language_short LIKE '$lang'
                    LIMIT 1");
        if (num_rows($q) === 0) return null;

        $row = fetch_assoc($q);
        $row['image'] = files::loadURL($row['image']);

        return ['product' => $row];
    }
}

Im Vue-Template:

vue
<template>
  <div v-if="widget.content?.product" class="product-teaser">
    <NuxtImg :src="widget.content.product.image" :alt="widget.content.product.title" />
    <h3>{{ widget.content.product.title }}</h3>
    <span>{{ widget.content.product.price }} EUR</span>
  </div>
</template>

Pattern: Items laden (Repeater)

Wenn das Widget ein items-Feld hat, liegen die Sub-Daten in page_widget_item. base_id kommt aus $widget_array['item_id']:

php
public static function getWidgetContent($widget_array, $backend = false)
{
    $widgetBaseId = (int) ($widget_array['item_id'] ?? 0);
    $language     = real_escape_string($GLOBALS['current_language'] ?? 'de');

    $q = query("SELECT * FROM page_widget_item
                WHERE widget_id = $widgetBaseId
                  AND language_short LIKE '$language'
                ORDER BY position ASC");

    $items = [];
    while ($row = fetch_assoc($q)) {
        $row['image'] = files::loadURL($row['image']);
        $items[] = $row;
    }

    return ['items' => $items];
}

Details zum Items-System: Widget-Items / Repeater.

Pattern: Frontend- vs. Backend-Rendering

Manche Widgets laden im Live-Frontend Daten, wollen im Pagebuilder-Preview aber nur Platzhalter zeigen (z. B. um DB-Calls zu sparen oder Preview-Seitenrendering nicht zu stoeren):

php
public static function getWidgetContent($widget_array, $backend = false)
{
    if ($backend) {
        return ['items' => []];   // Platzhalter fuer Live-Preview
    }

    // Nur im echten Frontend: aufwendige Query
    $items = [];
    $q = query("SELECT * FROM blog WHERE visible = 1 ORDER BY postdate DESC LIMIT 10");
    while ($row = fetch_assoc($q)) {
        $items[] = $row;
    }
    return ['items' => $items];
}

Das reale blog-Widget nutzt genau dieses Pattern: if (!$backend) { ...DB-Queries... }. Im Preview-Mode laedt es gar nichts, weil Blog-Eintraege erst im Live-Kontext mit URL-Parametern (?p=2, ?c=news, ?s=query) Sinn ergeben.

Pattern: Session-abhaengige Daten

Fuer User-spezifische Widgets (User-Center, My-Courses, personalisierte Empfehlungen):

php
public static function getWidgetContent($widget_array, $backend = false)
{
    $userId = (int) ($_SESSION['userid'] ?? 0);
    if ($userId === 0) {
        return ['loggedIn' => false];
    }

    $q = query("SELECT id, title FROM elearning_purchases p
                JOIN elearning_courses c ON c.id = p.course_id
                WHERE p.user_id = $userId");
    return [
        'loggedIn' => true,
        'courses'  => fetch_all($q),
    ];
}

Session-abhaengige Widgets cachen nicht

Der Response-Cache (APCu) greift nur fuer anonyme GETs. Widgets, die $_SESSION lesen, werden pro Request neu gerendert — das ist korrekt so, aber gut zu wissen fuer Performance-Tuning.

Pattern: Multi-Language mit Fallback

Bei mehrsprachigen Inhalten: aktuelle Sprache zuerst, bei leerem Resultat Fallback auf Basissprache.

php
public static function getWidgetContent($widget_array, $backend = false)
{
    $currentLang = real_escape_string($GLOBALS['current_language'] ?? 'de');
    $baseLang    = real_escape_string($GLOBALS['baseLanguage']     ?? 'de');

    $q = query("SELECT * FROM blog
                WHERE visible = 1 AND language_short LIKE '$currentLang'
                ORDER BY postdate DESC LIMIT 10");

    if (num_rows($q) === 0 && $currentLang !== $baseLang) {
        $q = query("SELECT * FROM blog
                    WHERE visible = 1 AND language_short LIKE '$baseLang'
                    ORDER BY postdate DESC LIMIT 10");
    }

    return ['items' => fetch_all($q)];
}

Rueckgabe-Shape

Der zurueckgegebene Array ist komplett frei — das Vue-Template bestimmt, welche Keys es erwartet. Konventionell:

KeyInhalt
itemsArray fuer Listen-Widgets (Blog, Shop, Galerie)
product / post / courseEinzel-Objekt fuer Teaser/Detail-Widgets
loggedIn + userAuth-Status fuer personalisierte Widgets
configPlugin-Config fuer Einstellungs-Widgets
pagesPagination-Daten

Fehler-Handling

getWidgetContent() wird waehrend des normalen Page-Renderings aufgerufen. Fehler sollten nicht zu HTTP-500 eskalieren — sie verhindern das Rendering der ganzen Seite.

php
public static function getWidgetContent($widget_array, $backend = false)
{
    try {
        // DB-Operationen
        return ['items' => $items];
    } catch (Throwable $e) {
        error_log('Widget xyz: ' . $e->getMessage());
        return ['items' => []];   // stilles Fallback
    }
}

Die Vue-Komponente zeigt dann einen leeren Zustand — besser als kaputte Seite.

Performance

  • Keine schweren Queries ohne Index — Widgets laufen bei jedem Seitenaufruf. Indizes auf Filter-Spalten setzen.
  • LIMIT immer setzen — auch wenn die Datenmenge "klein" ist, kann sie wachsen.
  • $backend-Guard nutzen — Pagebuilder-Preview braucht keine echten Daten.
  • Response-Cache greift fuer anonyme GETs — aber nur wenn das Widget keine Session liest.

Haeufige Fehler

$widget_array['widget_text'] bei row: "text"

Wie oben erwaehnt: Key ist widget_html, nicht widget_text. Ergibt null statt des eingegebenen Textes.

Nicht-statische Methode

getWidgetContent() muss public static sein. Der Core ruft sie via ClassName::getWidgetContent($widget_array, $backend) auf. Eine Instanzmethode wird nicht gefunden und nicht ausgefuehrt.

real_escape_string vergessen

String-Parameter aus $widget_array (z. B. Kategorie-Namen, Slugs) in SQL-Queries: immer mit real_escape_string() escapen. Numerische Werte mit (int). Widgets laufen im Frontend-Kontext mit User-Eingaben aus URL-Parametern — SQL-Injection-relevant.

Sprach-Globals

Die aktive Sprache steht unter $GLOBALS['current_language'] (Snake-Case), die Hauptsprache unter $GLOBALS['baseLanguage'] (CamelCase — historisch inkonsistent). Beide Variablen haengen vom Aufruf-Kontext ab; bei Unsicherheit null coalesce mit Fallback: $GLOBALS['current_language'] ?? 'de'.

Keine URL-Aufloesung fuer Bilder

Formular-Wert widget_image ist ein JSON mit Media-ID, keine URL. Ohne files::loadURL() oder aequivalente Aufloesung bekommt das Vue-Template die rohe ID und kann das Bild nicht anzeigen.

Siehe auch