Files
summercms/docs/services/configuration.md
Jakub Zych 5b09b32512 feat(diary): channels, components and config/log.yaml
- stderr and null drivers; per-component levels from log.components
- SUMMER_LOG__ overrides parse as strings and overlay the defaults key by key
- Validation names the key: driver, format, level, days, stack children and cycles, daily path outside the root
- summer new writes config/log.yaml (stack of console and daily, river warn, http info)
- docs/services/logging.md: channels, config/log.yaml, component loggers, daily files, redaction
- Log keys in the configuration docs; ExampleFor backs the docs snippet
2026-10-21 11:48:25 +02:00

98 lines
4.7 KiB
Markdown

---
title: Configuration
description: Read layered configuration with compass, from plugin defaults through per-environment files and SUMMER_ variables to runtime overrides saved to disk.
section: services
order: 10
---
# Configuration
WinterCMS reads configuration with `Config::get('app.name')` from `config/*.php`, per-environment directories and `.env`. SummerCMS reads it from a `compass.Config` built by [compass](../../modules/compass/README.md), with the same dot paths. [Setup, Configuration](../setup/configuration.md) lists the keys an application sets; this page covers how the layers merge and how code reads and changes them.
## Layers
`compass.Config` merges its sources in a fixed order. Each layer overrides the ones before it:
1. Plugin defaults, merged with `compass.Config.MergePlugin` when the plugin is activated. A plugin's `config/config.yaml` becomes `<plugin id>.<key>`, so `acme.blog.posts_per_page` is the WinterCMS `acme.blog::posts_per_page`. Any other `config/<name>.yaml` becomes `<plugin id>.<name>.<key>`.
2. `config/*.yaml`. Each file is a section named after the file, so `config/app.yaml` provides `app.*`.
3. `config/env/<environment>/*.yaml`, the per-environment sections.
4. `SUMMER_` environment variables and the `.env` file next to `config/`. `SUMMER_MAIL__DRIVER` sets `mail.driver`: the prefix is removed, `__` separates the path segments and the name is lower-cased. A `.env` value applies only when the real environment does not set the same variable. Values arrive as strings; modules that expect numbers or lists either parse them (`SUMMER_LOG__CHANNELS__DAILY__DAYS=30` and `SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,daily` work for the [logging](logging.md) keys) or document that the key belongs in YAML.
5. `config/env/<environment>/overrides.yaml`, written by `compass.Config.Persist`.
6. Values set in memory with `compass.Config.Set`.
The environment comes from `SUMMER_ENV` and defaults to `production`. Its name may contain only letters, digits, `-` and `_`.
## Reading values
The typed getters `compass.Config.String`, `compass.Config.Int` and `compass.Config.Bool` return the zero value for a missing key. Use `compass.Config.Lookup` or `compass.Config.Has` when a missing key must be told apart from a zero value, and `compass.Config.LoadSection` to read a whole section into a struct with `koanf` tags:
```go src=modules/compass/example_test.go#ExampleOpen
dir, err := os.MkdirTemp("", "acme-config")
if err != nil {
fmt.Println(err)
return
}
defer os.RemoveAll(dir)
if err := writeConfig(dir); err != nil {
fmt.Println(err)
return
}
// Environ stands in for the process environment (nil reads os.Environ).
cfg, err := compass.Open(compass.Options{
Dir: dir,
Env: "development",
Environ: []string{"SUMMER_MAIL__DRIVER=smtp"},
})
if err != nil {
fmt.Println(err)
return
}
// A plugin's embedded config/config.yaml becomes its defaults.
plugin := fstest.MapFS{"config/config.yaml": {Data: []byte("posts_per_page: 10\n")}}
if err := cfg.MergePlugin("acme.blog", plugin); err != nil {
fmt.Println(err)
return
}
fmt.Println(cfg.String("app.name"), cfg.Bool("app.debug"), cfg.Int("acme.blog.posts_per_page"))
var mail mailSettings
if err := cfg.LoadSection("mail", &mail); err != nil {
fmt.Println(err)
return
}
fmt.Println(mail.Driver, mail.From)
_, found := cfg.Lookup("app.timezone")
fmt.Println(cfg.Environment(), found)
// A runtime override, saved to env/development/overrides.yaml.
if err := cfg.Set("acme.blog.posts_per_page", 25); err != nil {
fmt.Println(err)
return
}
if err := cfg.Persist(); err != nil {
fmt.Println(err)
return
}
saved, _ := os.ReadFile(filepath.Join(dir, "env", "development", "overrides.yaml"))
fmt.Print(string(saved))
// Output:
// Acme true 10
// smtp blog@example.com
// development false
// acme:
// blog:
// posts_per_page: 25
```
The application opens its configuration once with `compass.Load("config")` in the generated `main` and hands it to the `backpack.App`. Plugins read it through `app.Config` and never open their own.
## Changing values at runtime
`compass.Config.Set` changes a value in memory. `compass.Config.Persist` saves every value set at runtime to `config/env/<environment>/overrides.yaml`, keeping the keys already saved there. It replaces the file atomically, creates it readable only by its owner and refuses any path outside the config directory. `compass.Config.Reload` rereads every source and discards values that were set but not persisted.
Values that admins edit in the backend are not configuration: settings pages store them in a database row; see [Settings](../backend/settings.md).
> [!WARNING]
> `overrides.yaml` is written by the running application. Keep it out of version control, and keep secrets in environment variables rather than in values the application persists.