Per-Module Site Settings

Introduction

Every module inevitably ends up with a handful of small, per-site-configurable knobs — how many posts per page, how long to retain spam comments, what a rate limit should be. Rather than have each module hand-roll its own settings controller and view, App::registerModuleSettings(string $moduleId, array $schema) lets a module declare its settings the same declarative way it already declares a model's admin form via getConfig(), and get a fully-rendered, persisted settings page for free.

Values are stored per-site, not globally, inside a single sites.settings TEXT column (JSON-encoded, one top-level key per module) — so a multi-tenant install can have completely different Blog pagination or Artworks curation settings per tenant without any extra schema.

Registering a Schema (Blog Module.php::init())

Registering a Schema (Blog Module.php::init()) php
App::registerModuleSettings('blog', [
    'default_comment_status' => [
        'type' => 'select',
        'label' => 'Default Comment Status',
        'options' => ['pending' => 'Pending Review', 'approved' => 'Auto-Approved'],
        'default' => 'pending',
        'required' => true,
    ],
    'spam_retention_days' => [
        'type' => 'number',
        'label' => 'Spam/Rejected Comment Retention(Days)',
        'default' => 7,
        'required' => true,
        'min' => 1,
        'helper_text' => 'Rejected or spam-flagged comments older than this are permanently purged automatically.',
    ],
    'posts_per_page' => [
        'type' => 'number',
        'label' => 'Blog Posts Per Page',
        'default' => 6,
        'required' => true,
        'min' => 1,
    ],
]);

Schema Shape & Automatic Validation

Each field mirrors getConfig()'s shape: type (text|number|select|checkbox|textarea), label, default, required, options (for select), and helper_text. Number fields additionally accept min/max bounds, which the generic ModuleSettingsController enforces server-side by clamping any out-of-range submission — this closes off a whole class of bug at the source: a 0 or negative rate-limit-seconds value silently disabling rate limiting entirely, or a negative pagination size producing a SQL LIMIT -5 syntax error. Registering a schema with a sensible min is enough; no controller ever needs its own bounds-checking code.

Reading a Setting Back

Reading a Setting Back php
// Inside any controller/model that needs the current site's value:
$postsPerPage = (int)App::getModuleSetting('blog', 'posts_per_page', 6);

// Equivalent, when you already have a Site instance in hand:
$postsPerPage = (int)$site->getModuleSetting('blog', 'posts_per_page', 6);

The Generated Admin Page

Registering a schema automatically adds a sidebar link and a working settings page at /admin/settings/{moduleId} — restricted to super_admin, CSRF-protected, and rendered/validated entirely by the shared ModuleSettingsController and its generic view. A module never writes its own settings controller or template.