Backend

Settings

Declare singleton settings pages with pact.SettingsItem, backed by a model, a fields.yaml form and validation rules, and read them from plugin code.

On this page

A WinterCMS settings model extends SettingModel, stores its values in system_settings and registers its page with registerSettings. In SummerCMS a settings page is a singleton row of the plugin's own table, edited through a form described in fields.yaml, and declared with pact.HasSettings.

Declaring a settings page#

pact.HasSettings returns pact.SettingsItem entries. Each names the page's code, label, category and icon for the settings index, the permissions it needs, the form file inside the plugin's embedded tree, and a factory for its model:

modules/cabana/example_controller_test.go#BlogPlugin.Settings
// Settings replaces registerSettings().
func (p *BlogPlugin) Settings() []pact.SettingsItem {
	return []pact.SettingsItem{{
		Code:        "blog",
		Label:       "acme.blog::lang.settings.label",
		Description: "acme.blog::lang.settings.description",
		Category:    "acme.blog::lang.plugin.name",
		Icon:        "icon-pencil",
		Model:       "BlogSettings",
		Permissions: []string{"acme.blog.access_settings"},
		Form:        "models/settings/fields.yaml",
		NewModel:    func() any { return &BlogSettings{} },
	}}
}

The model is a GORM struct with a Fillable list and a Rules method; cabana refuses at start-up a settings model without either:

modules/cabana/example_controller_test.go#BlogSettings
// BlogSettings is the singleton row behind the plugin's settings page.
type BlogSettings struct {
	ID              uint `gorm:"column:id;primaryKey"`
	PostsPerPage    int  `gorm:"column:posts_per_page"`
	CommentsEnabled bool `gorm:"column:comments_enabled"`
}
modules/cabana/example_controller_test.go#BlogSettings.Rules
// Rules validates a settings save, as a WinterCMS settings model's $rules.
func (BlogSettings) Rules() map[string]string {
	return map[string]string{"posts_per_page": "required|integer|between:1,100"}
}

The form uses the same field types and options as a controller form; see Forms:

modules/cabana/testdata/docs/models/settings/fields.yaml
fields:
    posts_per_page:
        label: acme.blog::lang.settings.posts_per_page
        type: number
        span: left
        default: 10
    comments_enabled:
        label: acme.blog::lang.settings.comments_enabled
        type: switch
        span: right

Create the table in a migration like any other; see Migrations.

How values are stored#

The settings row is the one with ID 1. Before it exists, the page shows the fields' default values and the API reports that the row does not exist yet; the first save creates it. A save writes only fillable fields, validates them with the model's rules and the form's required flags, and runs in a transaction, as a controller form save does.

The admin API serves the page at <prefix>/api/v1/settings/{code} (values) and .../settings/{code}/schema (the form), and GET .../settings lists the pages the administrator may open.

Reading settings in plugin code#

Settings are an ordinary table, so plugin code reads them with GORM:

modules/beachcomber/example_test.go#gate
svc.SetGate(beachcomber.GateFunc(func(ctx context.Context, db *gorm.DB) bool {
	var enabled bool
	err := db.WithContext(ctx).Raw(`SELECT search_enabled FROM acme_blog_settings WHERE id = 1`).Scan(&enabled).Error
	return err == nil && enabled
}))

That example reads a flag from a settings row inside a search gate, treating a failed read as off. A setting that changes rarely and that operators, not administrators, own belongs in configuration instead; see Configuration.