# Template Variables

https://dev.wisecp.com/es/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.

- **$template_dir**: Filesystem path of the active theme directory, with a trailing separator. Not an address: use it only to read files.
- **$tadress**: Address of the theme directory. Prefer `{asset path='...'}`, which points inside `assets/` and adds a cache-busting stamp to CSS and JS.
- **$badress**: Base address of the installation, with a trailing slash. On a bound subdomain this is that host, not the main site.
- **$sadress**: Address of the shared resources directory (uploads, flags, plugin assets), owned by no theme.
- **$ui_lang**: Active language code, resolved when the page is built, so a late language switch is reflected.
- **$ui_dir**: `ltr` or `rtl`, from the language pack. Print it on the html element; do not hardcode a direction.
- **$cookie_domain**: The host scope this installation's cookies use, empty on a single-host install. Theme JavaScript writing a cookie uses the same scope.
- **$demo_mode · $demo_themes**: Demo chrome only. The theme list is computed only while demo mode is on.
- **$setting**: Every field of this theme's settings schema merged with its saved value, from `Theme::allSettings()`. Colour fields also expose a `_rgb` twin.

### What Every Client Page Adds

- **$company_name**: Trading name, from the language pack's constants, falling back to the company information block.
- **$light_logo_link · $dark_logo_link**: Site logo per colour mode. The client panel and the invoice document carry their own pair (`$client_logo_light_link`, `$invoice_logo_light_link` and their dark twins). Each falls back to the site logo.
- **$favicon_link**: Square brand mark, for places a wordmark does not fit (a collapsed rail, an app icon).
- **$current_year**: Current year as a string, for the footer copyright line. Do not compute a date in a view.
- **$lang_list · $lang_count · $selected_lang_key**: Active languages with a ready link each, their number, and the selected code in upper case. The layout also prints them as alternate language links.
- **$currencies · $currencies_count · $selected_currency · $selected_currency_code**: Currencies offered to the visitor (hidden ones filtered out) and the active one. Switching is a server round trip: link to the same page with a `currency` parameter.
- **$currency_formats_json · $currency_ids_json · $currency_rates_json · $currency_default**: Ready JSON for the theme's money script. Each format is a sample string the script parses to learn that currency's separators.
- **$header_menu · $footer_menu · $mobile_menu**: The three operator-managed menu trees. The mobile tree falls back to the header tree when it is empty.
- **$social_links · $contact_email · $contact_phone**: Social profiles as a list, plus the first configured address and number as plain strings, for the header and footer strips.
- **$visibility_cart · $cart_count**: Whether the shop is on at all, and the live item count. The count is always set, so a badge can be revealed without a reload.
- **$login_enabled · $registration_enabled · $account_actions_enabled**: The operator's account switches. The third is false when neither signing in nor signing up is possible. That hides the account menu and the cart.
- **$affiliate_enabled · $reseller_enabled · $only_client_panel**: Program switches for the account menu, and portal-only mode, in which a theme drops its links back to the public website.
- **$is_logged_in · $client_id**: The canonical session flag and the signed-in identity. Every "signed in or guest" branch tests this flag.
- **$privacy_contract_link · $terms_contract_link · $cookie_contract_link**: Legal pages the operator mapped, empty when unmapped. Hide the link when it is empty instead of printing a dead one.
- **$cookie_notice**: The consent prompt as a finished payload, or an empty array when consent is off. Decided server side: the cookies are not readable from the browser.
- **$notification_count · $notification_count_text**: Bell counter and its pill text (capped beyond two digits). Always set, but only queried for a page that asked for the client shell.
- **$password_min_length · $password_special_chars · $default_country**: The operator's password rules for the suggestion button, and the site's default country as a last-resort fallback for phone and address fields.
- **$links · $meta · $breadcrumb · $local_l · $show_powered_by**: Page frame: the controller's link map, page metadata, the trail, the installation's default language, and whether the footer prints the platform credit.

### 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`.

- **$account_info · $user_info**: Display identity for the chrome (name, avatar, initials, formatted balance, support PIN, previous sign-in) and the raw account row behind it.
- **$announcements**: Operator notices for the account shell, already filtered by language and by the active account's country.
- **$show_services · $show_domains · $show_invoices · $show_support · $show_sms**: Section visibility: the granted sub-user permission AND the operator's feature switch. A section the visitor cannot open is never linked.
- **$is_self_account · $switch_accounts**: Whether the active account is the member's own, and the accounts they may switch to.
- **$client_badges · $client_badges_text**: Attention counters per section as raw numbers, plus their pill text. The raw numbers stay for accessible names, which say the real figure.
- **$order_service_link**: Where "buy a service" goes, empty when the sub-user may not order. A theme that disables the catalogue overrides this in its hooks file.
- **$dashboard_modal · $twofa_required · $password_required**: The dashboard gate chain. Only one modal opens per load, and the chosen one is named here rather than decided in the view.
- **$client_addon_links**: Client-area pages contributed by enabled addon modules, each with a name, an address and an icon.
- **$verification_required · $verify_link**: Set while the member owes identity verification. The banner belongs to the layout; the redirect for gated pages already happened before the view ran.

### The Shape of the Composite Values

```php
// $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' => 'ada@example.com', '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

```php
// 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.

```php
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 '';
}
```

```smarty
{* 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

> **A variable set inside a condition is missing on the pages that skipped it**
> 
> 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.

> **The variables hook replaces the array, it does not merge into it**
> 
> 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 background page gets the environment, not the client pack**
> 
> 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.

> **Addresses are built in the view, not passed as variables**
> 
> 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

- [The Theme Engine](https://dev.wisecp.com/en/the-theme-engine)
- [Theme Hooks and Output Filters](https://dev.wisecp.com/en/theme-hooks-and-output-filters)
- [Theme Settings](https://dev.wisecp.com/en/theme-settings)
- [Menus and Navigation](https://dev.wisecp.com/en/menus-and-navigation)
- [Controllers and Routing](https://dev.wisecp.com/en/controllers-and-routing)
