Module Configuration
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.
| Key | Read by | What it holds |
|---|---|---|
meta.name | The module list | A display name used when the language file has none |
meta.version | You, and the update machinery | Your own version string |
meta.logo | Logo resolution | A file name inside the module directory, or an absolute address |
settings | Your code, and the settings save path | The operator's answers; every declared field lands here under its own key |
status | The registry, on status-filtered loads | Whether the module is enabled, for the four types that store it here |
fields | Server modules, on the product and service screens | Field descriptors shown on the product configuration form |
access_ps | The addon settings screen | The privilege selection saved alongside the settings |
show_on_adminArea, show_on_clientArea | The addon page router | Whether the addon opens a panel page, a customer page, or both |
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.
| Type | Method you declare | What the screen passes | Where the saved value comes from |
|---|---|---|---|
| Registrars | config_fields($settings = []) | The settings block of the current configuration | The argument |
| Payment | config_fields() | Nothing | $this->config['settings'] |
| Addons | fields() | Nothing | $this->config['settings'] |
// 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
- Create
config.phpreturning an array with ametablock and asettingsblock. - Put every key your code reads into
settings, with an empty or harmless default. Never ship a real credential. - Reload the module list; the settings screen now has something to show.
Declare the Form
- Add
config_fields($data = [])to your class, orfields()if you are writing an addon. - Return one descriptor per setting, keyed by the setting name, with the current value read from the source your type provides.
- Open the module's settings page. The generic template finds your method and builds the form, submit button and action address included.
- Change a value and save. The answers arrive under a single request key,
fields, keyed by your field names.
Write It Back
- 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.
- Encrypt secrets on the way in, and keep the stored value when the field arrives masked or empty.
- Write through the file manager, which invalidates the compiled copy.
- Reload. If you read back the old value, the write went through but the compiled copy did not.
Reference
Writing the File
// 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
text (the default), password, textarea, dropdown, radio, switch, approval, file, output and javascript. An output field prints free markup and is never saved.
checked for their state instead.
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.
'L'. With is_tooltip it collapses into a question-mark icon.
hide, disable or collapse; with a radio parent, parentValue lists which options reveal it. The parent must be declared before the child.
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.
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")];
}
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:
// 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
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.
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.
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.
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.
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.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.