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.
Menu-style layout (no superblock)
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.