Template Variables
Everything a view can print arrives as a named variable. Each is written by one of three layers: the engine, the client page pack, or the theme's hooks file.
Overview
A theme does not query. The platform prepares the data and the view prints what it was given. Knowing which layer wrote a variable tells you where to look when it is missing, and whether a theme may change it.
The layers always run in the same order. The engine writes the display environment on every page. Then set_predefined_data("client") writes what every client page needs, and the theme's hooks.php adds the rest, the only layer the theme owns.
Structure
The Three Layers
| Layer | Written by | What belongs there |
|---|---|---|
| Display environment | View::render(), on every page including admin | Language, writing direction, asset addresses, theme settings |
| Client content | set_predefined_data("client"), from every client controller | Branding, menus, currency, cart, session state, legal links |
| Theme data | hooks.php, through filter:template.variables | Anything only this theme needs, derived from core values |
Only the theme layer answers with a value, and that value replaces the whole set instead of merging into it. A listener returns the array it was handed, with its own keys added; a return that is not an array leaves the set untouched.
Page data sits on top of all three. The controller adds it with addData(), and only the page that asked for it gets it.
Reference
What the Engine Writes on Every Page
Assigned in the themed branch of the view layer, after the hook has run. A listener cannot remove them. The list is narrower on the plain PHP engine. There, $cookie_domain, $demo_mode and $demo_themes are written only on the tag-engine path. $template_dir resolves to a path built from the theme's name. A theme on that engine asks the theme object for its directory.
https://dev.wisecp.com/templates/website/WCOM/assets/..., which points inside assets/ and adds a cache-busting stamp to CSS and JS.
ltr or rtl, from the language pack. Print it on the html element; do not hardcode a direction.
Theme::allSettings(). Colour fields also expose a _rgb twin.
What Every Client Page Adds
$client_logo_light_link, $invoice_logo_light_link and their dark twins). Each falls back to the site logo.
currency parameter.
What a Signed-In Page Adds
These exist only while a member is signed in. A public page must not read them without guarding on $is_logged_in.
The Shape of the Composite Values
// $lang_list : one row per ACTIVE language, ranked, the installation default first.
$lang_list = [
['rank' => 1, 'local' => 1, 'selected' => true, 'key' => 'en', 'name' => 'English',
'global-name' => 'English', 'link' => 'https://example.com/home',
'cc' => 'gb', 'cname' => 'United Kingdom', 'pc' => '44', 'flag-img' => 'https://example.com/resources/assets/images/flags/gb.svg'],
];
// $currencies : active, non-hidden currency rows.
$currencies = [
['id' => 1, 'code' => 'USD', 'name' => 'US Dollar', 'prefix' => '$', 'suffix' => '',
'rate' => '1.00000000', 'local' => 1, 'hidden' => 0],
];
// $social_links : one entry per configured profile.
$social_links = [
['name' => 'X', 'url' => 'https://x.com/example', 'icon' => 'bi bi-twitter-x'],
];
// $account_info : display identity, already formatted. `balance` is a STRING with its symbol.
$account_info = [
'name' => 'Ada', 'surname' => 'Lovelace', 'full_name' => 'Ada Lovelace',
'email' => '[email protected]', 'avatar' => '', 'initials' => 'AL',
'balance' => '$120.00', 'support_pin' => '481625', 'two_factor' => true,
'is_reseller' => false, 'last_login' => [], 'last_login_date' => '02/08/2026 - 11:40',
'last_login_ip' => '203.0.113.9', 'last_login_country' => 'GB', 'last_login_city' => 'London',
'dealership' => [],
];
// $client_badges : raw counts; $client_badges_text holds the same keys as pill strings.
$client_badges = ['invoices' => 2, 'support' => 1, 'domains' => 0, 'services' => 0];
// $cookie_notice : empty array when consent is off.
$cookie_notice = [
'needs_prompt' => true, 'text' => 'We use cookies…', 'policy_link' => 'https://example.com/cookie-policy',
'policy_label' => 'Cookie policy', 'accept' => 'Accept', 'reject' => 'Reject',
'prefs' => 'Preferences', 'save' => 'Save', 'prefs_title' => 'Cookie preferences',
'categories' => [
['key' => 'necessary', 'label' => 'Necessary', 'desc' => '…', 'locked' => true, 'granted' => true],
],
'url' => 'https://example.com/cookie-consent',
];
Signatures
// Controllers : the page and the pack.
public function set_predefined_data(string $type = 'client', array $meta = [], array $breadcrumbs = [], array $links = []): void;
public function addData($k = '', $v = ''): void;
public function getData($key);
// View : the render itself. $return_output = true returns the HTML instead of printing it.
public function chose($dir, $noTemplate = false): self;
public function render($_name = null, $data = [], $return_output = false, $source = false): mixed;
// Theme : the settings map behind $setting, and the context-free render used off-request.
public function allSettings(): array;
public function render(string $view, array $data = []): string;
Example
A client page and the view that reads it back. The controller adds only what belongs to this page.
public function page_overview(&$links, &$meta, &$breadcrumbs): string
{
// Read INSIDE set_predefined_data, so it has to be set before the call.
$this->addData("show_client_subnav", true);
$this->set_predefined_data("client", $meta, $breadcrumbs, $links);
// Page data on top of the pack. Set unconditionally: a key the view reads has to
// exist on every render, empty or not, or the view falls into an undefined key.
$this->addData("recent_orders", $this->model->recent_orders((int) $this->getData("client_id")));
echo $this->view->chose("website")->render("account/overview", $this->data, true);
return '';
}
{* Environment layer: direction and language come from the engine, never hardcoded. *}
<section dir="{$ui_dir}" lang="{$ui_lang}">
{* Client layer: guard on the session flag before touching a signed-in-only value. *}
{if $is_logged_in}
<p>{$account_info.full_name} · {$account_info.balance}</p>
{if $client_badges.invoices > 0}
<span class="badge">{$client_badges_text.invoices}</span>
{/if}
{/if}
{* Theme layer: a setting this theme declared, read straight off $setting. *}
{if $setting.topbar_enabled}<div class="topbar">{$setting.topbar_text nofilter}</div>{/if}
{* Page layer: default:[] so an empty page never costs a warning per row. *}
{foreach $recent_orders|default:[] as $order}
<a href="{link route='invoice-detail' p1=$order.id}">{$order.number}</a>
{/foreach}
</section>
Pitfalls
Set every global unconditionally and decide visibility in the view. Absence is not "false": the view falls into an undefined key, and every one costs a log write. Inside a loop that is a measurable slowdown.
A listener that returns something other than the array it was handed drops every key the platform collected. The layout then breaks on the first missing address. Start from the incoming array, add to it, return all of it.
A view built from a scheduled task or a document generator goes through the theme object directly. No controller ran to fill the pack. Only the environment layer and the theme settings are there; anything else is passed as page data.
The controller does not hand out ready links for pages the theme can address itself. Build them with the link function and a route key. A theme that renames a page then needs no controller change.
Related Articles
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.