Skip to content

AI blog · DotApp PHP Framework 2.0

How to use blocks and superblocks in DotApp PHP Framework

The kernel has two block tools. A named block is {{ block:shop.cards(partials/item-cards) }} plus Renderer::new()->addBlock(). A superblock is {{ privateblock:card }} in the same file: the compiler builds $block['card'], you clone it with ->set()->html(), then compile the clone with renderCode(). {{ sblock: }} and {{ article }} are not Renderer tags — they belonged to an old CMS module and print as text. A folder named sblocks/ is just layouts. This article is a complete Shop card list: register, layout, view, controller.

Common mistakes

Wrong Right
{{ sblock:dotcms.menu(…) }} in a DotApp 2.0 view {{ block:shop.menu(partials/nav) }} plus addBlock
echo $block['card']->html() and expect {{ var: }} to print echo Renderer::new()->renderCode($block['card']->set(…)->html())
Treat privateblock as a PHP function card() The name is the key: tag privateblock:card, PHP $block['card']
setViewVar('fndata', $rows) then renderLayout() renderLayout() sees layout vars. Use setLayoutVar('fndata', $rows)
Spaces after commas in block:name(a, b) The splitter does not trim. Write block:name(a,b) or trim() in the handler
Put queries inside the layout PHP Load rows in the controller (or in the addBlock handler). The sandbox strips file_*, header, extract, eval

When to use which

Use a named block when the view should only name a widget: alert, gallery, card list, menu. Use a superblock when a layout repeats the same card markup with different fields — the old “newest articles” pattern. Use {{ foreach $items as $item }} when the row is simple and you do not need a cloneable fragment. Do not invent a third dispatcher. Official reference: Templates — blocks and superblocks.

Named block API

Register once in initialize($dotApp). The name in addBlock must match the tag. Allowed characters: letters, digits, _, ., -shop.cards is valid. Handler: function (string $inner, array $args, array $vars): string. $vars is the bag of the current render (view vars on renderView(), layout vars on renderLayout()). A controller string works: addBlock('youtube', 'Shop:Embed@youtube!').


use Dotsystems\App\Parts\Renderer;

Renderer::new()->addBlock('alert', function (string $inner, array $args): string {
    $kind = htmlspecialchars(trim((string) ($args[0] ?? 'info')), ENT_QUOTES, 'UTF-8');
    return '<div class="alert-' . $kind . '">' . $inner . '</div>';
});
    

{{ blockerror: }} Undefined callable function ! {{ /blockerror: }}
    

Superblock API

The compiler cuts {{ privateblock:card }}…{{ /privateblock }} out of the file before {{ var: }} compiles, and injects $block['card'] = new PrivateBlock(…). ->set('title', $value) keys must match {{ var: $title }} in the fragment. set() rejects callables and sandbox-disabled function names. ->html() prefixes variable names so two clones do not clash, and still contains {{ var: }}. Compile that string with Renderer::new()->renderCode(…) or the browser shows the token.

Named functions in a layout (function create_menu($menu)) are allowed — each render evals in a random namespace, so you do not hit “Cannot redeclare”. Prefer a closure use ($block) when you need the superblock object.

Complete Shop card list

Three files. The view only names the widget. The layout holds the superblock. The controller loads rows (here: an in-memory list — swap for DB::module('RAW') when you have a table).

File: app/modules/Shop/views/layouts/partials/item-cards.layout.php


<?php $block["card"] = new \Dotsystems\App\Parts\PrivateBlock(base64_decode("DQogICZsdDthcnRpY2xlIGNsYXNzPSJzaG9wLWNhcmQiJmd0Ow0KICAgICZsdDtoMiZndDsmbHQ7YSBocmVmPSJ7eyB2YXI6ICR1cmwgfX0iJmd0O3t7IHZhcjogJHRpdGxlIH19Jmx0Oy9hJmd0OyZsdDsvaDImZ3Q7DQogICAgJmx0O3AmZ3Q7e3sgdmFyOiAkcGVyZXggfX0mbHQ7L3AmZ3Q7DQogICZsdDsvYXJ0aWNsZSZndDsNCg==")); ?>

    

use Dotsystems\App\Parts\Renderer;

$paint = function ($fndata) use ($block) {
    foreach ($fndata as $item) {
        echo Renderer::new()->renderCode(
            $block['card']
                ->set('url', $item['url'])
                ->set('title', $item['title'])
                ->set('perex', $item['perex'])
                ->html()
        );
    }
};
$paint($fndata);
    

File: app/modules/Shop/module.init.php — register the named block next to routes.


use Dotsystems\App\DotApp;
use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Router;

public function initialize($dotApp)
{
    Config::module('Shop', 'prefix') ?? Config::module('Shop', 'prefix', '/shop');
    $p = Config::module('Shop', 'prefix');

    Renderer::new()->addBlock('shop.cards', function (string $inner, array $args, array $vars): string {
        $layout = trim((string) ($args[0] ?? 'partials/item-cards'));
        $items = $vars['items'] ?? [];
        return Renderer::new()
            ->module('Shop')
            ->setLayout($layout)
            ->setLayoutVar('fndata', $items)
            ->useCache(false)
            ->renderLayout();
    });

    Router::get($p . '/', 'Shop:Home@index!', Router::STATIC_ROUTE);
}

new Module(DotApp::DotApp());
    

File: app/modules/Shop/views/home.view.php — the view that renderView() loads. Pass items with setViewVar so the block handler can read it.


<h1>{{ var: $title }}</h1>
{{ blockerror: }} Undefined callable function ! {{ /blockerror: }}
    

File: app/modules/Shop/Controllers/Home.php


namespace Dotsystems\App\Modules\Shop\Controllers;

use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Response;

class Home extends \Dotsystems\App\Parts\Controller
{
    public static function index($request)
    {
        $items = [
            ['url' => '/shop/item/1', 'title' => 'Oak desk', 'perex' => 'Solid top, steel legs.'],
            ['url' => '/shop/item/2', 'title' => 'Wool throw', 'perex' => 'Natural dye, 140 cm.'],
        ];

        $html = Renderer::new()
            ->module('Shop')
            ->setView('home')
            ->setViewVar('title', 'New in Shop')
            ->setViewVar('items', $items)
            ->renderView();

        if ($html === '') {
            Logger::use()->error('Shop home view produced empty output');
            return new Response(500, 'Template error');
        }
        return $html;
    }
}
    

Inner HTML of {{ block:shop.cards }} is ignored unless the handler uses $inner. To paint the same layout without a named block, skip the view tag and call setLayout('partials/item-cards')->setLayoutVar('fndata', $items)->renderLayout() from the controller.

Nested trees are often easier as PHP that echoes HTML, the way the old CMS menu layouts did. Pass the tree as $fndata. Keep markup in the layout; keep the query in the controller.


create_menu($fndata);

function create_menu($menu)
{
    if (!is_array($menu) || !isset($menu[0]) || !is_array($menu[0])) {
        return;
    }
    echo '<ul class="shop-nav">';
    foreach ($menu[0] as $item) {
        $url = htmlspecialchars((string) ($item['url'] ?? '#'), ENT_QUOTES, 'UTF-8');
        $name = htmlspecialchars((string) ($item['name'] ?? ''), ENT_QUOTES, 'UTF-8');
        echo '<li><a href="' . $url . '">' . $name . '</a></li>';
    }
    echo '</ul>';
}
    

Do not call header(), extract(), or file_get_contents() in that file — the sandbox strips them. Escape in PHP here because this path does not go through {{ var: }} (which also does not auto-escape).

FAQ

Where did sblock go?

It was never in Renderer. The old CMS registered a custom renderer that parsed {{ sblock: }} and looked up handlers (and a database table). Today: addBlock + {{ block: }}. Keep sblocks/*.layout.php as layouts if they already expect $fndata.

I see raw braces in the HTML

Superblock html() runs during eval, after the layout already compiled {{ var: }}. Pass the clone through Renderer::new()->renderCode(…).

Can I skip privateblock?

Yes. {{ foreach $fndata as $item }} plus {{ var: $item['title'] }} is enough for a flat list. Superblocks pay off when the card markup is large and you want to clone it like an object.

May I enable Renderer cache on these layouts?

No. useCache(true) is broken. Keep useCache(false) on nested renderLayout() calls.

The cards never appear

Check three bags: the view got setViewVar('items', …), the named block reads $vars['items'], the layout got setLayoutVar('fndata', $items). A missing layout file returns "" and a log warning — not an exception.

See also