Module Configuration

3 views Markdown

Settings an operator can change: the array a module ships with, the field descriptors that become a form, and the write that saves the answers.

Overview

A module's configuration is one PHP file that returns an array. It holds both the defaults you ship and the operator's saved answers, because saving rewrites that same file. There is no settings table and no migration step: the file is the state.

Two properties of that file drive most of the mistakes here: it is compiled, and it is readable source.

Prerequisites

  • A module that already loads. If you have none, start from Your First Module.
  • Write permission on the module directory for the web server user; without it every save fails silently at the file layer.
  • How a form field's name becomes a request key, from The Admin Form Builder.

Structure

The Shape of the File

The top level is yours apart from a handful of keys the platform reads, which have to be spelled exactly.

KeyRead byWhat it holds
meta.nameThe module listA display name used when the language file has none
meta.versionYou, and the update machineryYour own version string
meta.logoLogo resolutionA file name inside the module directory, or an absolute address
settingsYour code, and the settings save pathThe operator's answers; every declared field lands here under its own key
statusThe registry, on status-filtered loadsWhether the module is enabled, for the four types that store it here
fieldsServer modules, on the product and service screensField descriptors shown on the product configuration form
access_psThe addon settings screenThe privilege selection saved alongside the settings
show_on_adminArea, show_on_clientAreaThe addon page routerWhether the addon opens a panel page, a customer page, or both
config.php, as shipped
return [
    'meta' => [
        'name'    => 'AcmeDomains',
        'version' => '1.0',
        'logo'    => 'logo.png',
    ],

    // Ship every key your code reads, with a safe default. A key that only appears
    // after the first save is a key your code has to guard on every read.
    'settings' => [
        'username'      => '',
        'apiKey'        => '',
        'test-mode'     => 0,
        'nameservers'   => ['ns1.example.com', 'ns2.example.com'],
        'cost-currency' => 4,
    ],
];

Declaring the Settings Fields

You do not write the form: you return an array of descriptors and the admin form builder turns it into one. The array key is the field name; the name entry inside it is the label. That pair is the most common thing to get backwards.

Which method you declare, and whether it is handed anything, depends on the type.

TypeMethod you declareWhat the screen passesWhere the saved value comes from
Registrarsconfig_fields($settings = [])The settings block of the current configurationThe argument
Paymentconfig_fields()Nothing$this->config['settings']
Addonsfields()Nothing$this->config['settings']
the descriptor array
// The registrar form. The screen calls this with the saved settings block, so
// $data is populated. On a payment gateway or an addon the same method is called
// with NO argument, and $data would silently stay empty: read the property there.
public function config_fields($data = []): array
{
    return [
        // KEY is the field name. 'name' is the LABEL.
        'username' => [
            'name'        => $this->lang['username'] ?? 'Username',
            'description' => $this->lang['username-desc'] ?? '',
            'type'        => 'text',
            'value'       => $data['username'] ?? '',
            'placeholder' => 'api-user',
        ],

        'apiKey' => [
            'name'  => $this->lang['api-key'] ?? 'API Key',
            'type'  => 'password',
            'value' => $data['apiKey'] ?? '',
        ],

        // A checkbox. 'checked' is the current state, not the submitted value.
        'test-mode' => [
            'name'    => $this->lang['test-mode'] ?? 'Test Mode',
            'type'    => 'approval',
            'checked' => (bool) ($data['test-mode'] ?? false),
        ],

        // Shown only while the checkbox above is ticked.
        'test-endpoint' => [
            'name'         => $this->lang['test-endpoint'] ?? 'Test Endpoint',
            'type'         => 'text',
            'value'        => $data['test-endpoint'] ?? '',
            'parent'       => 'test-mode',
            'parentEffect' => 'hide',
        ],

        'mode' => [
            'name'    => $this->lang['mode'] ?? 'Mode',
            'type'    => 'dropdown',
            'value'   => $data['mode'] ?? 'live',
            'options' => ['live' => 'Live', 'sandbox' => 'Sandbox'],
        ],
    ];
}

Walkthrough

Ship the Defaults

  1. Create config.php returning an array with a meta block and a settings block.
  2. Put every key your code reads into settings, with an empty or harmless default. Never ship a real credential.
  3. Reload the module list; the settings screen now has something to show.

Declare the Form

  1. Add config_fields($data = []) to your class, or fields() if you are writing an addon.
  2. Return one descriptor per setting, keyed by the setting name, with the current value read from the source your type provides.
  3. Open the module's settings page. The generic template finds your method and builds the form, submit button and action address included.
  4. Change a value and save. The answers arrive under a single request key, fields, keyed by your field names.

Write It Back

  1. Merge the posted values into the loaded array rather than replacing it: the write is a full-file write, so anything you drop is gone.
  2. Encrypt secrets on the way in, and keep the stored value when the field arrives masked or empty.
  3. Write through the file manager, which invalidates the compiled copy.
  4. Reload. If you read back the old value, the write went through but the compiled copy did not.

Reference

Writing the File

signatures
// The shared trait, used by server, registrar, product and social login modules.
// $auto_status = true turns the module on when the array carries a non-empty settings block.
protected function save_config($data = [], $auto_status = true);

// Server modules narrow it: no auto-status flag, and a strict boolean return.
public function save_config($data = []): bool;

// Addons narrow it the same way, and also assign the array to $this->config.
public function save_config($data = []): bool;

// What all of them call underneath. It invalidates the compiled copy of any .php target.
public static function file_write($file, $data = null, $mode = 'w', $flags = 0);

// Array to source. ['pwith' => true] wraps it as a complete PHP file.
public static function array_export($array = [], $options = []);

// Platform configuration, not module configuration. Slash paths into the files
// under the configuration directory.
public static function get($arg = null);
public static function set($key, $values, $merge = false): array|false;
public static function save($name = '', $data = []): bool;

// Database-backed settings, keyed by name. A module admin area uses these
// instead of its own file.
public static function getd($name = '');
public static function setd($name = '', $content = '');

Field Descriptor Keys

type One of text (the default), password, textarea, dropdown, radio, switch, approval, file, output and javascript. An output field prints free markup and is never saved.
name The label shown beside the field. Not the field name: that is the array key.
value The current value for text-like and dropdown fields, and the submitted value for a switch. Checkboxes use checked for their state instead.
options A value => label map for a dropdown or a radio group. A comma-separated string is accepted and expanded into a map with identical keys and labels.
description · description_pos · is_tooltip Help text, on the right by default and beside the label with 'L'. With is_tooltip it collapses into a question-mark icon.
parent · parentEffect · parentValue Show or disable this field based on another one. The effect is hide, disable or collapse; with a radio parent, parentValue lists which options reveal it. The parent must be declared before the child.
width · wrap_width Percentages for the input itself and for its row. A wrap width of one hundred is treated as unset.
advanced_selector · multiple · rows · disabled A searchable dropdown, a multi-value field, the height of a textarea, and a read-only field. The searchable dropdown is the one that can load its options from one of your own methods.
fieldOptions · rowOptions Passed straight through to the form builder for that field and its row. This is the escape hatch for attributes the descriptor has no key for.

Example

The full round trip: what arrives, what is written, and what your code reads back. The saving half is an override of the settings controller, so the merge is visible.

saving, with the merge and the secret handled
public function controller_settings($extraFields = []): array
{
    // Everything the form posted, under one key, named after your descriptor keys.
    $fields = \Filter::POST("fields") ?: [];

    // Start from what is already on disk: the write below replaces the whole file.
    $config = $this->config;

    $config['settings']['username']  = \Filter::html_clear((string) ($fields['username'] ?? ''));
    $config['settings']['mode']      = in_array($fields['mode'] ?? '', ['live', 'sandbox'], true)
        ? $fields['mode'] : 'live';

    // An unticked checkbox is ABSENT from the post, so absence is the value "off".
    $config['settings']['test-mode'] = (int) ($fields['test-mode'] ?? 0) === 1 ? 1 : 0;

    // Secrets: encrypt on the way in, and keep the stored value when the field came
    // back masked or empty, which is what the screen sends for an unchanged secret.
    $posted = (string) ($fields['apiKey'] ?? '');
    if ($posted !== '' && !str_starts_with($posted, '*'))
        $config['settings']['apiKey'] = $this->encode_str($posted);

    // Full-file write, through the manager that invalidates the compiled copy.
    \FileManager::file_write($this->dir . 'config.php', \Utility::array_export($config, ['pwith' => true]));

    return ['status' => "successful", 'message' => \Language::gc("admin/ac-settings/successful1")];
}
reading it back, from anywhere in the module
private function credentials(): array
{
    $settings = $this->config['settings'] ?? [];

    return [
        // Null-safe on every read: a key can be missing on an installation that
        // upgraded from an older version of your module.
        'username' => (string) ($settings['username'] ?? ''),
        'apiKey'   => $this->decode_str((string) ($settings['apiKey'] ?? '')),
        'sandbox'  => (int) ($settings['test-mode'] ?? 0) === 1,
    ];
}

The same values read without building the module, which is what a listing screen or a hook does:

from outside the module
// Config() reads the static cache only, so the load has to happen first.
// The third argument keeps the class file out of it.
$record = Modules::Load("Registrars", "AcmeDomains", true);

$mode = $record["config"]["settings"]["mode"] ?? 'live';

// The secret is NOT readable from here: decoding is a method on the instance.
// If you need the plain value, build the module and ask it.

Pitfalls

A configuration file is compiled PHP

It is read back with an include, so the compiled copy is served until something invalidates it. The file manager does; a raw write or a rename does not. The operator saves, reloads, sees the old value, and no log explains it.

Saving replaces the whole file

Build the new array from the one already loaded, change the keys you own and leave the rest alone. Passing only the settings block wipes the metadata, the status and everything else in one save.

A field that was not posted can be written as false

The addon settings path walks your declared fields and stores false for every one the post did not contain. An unticked checkbox sends nothing, so that is right for checkboxes and wrong for anything shown conditionally. Derive the fields you declare from the same source you read, never a hand-written list.

Encrypt secrets, and never paste one in by hand

An API key belongs in the array encrypted, through the module's own helper, so the sub-key bound to this installation is used. A value typed straight into the file cannot be decrypted and reads as garbage; enter it through the settings screen.

Module configuration and platform configuration are different things

The module file is yours and travels with the module. The platform configuration files hold installation-wide settings, including which module of each single-choice type is active. A module writes only to its own file, and reads the platform files.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.