Skip to content

getWidgetContent()

The static getWidgetContent() method on the widget class is where widgets load dynamic data — e.g. blog articles, products, menu structures, or user-specific content. Pure form-driven widgets simply return null.

Signature

php
class myWidget_PageBuilderPlugin
{
    public static function getWidgetContent($widget_array, $backend = false)
    {
        // Return value: null | array
    }
}
ParameterTypePurpose
$widget_arrayarrayForm values + meta (IDs, folder path, class)
$backendbooltrue when rendering in Pagebuilder preview mode, false in the public frontend

The return value ends up in the Vue template under widget.content. null means: no extra data, Vue only sees widget.data.

When to implement?

SituationgetWidgetContent()
Widget shows only form values (title, text, image, link)return null;
Widget loads content from the DB (blog, shop, menu)Query + return array
Widget shows login status or user-specific contentRead session, return array
Widget renders sub-items (repeater)Load items from page_widget_item
Widget behaves differently between preview and liveif ($backend) branch

$widget_array keys

Form values are exposed with a widget_ prefix — not under the row name directly.

row in 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" maps to key widget_html

The only mapping that doesn't follow the widget_{row} schema: "row": "text" becomes $widget_array['widget_html']. If you read $widget_array['widget_text'], you get null and wonder why. The remap happens in PageService.php — the DB column is called text, but the key name widget_html makes the HTML-content semantic explicit.

$widget_array also contains system keys:

KeyPurpose
item_id / base_idWidget base ID — for item queries against page_widget_item
widget_idConcrete widget record ID (one per placement on a page)
widget_folderAbsolute path to the widget folder
widget_classWidget template name (myWidget)

sale_status and sliderrevolution exist as DB columns but are not mapped into $widget_array — if you need them, load them directly from page_row_col_widget using $widget_id.

Pattern: return null (pure form widget)

For purely static widgets that only render form values:

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;
    }
}

The real image widget from widgetsBase/widgets/image/bootstrap.php — it doesn't get shorter. The Vue template then only sees widget.data.*.

Pattern: load DB data

Typical pattern: the widget has a select field (e.g. a product ID or category ID), and the data gets loaded dynamically from it.

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];
    }
}

In the 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: load items (repeater)

When the widget has an items field, the sub-data lives in page_widget_item. base_id comes from $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 on the items system: Widget items / repeater.

Pattern: frontend vs. backend rendering

Some widgets load data in the live frontend but want to show only placeholders in the Pagebuilder preview (to save DB calls or avoid disrupting preview rendering):

php
public static function getWidgetContent($widget_array, $backend = false)
{
    if ($backend) {
        return ['items' => []];   // placeholder for live preview
    }

    // Only on the real frontend: expensive 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];
}

The real blog widget uses exactly this pattern: if (!$backend) { ...DB queries... }. In preview mode it loads nothing, because blog entries only make sense in the live context with URL parameters (?p=2, ?c=news, ?s=query).

Pattern: session-dependent data

For user-specific widgets (user center, my courses, personalized recommendations):

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-dependent widgets bypass the cache

The response cache (APCu) only kicks in for anonymous GETs. Widgets that read $_SESSION are re-rendered on every request — that's correct behavior, but useful to know for performance tuning.

Pattern: multi-language with fallback

For multi-language content: try the current language first, fall back to the base language on an empty result.

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)];
}

Return shape

The returned array is completely free — the Vue template decides which keys it expects. By convention:

KeyContents
itemsArray for list widgets (blog, shop, gallery)
product / post / courseSingle object for teaser/detail widgets
loggedIn + userAuth status for personalized widgets
configPlugin config for settings widgets
pagesPagination data

Error handling

getWidgetContent() runs during normal page rendering. Errors should not escalate to an HTTP 500 — that would block rendering of the whole page.

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

The Vue component then shows an empty state — much better than a broken page.

Performance

  • No heavy queries without an index — widgets run on every page load. Add indexes on filter columns.
  • Always set LIMIT — even if the dataset is "small", it can grow.
  • Use the $backend guard — the Pagebuilder preview doesn't need real data.
  • The response cache kicks in for anonymous GETs — but only if the widget doesn't read the session.

Common issues

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

As mentioned above: the key is widget_html, not widget_text. You get null instead of the entered text.

Non-static method

getWidgetContent() must be public static. The core calls it via ClassName::getWidgetContent($widget_array, $backend). An instance method is neither found nor executed.

Forgetting real_escape_string

For string parameters coming out of $widget_array (e.g. category names, slugs) that end up in SQL queries: always escape them with real_escape_string(). Use (int) for numeric values. Widgets run in the frontend context with user input from URL parameters — SQL-injection-relevant.

Language globals

The active language lives at $GLOBALS['current_language'] (snake_case), the main language at $GLOBALS['baseLanguage'] (camelCase — historically inconsistent). Both variables depend on the calling context; when in doubt, null-coalesce with a fallback: $GLOBALS['current_language'] ?? 'de'.

No URL resolution for images

The form value widget_image is JSON with a media ID, not a URL. Without files::loadURL() or equivalent resolution, the Vue template gets the raw ID and can't display the image.

See also