The Melbis platform uses MVC architecture: data processing logic is concentrated in the module’s PHP code, while HTML generation is handled by templates. Although PHP allows you to assemble all HTML directly in the module using simple string substitution operations, we strongly recommend against doing so. The Melbis template engine is equipped with powerful tools: loops, conditions, modifiers, callbacks, nesting — all of this frees the module from display logic and makes the code cleaner and more maintainable.
Templates are stored in the templates/ directory at the
project root. Inside are named template groups:
templates/
default/
units/
melbis_base_page/
main.htm
page_index.htm
page_goods.htm
page_404.htm
melbis_store_card/
main.htm
melbis_cataloge/
main.htm
Each module has its own subdirectory inside units/,
which stores all its .htm templates. A single module can
contain any number of templates — they are loaded by Tpl*
methods using the filename without the extension.
A template group is a named set of all .htm files in the
project. The default group is used by default. If desired,
you can create additional groups — for example, for a mobile version of
the site or an alternative design. Once a new group is created, it will
automatically appear in each module’s tree in the IDE.
The active group is set in config.json (the
MELBIS_TEMPLATE parameter) or through the “Design →
Installation” dialog — this is described in detail in the
“Configuration” section.
If necessary, the group can be switched directly in the root script
before calling Run:
MELBIS()->TemplateSet('mobile');
MELBIS()->Run($entry_point, $entry_param);This is convenient, for example, for automatically selecting a mobile template based on the User-Agent.
All module interactions with the template engine occur through
MELBIS()->Tpl* methods:
TplCreate() — creates a new template
engine context and returns its pointer. Called at the beginning of each
module:
$tpl = MELBIS()->TplCreate();TplAssign($tpl, $vars) — passes data to
the context. Accepts both an array (all keys become template variables)
and a “key name” + “value” pair:
// Pass the entire record array
MELBIS()->TplAssign($tpl, $product);
// Pass a single value
MELBIS()->TplAssign($tpl, 'TITLE', 'Product Catalog');
// Pass a nested array (accessible in the template as {PAGE:TITLE}, etc.)
MELBIS()->TplAssign($tpl, 'PAGE', $page);TplParse($tpl, $var, $file) — loads an
.htm template by filename, parses it with the current
context data, and stores the result in a variable. This is the method
the module uses to assemble the various parts of its HTML:
// Parse the page_index.htm template and write to the CONTENT variable
MELBIS()->TplParse($tpl, 'CONTENT', 'page_index');
// Additional templates
MELBIS()->TplParse($tpl, 'WINDOWS', 'windows');
MELBIS()->TplParse($tpl, 'SCRIPTS', 'scripts');TplFinal($tpl, $file) — the final step:
parses the specified template (typically main) with the
current context state (including variables already populated by
TplParse) and returns the finished HTML string. The result
is returned from the module’s main function:
return MELBIS()->TplFinal($tpl, 'main');TplAppend($tpl, $var, $value) — appends
a value to an existing context variable without overwriting it. Useful
for accumulating HTML in parts.
Thus, a typical module workflow with the template engine looks like this:
function MELBIS_BASE_PAGE($mVars)
{
$tpl = MELBIS()->TplCreate();
// ... retrieve data from the database ...
// Pass data
MELBIS()->TplAssign($tpl, 'PAGE', $page);
MELBIS()->TplAssign($tpl, 'GOODS', $goods);
// Assemble the required content variant
MELBIS()->TplParse($tpl, 'CONTENT', 'page_goods');
// Assemble additional parts
MELBIS()->TplParse($tpl, 'SCRIPTS', 'scripts');
// Return the final HTML
return MELBIS()->TplFinal($tpl, 'main');
}The template engine operates on a safe pipeline principle: it
silently ignores missing keys (turning null into an empty
string), blocks direct array output to avoid PHP errors, and allows
building modifier chains.
The Melbis Shop template engine can work not only with flat variables, but also with deeply nested arrays, and supports data exchange between different system modules.
{TITLE} — Standard output. Safely
outputs the variable’s value (if the value is null or the
variable is absent, the parser returns an empty string without
errors).[TITLE] — URL-encoded output. Applies
the rawurlencode function (converts spaces to
%20, Cyrillic characters to %D0%...). Ideal
for forming GET parameters in links:
<a href="?search=[QUERY]">.{CONTENT} contains HTML code
with a {USER_NAME} tag inside it, the parser will
automatically expand the entire chain (the process is protected by a
system iteration limit against infinite loops).If a module passes not just a string to the template, but a complex
multidimensional array (for example, an array with profile settings or
prices), you can access any level of nesting using a colon
:.
The template engine automatically “flattens” arrays, creating
convenient keys. Syntax: {ARRAY:KEY:SUBKEY}
Usage examples:
{USER:PROFILE:PHONE} — outputs the phone number from
the array ['USER']['PROFILE']['PHONE'].{PRODUCT:PRICES:WHOLESALE|num:2} — Modifiers
work everywhere! More about modifiers below. You can apply XSS
protection, truncation, or number formatting directly to elements of
deep arrays.{USER:SETTINGS:AVATAR|def:/img/no-avatar.png} — Sets a
default value if the nested key does not exist.Modules often pass not complex associative arrays, but simple value
lists (for example, an array of tags, a list of IDs, a set of colors:
['red', 'blue', 'green']).
To output such lists in a loop, the parser automatically creates a
virtual key ITEM, which holds the current
value.
How it works: Inside the loop
{#LIST} ... {LIST#} you simply use the {ITEM}
tag. Since this is now a full engine element, all modifiers and
system variables can be applied to it.
Example (Outputting tags separated by commas, without a trailing comma):
Passed array: $mData['TAGS'] = ['php', 'mysql', 'js'];
In the template:
{#TAGS}
<a href="/search/?q=[ITEM]">{ITEM|html}</a>{*!IS_LAST}, {IS_LAST*}
{TAGS#}gVars) — Data Exchange Between
ModulesNormally, each module only parses the data it generated itself (local context). But in complex interfaces, data from one module is often needed in another (for example, the Cart module calculated a total, but it needs to be displayed in the site header).
For this purpose, the global array MELBIS()->gVars is
used.
How it works:
MELBIS()->gVars['cart']['total_sum'] = 1500;MELBIS()->gVars['user']['is_logged'] = 1;{CART:TOTAL_SUM|num} UAH{*?USER:IS_LOGGED==1} Hello, user! {USER:IS_LOGGED*}🔥 Important priority rule: Local module variables
always “win” over global ones. If the global array has a
TITLE key, and the current module also passed a
TITLE key, the parser will output the local value from the
module. This protects the markup from accidental conflicts.
🔥 Important: Global variables gVars
are processed by the same parser as regular data. This means that
all template engine capabilities are available for
global variables:
{CART:TOTAL_SUM|calc:total*0.9|num:0}).{*?CART:TOTAL_SUM>0}).gVars contains an array of elements
(MELBIS->gVars['menu']['items']), you can iterate over
it in any part of the site:
{#MENU:ITEMS}
<a href="{URL}">{NAME}</a>
{MENU:ITEMS#}Used for iterating over arrays (for example, lists of products, properties, files).
{#STORE}
<div class="item">
<b>{NUM1}. {NAME}</b>
</div>
{STORE#}Service variables inside a loop:
{NUM0} / {NUM1} — Element index (from 0) /
Sequential number (from 1).{COUNT} — Total number of elements.{IS_FIRST} / {IS_LAST} — Returns
1 if this is the first/last element (useful for adding
classes).{IS_EVEN} / {IS_ODD} — Returns
1 for even/odd rows.{ROW} — A special variable containing the clean
original array of the current element. Used to pass all row
data to modifier functions (for example, {ROW|json} or
{ROW|calc:...}).Condition tags start with * and close the same way.
Basic checks:
{*NAME} Text {NAME*} — Displayed if the variable is not
empty (not null, not '', not
0).{*!NAME} Text {NAME*} — Negation (displayed if
empty).Comparison operations (?):
{*?PRICE>1000} Expensive {PRICE*} — Supports
==, !=, <, >,
<=, >=.{*?PRICE<OLD_PRICE} Discount! {PRICE*}.Smart checks (IN and BETWEEN):
{*?STATUS==new,active} In progress {STATUS*} —
List check. Compares case-insensitively. Triggers if
STATUS equals NEW, New, or
active. Works with == and
!=.{*?ID==1-10} Top 10 {ID*} — Range
check. Triggers if ID is from 1 to 10 inclusive. Works with
== and !=.Called using the pipe symbol |. Parameters are passed
using a colon :. Modifiers can be chained:
{VAR|mod1|mod2:param}.
{BRAND|def:No brand} — Default
value.{DESC|short:150} — Truncation. Keeps
150 characters (100 by default) and appends ....{TEXT|html} — Safe output of HTML entities
(htmlspecialchars).{TEXT|slash} — Safe output
(addslashes).{CONTENT|plain} — Powerful cleanup.
Removes <script> and <style> tags,
strips HTML tags, decodes ,
©, and collapses extra whitespace. Ideal for SEO
tags.{ID|int} — Cast to integer.{PRICE|num:2,., } — Formatting.
Parameters: digits, decimal_separator, thousands_separator.
Defaults: 2, ,, '. Output:
1'250,50.{QTY|nums:2} — Smart formatting. Drops
zeros if the number is whole (5.00 -> 5,
but 5.50 -> 5.50).{ROW|calc:price-(out_price/100)|num:0} —
Calculator. Safely evaluates a formula, substituting
keys from the {ROW} array. {QTY|calc:value*2}
— math for a single variable. Use the word value inside the
formula.{TIME|date:Y-m-d} — Smart date.
Automatically understands both UNIX timestamps and string dates (passing
them through strtotime). Default format:
d.m.Y H:i.These modifiers automatically adapt output formats to the current
page language (MELBIS()->LanguageSet('en')), using the
ICU library. Ideal for projects with en, ru,
ua, etc.
{PRICE|inum:2} — Formats a number according to language
rules. (In ru outputs 1 250,50, in
en outputs 1,250.50). No need to specify
separators, only the number of decimal places (2 by default).{QTY|inums:2} — Same, but drops the fractional part for
whole numbers (5.00 -> 5).{TIME|idate} — Smart localized date. Without
parameters, outputs the standard format for the language (ru: 9 Apr
2026, 14:00, en: Apr 9, 2026, 2:00 PM).{TIME|idate:dd MMMM yyyy} — Custom date format.
Important: the ICU standard is used, not the PHP
standard. Case matters (for example, month is M, and
minutes are m).{ROW|join:, } — Joins a flat array into a string.{ROW|json} / {ROW|js} — Converts an array
to JSON. js uses HEX escaping of tags and quotes for safe
insertion of both arrays and text strings into JavaScript.{UPLOAD_TIME|path} — Generates a path to the file
storage directory based on the upload date. Returns a string with a
trailing slash. Used in templates together with the filename:
<img src="{UPLOAD_TIME|path}{FILE_NAME}">.{TEXT|text} — Adds the site’s base path to relative
src= and href= links (uses
MELBIS_ROOT).The parser allows you to pass a variable’s value through any available PHP function (unless it is blocked for security reasons).
How arguments are passed:
|) always automatically becomes the first argument of the
called function.: are split by
comma , and passed to the function as the second, third,
and subsequent arguments.Syntax:
{VARIABLE|function_name:param2,param3}
Usage examples:
{PRICE|round} — Calls round($PRICE).
Rounds 15.7 to 16.{TITLE|mb_strtoupper} — Calls
mb_strtoupper($TITLE). Converts the string to
uppercase.{EMAIL|md5} — Calls md5($EMAIL). Useful
for generating hashes (for example, for Gravatar).{PRICE|round:1} — Calls round($PRICE, 1).
Leaves one decimal place.{ARTICLE|trim:.} — Calls
trim($ARTICLE, '.'). Removes dots from the beginning and
end of the string.{TEXT|strip_tags:<b><a>} — Calls
strip_tags($TEXT, '<b><a>'). Removes all tags
except the allowed <b> and
<a>.For complex business logic (for example, string translation,
multi-currency support, specific formatting), you can register your own
functions. They are called through a modifier starting with the
$ sign (for example, |$trans).
The callback mechanism uses strict registration and passes all data to the function as a single associative array, allowing flexible combination of template variables and system settings.
You have two levels of registration: global and local (at the level of a specific module).
DefineCallback): Defined at the
core level and available to all connected modules whose prefixes match
the function name.MELBIS()->DefineCallback('trans', 'MELBIS_INC_translate', ['from' => 'en']);UnitCallbackCustom):
Called inside a specific module. Allows adding a local function or
overriding a global setting exclusively for the current module.MELBIS()->UnitCallbackCustom('trans', 'MELBIS_INC_translate', ['from' => 'ru']);UnitCallbackSet and
UnitCallbackGet methods are available for dynamically
changing default parameters during script execution.In the template, you specify the alias (with the $ sign)
and, if necessary, list additional keys separated by
commas — the values of which need to be extracted from the current data
context and passed to the function.
Syntax:
{MAIN_VARIABLE|$alias:EXTRA_KEY_1,EXTRA_KEY_2}
Example: {TITLE|$trans:ID,CODE}
When the parser encounters {TITLE|$trans:ID,CODE}, it
assembles a single associative array $args_assoc and passes
it to your function.
Assembly occurs with priority layering (from lowest to highest):
DefineCallback or UnitCallbackCustom) are
taken. Example:
['lang' => 'en', 'color' => 'black']| and places
it in the array. Example:
['lang' => 'en', 'color' => 'black', 'title' => 'Value']LANG, CODE) in the current
template data. If the template developer explicitly requested a key that
already exists in the PHP default settings, the value from the
HTML template will completely overwrite the default. Final
array:
['lang' => 'ru', 'color' => 'black', 'title' => 'Value', 'code' => 'ABC']
(here lang from the template overrode lang
from the PHP settings).Using modifier chains for safe generation of microdata and meta tags.
<title>{PRODUCT_NAME|html} buy for {PRICE|nums} UAH</title>
<meta name="description" content="{DESCRIPTION|plain|short:160}">
<script type="application/ld+json">
{ROW|json}
</script>Checking availability, calculating the discount percentage on the fly, and displaying placeholders.
<div class="product-card {*IS_FIRST}first-item{IS_FIRST*}">
<img src="{IMAGE_URL|def:/images/no-photo.png}" alt="{PRODUCT_NAME|html}">
<h3>{PRODUCT_NAME}</h3>
{*?PRICE<OLD_PRICE}
<div class="badge-discount">
Discount {ROW|calc:100-(price/old_price*100)|nums:0} %
</div>
<span class="old-price">{OLD_PRICE|num} UAH</span>
{PRICE*}
<span class="current-price">{PRICE|num} UAH</span>
{*?STOCK_STATUS==instock,preorder}
<button onclick="addToCart({ID|int})">Buy</button>
{STOCK_STATUS*}
{*?STOCK_STATUS!=instock,preorder}
<span class="out-of-stock">Out of stock</span>
{STOCK_STATUS*}
</div>Using ranges, dates, and loop counters.
<table>
<tr>
<th>#</th>
<th>Date</th>
<th>Amount</th>
<th>Status</th>
</tr>
{#ORDERS}
<tr class="{*IS_EVEN}bg-gray{IS_EVEN*}">
<td>{NUM1}</td>
<td>{CREATED_AT|date:d M Y}</td>
<td>{TOTAL|num:2,., } $</td>
<td>
{*?STATUS_ID==1-3} <span class="badge-blue">{STATUS_NAME}</span> {STATUS_ID*}
{*?STATUS_ID==4-5} <span class="badge-green">{STATUS_NAME}</span> {STATUS_ID*}
</td>
</tr>
{ORDERS#}
</table>