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())
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
// 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.