Template system
DotApp renders HTML with the Renderer facade and a small set of {{ … }} directives. Templates are PHP files owned by a module. Controllers pass data with setViewVar(). There is no separate template language runtime: directives compile to PHP, then a sandbox evaluates the result.
Live pages on this site follow the same rules. Hello World: /helloworld. Secure forms: /documentation/examples/run/forms2. The step-by-step walkthrough is Step-by-step guide.
1.1 What is the template system?
Each module keeps presentation in app/modules/{Module}/views/. A view is a full page or a shell. A layout is a reusable fragment (header, list row, heading). The controller chooses the files, assigns variables, and returns the HTML string from renderView() or renderLayout().
Print a value with {{ var: $title }}. That is the only supported print syntax. {{ $title }} is not a directive and will not output the variable.
1.2 Files and folders
| Kind | Path | Selected with |
|---|---|---|
| View | app/modules/{Module}/views/{name}.view.php |
setView('name') |
| Layout | app/modules/{Module}/views/layouts/{path}.layout.php |
setLayout('path') or {{ layout:path }} |
| Another module | Same structure under that module | setView('Shop:home'), {{ layout: Shop:partials/header }} |
| Base layout | app/parts/views/layouts/ |
{{ baselayout:name }} only |
| Assets | app/modules/{Module}/assets/... |
/assets/modules/{Module}/... |
{{ layout:partials/header }} loads views/layouts/partials/header.layout.php. The layouts directory is already the root — do not write layout:layouts/header. Nested includes stop at depth 20.
1.3 The Renderer facade
Create a renderer with Renderer::new(), point it at a module, then set the view. Named instances (Renderer::new('docs')) are reused as singletons. For a page, start a fresh chain with Renderer::new().
use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Response;
$html = Renderer::new()
->module('HelloWorld')
->setView('hello')
->setViewVar('title', 'Hello World')
->setViewVar('message', 'DotApp 2.0 is running.')
->renderView();
if ($html === '') {
Logger::use()->error('HelloWorld view produced empty output');
return new Response(500, 'Template error');
}
return $html;
There is no setViewVars() plural and no public getView() / getLayout(). Read a single value with getViewVar('title') (missing key returns "") or the whole bag with getViewVars().
1.4 Missing files fail silently
A missing view or layout does not throw. The renderer logs a warning and returns an empty string. A blank page usually means a wrong file name, the wrong module, or setView() never ran. Always pass a fallback name and test the return value:
$html = Renderer::new()
->module('Shop')
->setView('home', 'fallback/empty')
->setLayout('catalog/list', 'catalog/empty')
->setViewVar('title', $title)
->renderView();
The second argument of setView() and setLayout() is a fallback file, not a wrapper layout. loadViewStatic() does not check that the file exists — prefer setView() / loadView().
2.1 First render
The live Hello World module is the minimal pattern: one view, two variables, no nested layout.
app/modules/HelloWorld/views/hello.view.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ var: $title }}</title>
<link rel="stylesheet" href="/assets/modules/HelloWorld/css/hello.css" />
</head>
<body>
<main>
<h1>{{ var: $title }}</h1>
<p>{{ var: $message }}</p>
</main>
</body>
</html>
Call setView('hello') before any setViewVar(). Switching the view later drops the previous variable bag.
2.2 View variables versus layout variables
renderView() evaluates the compiled template with the view variable bag. Values set only with setLayoutVar() do not appear in that output. When you render a page with renderView(), pass every value the view and its included layouts need through setViewVar().
Use setLayoutVar() with renderLayout() when you render a layout file on its own (this documentation site does that for article chunks).
2.3 Shell view plus content layout
Give the page a shell view that contains {{ content }}, and put the inner HTML in a layout selected with setLayout(). Includes from inside the view still use {{ layout:… }}.
return Renderer::new()
->module('Shop')
->setView('home')
->setLayout('content/welcome')
->setViewVar('title', 'Shop')
->setViewVar('items', $items)
->renderView();
View views/home.view.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ var: $title }}</title>
<link rel="stylesheet" href="/assets/modules/Shop/css/page.css" />
</head>
<body>
<main>{{ content }}</main>
<script src="/assets/dotapp/dotapp.js"></script>
<script src="/assets/modules/Shop/js/page.js"></script>
</body>
</html>
Layout views/layouts/content/welcome.layout.php:
<h1>{{_ "Welcome" }}</h1>
{{ foreach $items as $item }}
<p>{{ var: $item['title'] }}</p>
{{ /foreach }}
A view can also be a complete HTML document with no setLayout() and no {{ content }}. Hello World, the Examples demos, and the Users demo all do that.
2.4 Cross-module files
Prefix the path with the other module’s name and a colon:
Renderer::new()->module('Checkout')->setView('Shop:home')->renderView();
The file still lives under that module’s views/ (or views/layouts/ for layouts).
2.5 Other render methods
| Method | Use |
|---|---|
renderView() |
Normal page. Evaluates with view variables. |
renderLayout() |
One layout file. Evaluates with layout variables. |
renderCode($code, $vars) |
Compile and evaluate an HTML string you already have. |
loadView($name) |
Read the view file as text. Missing file: "". |
3.1 Printing values
{{ var: $title }}
{{ var: $user['name'] }}
{{ var:$title }}
The compiler turns this into echo. There is no automatic escaping in the directive. Request data from $request->data() is already protected against XSS. If you pass raw HTML from PHP, escape it in the controller with htmlspecialchars() before setViewVar(), or leave it protected and call DotApp::DotApp()->unprotect($html) only when you intentionally render markup.
{{ var: }} does not accept expressions, ??, ->, or function calls. Prepare the value in the controller.
3.2 Translation
{{_ "Login" }}
{{_ var: $message }}
Use double quotes around the source string. A missing key prints the original text. Load JSON files from the module and set the locale in PHP:
use Dotsystems\App\Parts\Translator;
Translator::loadLocaleFile('Shop:sk_sk.json', 'sk_sk');
Translator::setLocale('sk_sk');
echo Translator::trans('Hello, {{ arg0 }}', $name);
Files live in app/modules/{Module}/translations/{locale}.json. Placeholders are {{ arg0 }}, {{ arg1 }}. There is no pluralization and no locale fallback chain.
3.3 Conditionals
{{ if isset($user) }}
<p>Signed in</p>
{{ elseif $guest === true }}
<p>Guest</p>
{{ else }}
<p>Unknown</p>
{{ /if }}
Put a space after {{ before if, elseif, else, and /if. The closing tag is {{ /if }}, not {{ endif }}.
3.4 Loops
{{ foreach $items as $item }}
<li>{{ var: $item['title'] }}</li>
{{ /foreach }}
{{ while $i < 5 }}
<p>{{ var: $i }}</p>
{{ /while }}
Closing tags are {{ /foreach }} and {{ /while }}. Increment counters in the controller or with a small PHP block in the template. Keep business logic in the controller.
3.5 Includes and the content slot
{{ content }}
Layout tags are includes. They have no closing tag. {{ content }} is filled only when you called setLayout() and then renderView().
3.6 Forms and encryption
<fo-rm method="POST" id="saveForm">
<input type="text" name="title" />
{{ formName(saveItem) }}
<button type="submit">Save</button>
</fo-rm>
<script src="/assets/dotapp/dotapp.js"></script>
{{ formName(saveItem) }}must sit between<fo-rm>(or<form>) and the matching close tag. The tag needs amethodattribute. Outside that pair the renderer leaves the token unchanged.- Prefer
<fo-rm>.dotapp.jsconverts it to a real form and posts with CRC. PHP still runs$request->crcCheck()then$request->form(…). - When the form posts to the current page, omit
actionand pass$request->getPath()as the last argument ofform(). {{ CSRF }}emits a plain token. UseformNamefor application forms.
Encrypt values in the template with a dedicated extra key per field:
<option value="{{ enc(Shop.user.id): $u['id'] }}">{{ var: $u['name'] }}</option>
{{ enc: $secret }}
{{ enc(mykey): "literal" }}
{{ enc: "literal" }} encrypts while the template compiles. {{ enc(key): $var }} encrypts when the page runs. Decrypt with the same extra key: Crypto::decrypt($cipher, 'Shop.user.id'). Failure is === false. Full form walkthrough: Secure forms.
3.7 Blocks
Register a named block in initialize($dotApp), then wrap markup in the view:
Renderer::new()->addBlock('alert', function ($inner, array $args) {
$kind = $args[0] ?? 'info';
return '<div class="alert-' . htmlspecialchars($kind, ENT_QUOTES, 'UTF-8') . '">' . $inner . '</div>';
});
{{ blockerror: }} Undefined callable function ! {{ /blockerror: }}
privateblock stores a fragment as a PHP object you can clone inside the same file:
<?php $block["row"] = new \Dotsystems\App\Parts\PrivateBlock(base64_decode("Jmx0O2xpJmd0O3t7IHZhcjogJG5hbWUgfX0mbHQ7L2xpJmd0Ow==")); ?>
<?php foreach ($items as $it): ?>
<?php echo $block['row']->set('name', $it['name'])->html(); ?>
<?php endforeach; ?>
Native PHP in a template is allowed, but the sandbox strips dangerous functions (eval, exec, system, file_*, curl_*, mail, header, extract, call_user_func*, …). If a call does nothing, that is why. Put I/O and queries in the controller.
3.8 Syntax that is not supported
| Do not write | Write |
|---|---|
{{ $title }} |
{{ var: $title }} |
{{ endif }} / {{ endforeach }} |
{{ /if }} / {{ /foreach }} |
{{ include 'x' }} in a PHP view |
{{ layout:x }} |
| extends / section / yield | renderView() + {{ content }} or {{ layout: }} |
{{ $x ?? 'd' }} |
Prepare the value in the controller |
{{ include path }} exists only in the optional JavaScript template engine (section 8), never in PHP views.
Input-group tags such as {{ InputKeys('register_form') }} and {{ input:text … }} come from Input.php, not from the core directive table. Prefer formName for ordinary HTML forms.
Bridge attributes ({{ dotbridge:on(click)="…" }}) are documented on DotBridge.
4. Assets
Store CSS, JS, and images under app/modules/{Module}/assets/. The framework serves them as:
/assets/modules/{Module}/{path}
<link rel="stylesheet" href="/assets/modules/Shop/css/page.css" />
<script src="/assets/dotapp/dotapp.js"></script>
<script src="/assets/modules/Shop/js/page.js"></script>
Pages that submit <fo-rm>, call $dotapp().load(), or use Bridge must load /assets/dotapp/dotapp.js first. That URL is a framework route. It injects per-session keys. Do not link a raw file from app/parts/js/ on a public page.
Optional CSS helpers: prepareCss() concatenates and minifies into a cache file and prints a <link> tag. removeUnusedCss(true) drops selectors that do not appear as class="…" in the HTML — it also removes classes added later by JavaScript. Leave it off unless you verified the output. There is no built-in cache-busting helper; append ?v= yourself if you need it. Do not enable HTML page cache with useCache(true).
5. Custom renderers
A custom renderer is a callable that receives the compiled HTML (and, when present, the variable bag) and returns HTML. Register it once in initialize($dotApp). It runs for every render after that.
use Dotsystems\App\Parts\Renderer;
Renderer::new()->addRenderer('shop.money', function (string $code, array $vars = []): string {
$amount = number_format((float) ($vars['price'] ?? 0), 2);
return str_replace('{{ money }}', $amount, $code);
});
Renderer::add($name, $callable) is the same registration on the facade. getRenderer($name) returns the callable or false. renderWith($name, $code) runs one renderer on a string.
Built-in renderers registered by the framework include dotapp.block, reactive, and input_form_*. This documentation module registers Docs.code.replace so samples inside <pre><code> are escaped.
6. Pipeline, sandbox, debugging
A typical renderView() run:
- Resolve nested
{{ layout: }}/{{ baselayout: }}(depth ≤ 20). - Extract
privateblockand run custom renderers. - Insert
setLayout()HTML into{{ content }}. - Compile
var/if/foreach/while/enc/ translation. - Replace
{{ CSRF }}and{{ formName() }}. - Process Bridge tags.
- Evaluate in
RenderingIsolator.
A compile or eval failure prints ERROR WHILE EVAL: … into the response. For real line numbers:
define('__RENDER_TO_FILE__', true);
Compiled PHP is written under app/runtime/generator/rendering_*.php, included, then deleted.
7. Translator API
| Method | Result |
|---|---|
trans($text, ...$args) / t() |
Translated string, or the original text if the key is missing |
setLocale($locale) / getLocale() |
Current locale (default en_us) |
loadLocaleFile('Module:file.json', $locale) |
Missing file is skipped with no exception |
has($key, $locale = null) |
bool — use this to detect a missing key |
all($locale = null) |
All keys for that locale |
Product copy that a person can see (buttons, empty states, permission names) must read like shipped UI, not like a reply to a prompt. Keys are the source English (or source) string, lowercased on lookup.
8. Client-side templates
Optional script /assets/dotapp/dotapp.template.js adds $dotapp('#box').template('path/to/view', { items: […] }) in the browser. It understands {{ var: }}, {{ if }}, {{ foreach }}, {{ block: }}, and {{ include partials/header }}. The default base path is /app/views/. Load it after dotapp.js. Wait for the dotapp-template-ready event if the add-on is still loading.
PHP views never gain include. Server HTML stays on Renderer + {{ layout: }}. Use the JS engine when you patch a list from $dotapp().load() without a full page render. Core reactivity (variable, databind, computed) is in the reactivity example. Custom $dotapp().fn widgets are in the JS library example. The live list demo is /documentation/examples/run/lists.
9. Checklist
- View file:
{name}.view.php. Layout file:views/layouts/{path}.layout.php. Renderer::new()->module('Name')->setView('name')beforesetViewVar().- Treat
renderView() === ''as an error. - Print with
{{ var: $x }}. Close branches with{{ /if }}/{{ /foreach }}. - Pass every value through
setViewVar()when you callrenderView(). - Put
{{ formName(handler) }}inside<fo-rm method="…">and load/assets/dotapp/dotapp.js. - Keep queries, auth, and writes in the controller. The template sandbox will strip unsafe PHP.