--- title: Settings description: Declare settings pages with pact.SettingsItem, as singleton forms backed by a model or as links to admin controllers, and read settings from plugin code. section: backend order: 60 --- # Settings 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: ```go src=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](../../modules/cabana/README.md) refuses at start-up a settings model without either: ```go src=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"` } ``` ```go src=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](forms.md): ```yaml src=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](../database/migrations.md). ## Linking a settings entry to an admin controller A WinterCMS `registerSettings` entry can point at a backend controller instead of a settings model, with `'url' => Backend::url('acme/blog/posts')`. The entry appears on the Settings page and opens that controller's list. In SummerCMS the `Controller` field of `pact.SettingsItem` does the same: set it to the admin controller ID (`vendor.plugin.controller`) and leave `Model`, `Form` and `NewModel` empty. The plugin returns this entry from `Settings` next to, or instead of, its singleton pages: ```go src=modules/cabana/example_settings_test.go#settings-link link := pact.SettingsItem{ Code: "posts", Label: "acme.blog::lang.posts.title", Description: "acme.blog::lang.posts.description", Category: "acme.blog::lang.plugin.name", Icon: "icon-pencil", Order: 500, Permissions: []string{"acme.blog.access_settings"}, Controller: "acme.blog.posts", } ``` A link entry is not a singleton and needs no embedded admin tree. [cabana](../../modules/cabana/README.md) stops start-up when a link entry also declares `Model`, `Form` or `NewModel`, when it names a controller no plugin registered, or when its code repeats another settings code; singleton and link entries share one code namespace. `GET .../settings` lists a link entry with its `controller` set (and `model` empty) only when the administrator passes the entry's permissions and may also open the controller, so a controller the administrator cannot reach is never advertised. Singleton entries carry an empty `controller`. The singleton endpoints (`.../settings/{code}`, `.../settings/{code}/schema`) answer 404 for a link entry's code. ## 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 `/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: ```go src=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](../services/configuration.md).