Theme Settings

3.1k переглядів Markdown

Declare the settings a theme accepts in its manifest. The admin form, the validation and the saved file follow. The view reads the result as one variable.

Overview

A theme is data, not a class: it declares what it can be configured with, never how that is edited. The panel validates each field by its declared type and writes the values into the theme's own directory.

That is the boundary of the contract. A theme adds a field and gets a control, but does not decide the form's plumbing. A value outside the schema is never stored.

Structure

Two Files, Two Owners

theme.php Schema, defaults, and the theme's name, version and engine. Shipped with the theme, never written at runtime.
config.php The saved values only: a flat map written by the panel, absent until the first save and kept out of version control.
Reading The saved value if there is one, otherwise the schema default.
The form Generated from the schema. A theme wanting more ships its own admin markup, and is handed the builder and the saved values.

The Shape of the Schema

theme.php, the settings block
$manifest['settings'] = [
    // key => the group's heading and icon. The label is a LOCALE KEY of this theme.
    'groups' => [
        'topbar' => ['label' => 'grp_topbar', 'icon' => 'bi-megaphone'],
    ],
    // key => a descriptor. The key is the POST name, the config key and the name the
    // view reads, all at once: renaming it orphans whatever was already saved.
    'fields' => [
        'topbar_enabled' => ['type' => 'switch', 'group' => 'topbar', 'label' => 'set_topbar_enabled', 'default' => false],
    ],
];

Step by Step

Declare a Group

  1. Add an entry to the groups map with a heading and an icon class.
  2. Put the heading in the theme's own locale file, both languages, and reference it by key. An untranslated key prints as itself.
  3. A field whose group is not declared still appears, ungrouped.

Declare a Field

  1. Pick the type from the list below. It decides the control, the validation and what is stored.
  2. Give it a real default, not an empty placeholder. That is the value on a fresh install and after every reset.
  3. Add desc for a help sentence, and depends when the field only makes sense while another is on.
  4. Reload the configure page; the control is there, no other file touched.

Read It in a View

  1. Read it off the settings variable by key. It is a variable and not a function so that it works inside a condition.
  2. For a colour, read the property value and its _rgb twin wherever a translucent version is needed.
  3. From PHP, ask the theme object for one setting by key, without loading the whole map.

Take Over the Admin Form

  1. Add an admin-settings.php to the theme directory; it replaces the generated form entirely.
  2. Build the markup with the form builder, saved values and schema you are handed. Return the HTML as a string.
  3. Keep field names identical to the schema keys. A control named anything else is posted and discarded.
  4. You do not have to print the whole schema: a field you leave off the form keeps its stored value.

Print a checkbox through the form builder

An unticked checkbox posts nothing at all, so the form builder prints a hidden zero of the same name before the box; since saving asks whether a key arrived, a switch cannot be turned off again without it. If you hand-write the checkbox, print that hidden field yourself.

A theme still under development can be configured too: the gear button on its card (shown even while activation is closed), or the address settings/theme?group=theme&theme={Theme} directly.

Reference

Field Types

TypeControlStored asValidation on save
switch, checkboxCheckbox; desc is the label beside itBooleanSet means true, absent means false
selectDropdown built from optionsThe chosen keyMust be a key of options, else the default
numberNumber inputIntegerCast to integer; non-numeric becomes zero
decimalNumber input with stepFloatA comma is read as a decimal point; a value outside min/max is clamped to the bound
date, timeDate or time pickerYYYY-MM-DD / HH:MMMust be a real date and a real time; 2026-02-29 and 25:99 fall back to the default
multiselectMultiple-choice listArray of selected keysOnly keys from options are stored; clearing them all is allowed
imageFile picker with a preview and a remove boxThe stored file nameValidated as an image; picking no new file keeps the current one
groupThe fields sub-fields on one rowNested array, one levelEach sub-field is validated by its own type; sub-fields are scalars
colorColour picker plus a typeable hex boxHex string with the leading hashThree to eight hex digits, else the default
textareaTextarea sized by rowsString, or a language map when multilingualTags stripped unless html is on, then allowlist-filtered
anything else, or a missing typeText inputString, or a language map when multilingualSame as a textarea

What a Descriptor Accepts

type One of the types above; absent means a text field.
group The group this field belongs to. An undeclared group leaves it ungrouped.
label A locale key of this theme, not literal text. Resolved in the operator's language, then English, then the key.
desc A locale key for the help line. On a checkbox it becomes the label beside the box instead.
placeholder A locale key for the hint inside a text control. Not a label; the field still needs one.
default The value until the operator saves one, and the fallback when a posted value fails validation. An empty colour makes the derived rgb twin three zeroes.
options Select only, as stored value to label key: ['rail' => 'opt_rail', 'card' => 'opt_card']. Also the allowlist the saved value is checked against.
depends A map of another field to the value it must hold: ['topbar_enabled' => true]. The row collapses until every condition matches, on load and as the operator types.
multilang Text and textarea only. One tab per active language, stored as a language map. Views receive a plain string.
rows Height of a textarea, defaulting to four. Ignored by every other type.
html · allowed_tags Keeps markup in a text value instead of stripping it. The default is the page-body allowlist (headings, lists, links, buttons, images, embeds from named hosts; no scripts, event handlers or forms); allowed_tags can narrow it, never widen it. Print the value with {content var=…}.
from_logo Colour only. Marks the field as a brand colour: 0 is the primary, 1 the secondary (a colour field named text_color is the body text colour). The value lives in the installation's Site Colors, shared with the client area and the email templates, and never in config.php. The "pick colours from the logo" button fills it from the site logo: the primary takes a readable shade of the logo's dominant colour family, the secondary the next colour that differs clearly in hue. Neither index is a role. The panel does not know whether your theme uses a colour as an accent or as a surface, so a logo whose dominant colour is dark produces a dark primary. If your theme needs a particular role, normalise the pair in the theme itself, for example by checking lightness and contrast and adjusting the colours in your CSS.

The Settings API

signatures
// The manifest's settings block, exactly as declared (groups + fields).
public function settingsSchema(): array;

// One value: the saved one if the key was ever saved, otherwise the schema default,
// otherwise null. This is how the platform reads a single theme capability.
public function setting(string $key): mixed;

// The saved values only (config.php). Empty until the first save.
public function savedConfig(): array;

// Every schema field merged with its value : what the view receives as $setting.
// A multilingual value is reduced to the ACTIVE language here, and a colour field
// also produces a "{key}_rgb" entry.
public function allSettings(): array;

// A hex colour to the comma-separated triplet an rgba() expression needs. Short forms
// are expanded and a short value is padded, so an empty string yields three zeroes
// rather than an error. This is what produces the "{key}_rgb" entries above.
public static function hexToRgb(string $hex): string;

Example

A group with four fields, the saved file it produces and the view that reads it back. One reason to show them together: the schema key, the file key and the view name are the same string.

templates/website/Acme/theme.php
return [
    'engine' => 'smarty',
    'status' => 'ready',
    'meta'   => [
        'name'    => 'Acme',
        'version' => '1.0.0',
        'author'  => 'Acme Ltd',
        'image'   => 'cover.png',
    ],
    'settings' => [
        'groups' => [
            'brand'    => ['label' => 'grp_brand', 'icon' => 'bi-palette'],
            'topbar'   => ['label' => 'grp_topbar', 'icon' => 'bi-megaphone'],
            'checkout' => ['label' => 'grp_checkout', 'icon' => 'bi-cart3'],
        ],
        'fields' => [
            // A brand colour. `from_logo` is its index in the palette the panel extracts
            // from the site logo, so the "pick colours from the logo" button fills it.
            // The default is a hash plus six hex digits (the picker's own format) —
            // replace the placeholder with this theme's real colour. Leave it empty and
            // the derived primary_color_rgb reads as three zeroes, which paints every
            // translucent surface built from it black.
            'primary_color' => [
                'type'      => 'color',
                'group'     => 'brand',
                'label'     => 'set_primary_color',
                'from_logo' => 0,
                'default'   => '#RRGGBB',
            ],

            // The gate. Everything below it depends on this one.
            'topbar_enabled' => [
                'type'    => 'switch',
                'group'   => 'topbar',
                'label'   => 'set_topbar_enabled',
                'desc'    => 'set_topbar_enabled_desc',
                'default' => false,
            ],

            // Per-language, and allowed to carry markup: an announcement usually has a
            // link in it. The stored value is a map; the view still gets a string.
            'topbar_text' => [
                'type'      => 'textarea',
                'group'     => 'topbar',
                'label'     => 'set_topbar_text',
                'default'   => '',
                'rows'      => 3,
                'multilang' => true,
                'html'      => true,
                'depends'   => ['topbar_enabled' => true],
            ],

            // The keys are what gets stored; the values are locale keys of this theme.
            'checkout_sidebar' => [
                'type'    => 'select',
                'group'   => 'checkout',
                'label'   => 'set_checkout_sidebar',
                'options' => [
                    'rail'  => 'opt_checkout_rail',
                    'card'  => 'opt_checkout_card',
                    'stack' => 'opt_checkout_stack',
                ],
                'default' => 'rail',
            ],

            'popup_width' => [
                'type'    => 'number',
                'group'   => 'checkout',
                'label'   => 'set_popup_width',
                'default' => 500,
            ],
        ],
    ],
];
templates/website/Acme/config.php
return [
    // Stored with the leading hash, exactly as the picker posted it. Note that EVERY
    // schema field is written on every save, whether the operator touched it or not.
    'primary_color'    => '#RRGGBB',
    'topbar_enabled'   => true,
    // Multilingual: stored per language, one entry per ACTIVE language at save time.
    'topbar_text'      => [
        'en' => 'Free migration this month.',
        'tr' => 'Bu ay ücretsiz taşıma.',
    ],
    'checkout_sidebar' => 'card',
    'popup_width'      => 520,
];
the view that reads it
{* A boolean reads directly inside a condition, which is why this is a VARIABLE and
   not a function: a registered function cannot be called from {if}. *}
{if $setting.topbar_enabled}
    {* Markup was kept on save. {content} filters it again as it prints; nofilter would not. *}
    <div class="topbar">{content var=$setting.topbar_text}</div>
{/if}

{* A select is just its stored key: use it as a class rather than branching four times. *}
<div class="checkout-layout-{$setting.checkout_sidebar}">

{* A colour field also produces an _rgb twin, for a translucent version of the same colour. *}
<style>
    :root {
        --acme-brand: {$setting.primary_color};
        --acme-brand-rgb: {$setting.primary_color_rgb};
    }
</style>

Pitfalls

Renaming a key orphans what was already saved

The key is the form name, the stored key and the name the view reads. A value under the old name is never read again and never cleaned up. Every installation that had configured the theme falls back to the default.

The saved file is generated, not source

It holds values and nothing else. Name and version stay in the manifest, where the theme listing and the upgrade check read them. Shipping one in a theme package hands your test configuration to everyone who installs it.

A multilingual value is a map in the file and a string in the view

PHP asking for the raw setting gets the whole map. The view gets the active language resolved. Code confusing the two works in exactly one place.

A collapsed dependent field is still saved and still read

The dependency hides the row; it does not disable the value. A view reading a dependent setting must check the field it depends on too. Otherwise it shows something the operator believes is off.

Ця стаття була корисною?

Дякуємо за відгук!

Досі потрібна допомога?

Наша служба підтримки на зв’язку цілодобово з усього, чого ви не знайшли вище.