The Theme Engine

8 vues Markdown

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.

layout
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. 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.

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

coremio/classes/Theme.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.

templates/website/Acme/theme.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],
            ],
        ],
    ],
];
the view that reads it back
{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.

templates/website/Acme/hooks.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.

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.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.