A settings link to a singleton controller only worked on a plain left click; a new tab landed on the list route, which answers 404, and every non-singleton link paid an extra /singleton request. The settings index now links like any controller link, and the list view opens the controller's one record when its list answers 404 and the singleton route answers an id.
6.2 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Settings | 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. | backend | 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:
// 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:
// 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"`
}
// 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:
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.
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:
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 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.
A link entry may also open a singleton controller, one whose pact.AdminSingleton record the admin opens directly in its form, the way WinterCMS plugins auto-created one definition and redirected to it. The entry stays a link to the controller's list route: since a singleton controller has no list, the admin asks its singleton route for the record and opens it, also when the link is opened in a new tab.
In the admin, the entry's card on the Settings page opens the controller's pages. When the controller has no main navigation item of its own, the admin treats it as part of Settings: the rail marks Settings as current and the breadcrumbs read Settings, then the entry's label.
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:
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.