# The Theme Engine

https://dev.wisecp.com/es/the-theme-engine

A website theme owns the whole public surface: the markup, styling and wording of every page a visitor sees.

## Overview

The admin panel and the website are built by different systems. The panel is fixed: plain PHP templates. The website is themed: a directory of views the platform hands its data to.

A theme is not a skin: no markup underneath to fall back on. No cart view means no cart, so every theme implements the same list of surfaces.

## Structure

### Template Engines

Three engines are supported; a theme commits to one in its manifest. The same data reaches the view either way.

- **smarty**: Tag syntax with its own filters and functions, views ending in `.tpl`.
- **twig**: The other tag engine, views ending in `.twig`.
- **php**: Plain `.php` templates, no tag layer. Full language access, full responsibility for escaping.

### What a Theme Holds

A theme directory sits under `templates/website`. The directory is the theme; the manifest is the first file read.

```bash
templates/website/{Theme}/
├── theme.php     # the manifest: engine, meta, status, settings schema. Written by the author
├── config.php    # the saved setting VALUES. Written at runtime, never by hand
├── hooks.php     # the theme's listeners: variables, output filters, routing additions
├── cover.png     # the catalogue thumbnail meta['image'] points at
├── layouts/      # page shells a view extends: default, auth, checkout, invoice
├── partials/     # the pieces a layout assembles: header, footer, topbar, drawer, popup
├── components/   # markup two or more views share: plan grid, payment methods, panels
├── views/        # the surfaces, grouped by area: account/ auth/ checkout/ content/ page/ products/
├── tables/       # column presets for the client area list tables
├── assets/       # css, js, images, favicon the theme ships
├── locale/       # the theme's own wording, per language and per view scope
└── content/      # operator edits of that wording, written from the panel
```

- **theme.php**: The only required file: engine, catalogue entry, settings schema. Without it the directory never appears in the panel.
- **config.php**: The saved values. The platform writes it; a fresh theme ships without one.
- **layouts/**: Page shells: public, checkout, invoice, plus an optional sign-in shell. A view extends one and fills its blocks.
- **partials/**: Pieces a layout assembles rather than a view: header, footer, topbar, drawer, popup.
- **components/**: Markup two or more views share: plan grid, payment methods, dashboard panel.
- **views/**: The surfaces, grouped by area. A controller asks for `account/dashboard`; the engine adds directory and extension.
- **tables/**: Column presets for the client area lists. A missing preset is not an error: raw columns are shown.
- **assets/**: Everything the browser fetches: `css/`, `js/`, `images/`, favicon, libraries. One function addresses them.
- **locale/ and content/**: `locale/` holds the author's defaults, per language and view scope; `content/` holds the operator's edits.
- **hooks.php**: Optional, included once before the first page. Where a theme reaches the platform without editing a controller.

### How Data Arrives

Controllers do the work and hand the result to the view as named variables. A theme asks through a hook rather than querying the database itself.

Most variables belong to one surface, catalogued in [Template Variables](https://dev.wisecp.com/en/template-variables). A smaller set is injected on **every** themed view.

- **$setting**: Array. Manifest settings keys merged with saved values. A multilang field is already the active language's string. A colour field appears twice: `brand` as hex, `brand_rgb` as `"0, 149, 149"`.
- **$ui_lang · $ui_dir**: Strings. Active language key (`en`) and `ltr`/`rtl`. They belong on the `<html>` element; writing either by hand breaks the right to left packs.
- **$badress · $sadress · $tadress**: Strings with a trailing slash: installation root, shared `resources/`, this theme's directory. Use `$tadress` only outside `assets/`.
- **$template_dir**: String. The theme's **filesystem** path, not a URL. Printing it into markup leaks a server path.
- **$cookie_domain**: String, empty unless the installation shares cookies across subdomains. The theme's cookies must carry this scope.
- **$demo_mode**: Boolean, true only on a demonstration installation.

`$company_name` and `$current_year` are on every client page. Check anything else with `|default:` before printing.

## Reference

### The Manifest: theme.php

A plain array, no class. Five top-level keys; only `engine` changes how the theme is built. Every key is in [Theme Anatomy](https://dev.wisecp.com/en/theme-anatomy).

- **engine**: `'smarty'`, `'twig'` or `'php'`. The only place the engine is declared; it decides the view extension.
- **status**: `'ready'` or `'development'`. A development theme can be previewed but not activated. Omitting the key means ready.
- **update-url**: Where the version check posts. Empty means never checked.
- **meta**: Catalogue entry: `name`, `version`, `author`, `website`, `image`, `description`. Read from here, never from saved values.
- **meta['disabled_routes']**: Route keys the theme does not serve. Those addresses answer 404 and leave the sitemap.
- **settings**: The settings **schema**, below. The admin form is generated from it; values land in `config.php`.

### The Settings Schema

`settings` holds two maps: `groups` is `key => ['label', 'icon']`, `fields` is `key => descriptor`:

- **type**: `switch` · `checkbox` · `color` · `number` · `select` · `textarea`. Anything else appears as a text field.
- **group**: Key of the group the field belongs to. An undeclared group still appears, ungrouped.
- **label · desc · placeholder**: Not literal text: **keys into the theme's own locale file**. An unresolved key prints as itself.
- **default**: Value used until the operator saves one, so the views must be able to print it.
- **options**: Select only, as `value => label key`. Its presence makes the field a dropdown.
- **depends**: Map of `other field => required value`. The row collapses until every condition matches.
- **multilang · rows**: Text and textarea only. `multilang` gives one tab per active language and stores a `lang => value` map. `rows` sizes a textarea, default 4.

### The Theme Object

```php
public static function active(): self;                 // the installation's theme, falling back to Basic
public static function installed(): array;
public static function manifest(string $themeName): array;

public function getName(): string;
public function engine(): string;
public function exists(): bool;
public function dir(): string;                          // filesystem path, trailing separator
public function assetUrl(string $path = ''): string;    // address of a file under assets/
public function viewExists(string $view): bool;         // 'account/dashboard', no extension
public function render(string $view, array $data = []): string;
public function lang(string $key, array $vars = []): string;

public function settingsSchema(): array;                // theme.php → settings
public function savedConfig(): array;                   // config.php → the saved values
public function setting(string $key): mixed;            // saved value, else the schema default
public function allSettings(): array;                   // what the views receive as $setting
public function boot(): void;                            // includes hooks.php, once, before the first render
```

## Example

The two halves of a setting: the schema declares it, a view reads it back under `$setting`.

```php
return [
    'engine' => 'smarty',
    'meta'   => [
        'name'    => 'Acme',
        'version' => '1.0.0',
        'author'  => 'Acme Ltd',
        'image'   => 'cover.png',
    ],
    'settings' => [
        'groups' => [
            'topbar' => ['label' => 'grp_topbar', 'icon' => 'bi-megaphone'],
        ],
        'fields' => [
            'topbar_enabled' => [
                'type'    => 'switch',
                'group'   => 'topbar',
                'label'   => 'set_topbar_enabled',   // a key in locale/{lang}.php
                'default' => false,
            ],
            'topbar_text' => [
                'type'      => 'textarea',
                'group'     => 'topbar',
                'label'     => 'set_topbar_text',
                'default'   => '',
                'rows'      => 3,
                'multilang' => true,
                // The row stays collapsed until the switch above is on.
                'depends'   => ['topbar_enabled' => true],
            ],
        ],
    ],
];
```

```smarty
{if $setting.topbar_enabled}
    <div class="topbar">{$setting.topbar_text nofilter}</div>
{/if}
```

Data a theme needs on every page goes through its own hooks file. The listener gets the template path and the data, and it **must return the data**.

```php
// ONE handler per theme: only the last registration's return value survives.
Hook::add("filter:template.variables", 1, function ($template, $data) {

    // Runs on every website page, so anything with a query goes through the cache.
    $data["footer_groups"] = Cache::remember('website', 'acme_footer_' . Language::selected(), 3600,
        fn (): array => Products::groups());

    return $data;   // returning nothing drops every key the platform collected
});

// Markup, not data: the layout's hook points take a string and print it as-is.
Hook::add("ui:client.head.css", 1, fn () => '<link rel="stylesheet" href="' . Theme::active()->assetUrl('css/extra.css') . '">');
```

## Pitfalls

> **A fix belongs in every theme, not only in yours**
> 
> Themes are siblings, not forks: a defect in one is almost always in the others. Apply the correction across the set. When a core release changes a shipped theme, automatic updating stops on an installation whose active theme is custom. The updates page shows the warning and the operator takes the release by hand. Port the same change into your theme right after.

> **Form protection is the theme's job to wire, not to invent**
> 
> The protections exist in the platform, but a form only gets them if the theme includes them.

> **A template is not a place to keep a secret**
> 
> Both tag engines compile views to plain PHP on disk, literals and conditions included. Keep values and decisions in PHP.

> **A theme registers one variables listener, not several**
> 
> Only the last registration's return value survives, so a second one silently discards the first one's data.

## Related Articles

- [Theme Anatomy](https://dev.wisecp.com/en/theme-anatomy)
- [Your First Theme](https://dev.wisecp.com/en/your-first-theme)
- [Template Variables](https://dev.wisecp.com/en/template-variables)
- [Securing Theme Forms](https://dev.wisecp.com/en/securing-theme-forms)
