pact.SettingsItem gains an additive Controller field, the equivalent of a WinterCMS registerSettings 'url' => Backend::url(...) entry. A link entry declares no Model, Form or NewModel and needs no AdminFS; start-up fails when it combines Controller with a singleton form or a model, or names an unregistered controller. Settings codes stay one namespace. GET /settings lists a link entry with its controller only when the principal passes the item's permissions and may open the controller. Registry.Setting never returns a link entry, so the singleton settings endpoints answer 404 for its code. SettingsEntry carries controller, empty for singletons; the OpenAPI document, generated SPA types and settings fixture follow, and the pact and cabana READMEs and the settings docs describe the link.
107 lines
5.5 KiB
Markdown
107 lines
5.5 KiB
Markdown
---
|
|
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 `<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:
|
|
|
|
```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).
|