Normally, 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, there is a separate global array. It is accessed through four methods.
They work exactly like
Tpl*.GlobalAssignisTplAssign,GlobalAppendisTplAppend, and so on: the same calling forms, the same uppercase key conversion, the same merge rules, the same:path notation. There is exactly one difference — no context pointer is needed: there is only one global array for the entire page. If you have mastered one pair of methods, the second will require nothing new.
GlobalAssign($name, $value = '') —
writes a value under the specified name, completely
overwriting whatever was there before: both scalars and arrays.
Simple assignment, no merging.
Called in two forms — a name + value pair, or a single array of pairs:
// Name + value
MELBIS()->GlobalAssign('site_currency', 'UAH');
MELBIS()->GlobalAssign('cart', ['total_sum' => 1500]);
// Array of pairs: each key becomes a separate variable
MELBIS()->GlobalAssign(['cart' => $cart_data, 'user' => $user_data]);When passing an array of pairs, the second argument is not used.
Keys are converted to uppercase — both the variable
name and all string keys inside the passed array, at any depth. Write
however you like in PHP; in the template the key is always UPPERCASE:
GlobalAssign('cart', ['total_sum' => 1500]) →
{CART:TOTAL_SUM}. Numeric keys of lists remain as-is.
GlobalAppend($name, $value = '', $replace = false)
— does not overwrite, but merges with the existing value, and does so
differently depending on the type of $value:
., even if they
are numbers: if the value was 5 and you appended
3, the result will be the string "53", not
8. GlobalAppend never performs numeric
addition: calculate it in PHP and write it with
GlobalAssign.$replace = false) — deep merge at any nesting level:
matching associative sub-keys are preserved at every level, and matching
numeric lists (galleries, tags, etc.) are not corrupted positionally but
appended to the end. With $replace = true
— single-level merge: a matching key is replaced by the new value
entirely, along with everything that was inside it (including lists — in
this mode they are replaced, not appended). String concatenation never
occurs inside arrays in either mode — it is a feature of the scalar case
only.If the type does not match, the old value is lost. Appending an array to a location holding a scalar — the scalar is discarded and merging starts from an empty array. Appending a scalar to a location holding an array — the array is discarded and only the new string remains. No error will be raised.
The calling forms are the same, but with one distinction:
when passing an array of pairs, the role of
$replace is played by the second argument, not the
third.
// Name + value
MELBIS()->GlobalAppend('page', ['title' => 'Product Catalog']);
// Array of pairs: second argument is $replace
MELBIS()->GlobalAppend(['cart' => $extra_cart, 'user' => $extra_user]);
MELBIS()->GlobalAppend(['cart' => $extra_cart_data], true); // single-level replacement instead of mergeA typical use case for Append: several modules
independently write under the same name. The authorization library puts
page = ['auth' => 1, 'user_id' => 5], and the page
module wants to add a title there:
MELBIS()->GlobalAppend('page', ['title' => 'Product Catalog']);
// -> page = ['auth' => 1, 'user_id' => 5, 'title' => 'Product Catalog']GlobalFetch($name, $default = null) —
reads a value back on the PHP side (for example, to read what another
module wrote to the global array). Also accepts a path:
GlobalFetch('PAGE:DIR') will retrieve a nested element
without creating anything. Behavior when a key is missing — same as
TplFetch: without a second argument — a parse error; with
any second argument (including null) — silently returns it
instead of an error.
GlobalClear($name) — removes a value
from the global array. Accepts either a single name or an array of
names: GlobalClear(['CART', 'USER']) will delete both
variables at once. A path can be specified in any of them:
GlobalClear('PAGE:DIR') will delete one nested key, leaving
the rest of PAGE untouched. A non-existent path is simply
ignored.
All four methods understand path notation via
: — the same notation used for reading in
templates ({PAGE:DIR}). The method addresses not the entire
variable, but a single nested element, without touching its
neighbors.
They differ only in whether they create missing path levels:
write methods create them, read methods do not.
GlobalAssign and GlobalAppend will build the
structure down to the required leaf, while GlobalFetch and
GlobalClear will not create anything for a non-existent
path: the former will return a parse error or the default value, the
latter will simply do nothing.
// Instead of "read the entire PAGE, fix DIR in PHP, write PAGE back in full"
MELBIS()->GlobalAssign('PAGE:DIR', $url);
// only PAGE:DIR is affected — the other keys of PAGE remain as they wereThe path also works as a key inside an array of pairs, when you need to distribute values across different branches in a single call:
MELBIS()->GlobalAssign([
'PAGE:DIR' => $url,
'PAGE:LANG' => $lang,
'USER:NAME' => $client_name
]);Case does not matter: the path is converted to
uppercase entirely; 'page:dir' and 'PAGE:DIR'
are the same thing.
Numeric segments address a list element:
GlobalAssign('MENU:ITEMS:0:NAME', 'Home').
Intermediate levels are created automatically — by
Assign and Append. Fetch and
Clear create nothing: for a missing path,
Fetch will result in a parse error (or return the default
value if one was passed), and Clear will do nothing.
An intermediate scalar along the path will be destroyed. If
PAGEheld a string, thenGlobalAssign('PAGE:DIR', $url)will turnPAGEinto an array and the previous string will be lost: the path requires all levels above the leaf to be arrays. No error will be raised.
The difference is exactly the same as without a path, but it is more visible when using a path:
MELBIS()->GlobalAssign('page', ['dir' => '/catalog/', 'title' => 'Catalog']);
// Assign by path — clean leaf replacement
MELBIS()->GlobalAssign('PAGE:TITLE', 'New Arrivals');
// PAGE = ['DIR' => '/catalog/', 'TITLE' => 'New Arrivals']
// Append by path — text concatenation at the same leaf
MELBIS()->GlobalAppend('PAGE:TITLE', ' — page 2');
// PAGE = ['DIR' => '/catalog/', 'TITLE' => 'New Arrivals — page 2']Assign by path — replace the leaf,
Append by path — append to the leaf. Neighboring
keys are unaffected in either case.
This is exactly why the path notation exists: to address a single leaf deep inside a nested structure without reading or rewriting the parent in full, and to explicitly choose between replacement and appending.
What has been written can be read from any template on the page, regardless of which module rendered it.
How it works:
MELBIS()->GlobalAssign('cart', ['total_sum' => 1500]);MELBIS()->GlobalAssign('user', ['is_logged' => 1]);{CART:TOTAL_SUM|num} UAH{*USER:IS_LOGGED==1} Hello, user! {USER:IS_LOGGED*}GlobalAssign('site_currency', 'UAH') →
{SITE_CURRENCY}🔥 Important priority rule: Local module variables
always “win” over global ones. If the global array has a key
TITLE and the current module also passed a key
TITLE, the parser will output the local value from the
module. This protects the markup from accidental conflicts.
🔥 Full feature set: Global variables are processed
by the same powerful parser core as regular data. Modifier chains
({CART:TOTAL_SUM|calc:VALUE*0.9|num:0}), logical
conditions, and loops are all available:
{#MENU:ITEMS}
<a href="{URL}">{NAME}</a>
{MENU:ITEMS#}The parser uses a two-level HTML structure protection system. If there is no local data for a block, the parser checks whether the block is global in order to decide its fate.
The main development rule: For the parser to understand that a block
(e.g., {#PAGE:LANGS} or {*USER:IS_LOGGED})
belongs to the global context and must not be removed on the first pass,
its root key (array) must be initialized in PHP before
the module’s parsing begins, even as an empty element. The easiest way
to do this is via MELBIS()->GlobalAssign('page', []); at
the entry point — this way the PAGE key will appear in the
global array before the first module starts.
The template engine architecture is built on the principle of Deferred Evaluation. This means that page processing happens in two stages (passes). This solves the main problem: how to cache heavy modules while still keeping dynamic data in them (such as the current cart or the user’s name).
How the two passes work:
{#PAGE:LANGS}), it understands this is
someone else’s responsibility. It “freezes” this block
and leaves it in the HTML as raw text.🚀 Bypassing the module cache Precisely because the
second pass is not cached, the global array is the standard way to
“punch through” the cache of a heavy module with targeted dynamic
inserts, without disabling caching of the block entirely. You can
aggressively cache any heavy markup, while delivering visitor-specific
data — the number of items in the cart {CART:COUNT}, the
current language, the authorization check {*USER:IS_LOGGED}
— through the global array, and they will always execute in real
time, even inside a cached block.
Example: the site header is heavy, rarely-changing markup that is worth caching in full. But the logo link must account for the visitor’s current language, and next to it is a catalog phone widget also tied to the language:
<div class="container-fluid">
<div class="row">
<div class="col-xs-12 col-sm-5 col-md-4 col-lg-4">
<a href="/{VAR:LANG_PATH}"><img src="{PATH}/images/design/logo.png" class="img-responsive"></a>
</div>
<div class="hidden-xs col-sm-7 col-md-8 col-lg-8" style="position: relative;">
{MELBIS:studio_cataloge_phone([VAR:LANG])}
<img src="{PATH}/images/design/memo.png" class="img-responsive pull-right">
</div>
</div>
</div>{VAR:LANG_PATH} — a regular global array tag: the
current language is written somewhere in advance by another, non-cached
module (for example, one that determines the language from the
URL/session) via GlobalAssign('var', [...]). It is read
here on the second pass — even if the entire header is served from the
file cache, the logo link still points to the language that is current
for the visitor.[VAR:LANG] — the same global key, but in square
brackets (see “URL-encoded output” in the “Template Syntax” section),
used as a parameter for the nested module call
{MELBIS:studio_cataloge_phone(...)}. The second pass
expands global array tags throughout the module’s text before the engine
gets to expanding {MELBIS:...} calls — so the nested module
receives the already resolved, live language value as its parameter, not
the value that was current when the header was cached.A direct consequence of the second pass not being cached: session data cannot be output as a regular module variable — the very first visitor would bake their name into the cache, and everyone else would see it. Such values are passed only through the global array:
// In a non-cached module that identifies the visitor
$client_id = MELBIS()->SessionGetValue('client_id');
MELBIS()->GlobalAssign('user', [
'is_logged' => !is_null($client_id),
'name' => $client_name
]);{*USER:IS_LOGGED}
Hello, {USER:NAME|html}
{USER:IS_LOGGED*}This way the site header can be cached in full, while the greeting will still be personal. For more details on working with sessions, see the “Sessions and CSRF” section.