4.0 KiB
title, date, context
| title | date | context |
|---|---|---|
| Plugin file layout — WinterCMS directories as Go subpackages, models is a leaf | 2026-09-18 | /gsd-explore during Phase 4 discussion (scaffold stub layout question); binds every plugin port (golem15, jz, pxpx) |
Plugin file layout
Decision
A Go plugin mirrors the WinterCMS plugin directory layout. Each PHP directory becomes a Go package (or an embedded asset directory) under the plugin module root:
| WinterCMS | Go plugin | Kind |
|---|---|---|
Plugin.php |
plugin.go (root package) |
registration; imports the leaves |
routes.php |
routes.go (root package) |
surf group builder |
models/ |
models/ |
package, leaf (see rule) |
classes/ |
classes/ |
package, services, hooks, casts that need services |
controllers/ |
controllers/ |
package, HTTP handlers and admin controllers |
console/ |
console/ |
package, bonfire.Command implementations |
jobs/ |
jobs/ |
package, framework Job interface (River from Phase 11) |
middleware/ |
middleware/ |
package, named middleware |
updates/ |
updates/ |
package, the plugin's gormigrate set |
lang/<locale>/*.yaml |
lang/<locale>/*.yaml |
embedded assets (HasLang) |
views/mail/ |
views/mail/ |
embedded assets (HasMailTemplates) |
config/ |
config/ |
embedded assets (HasConfig) |
Chosen because conversion is mostly done by agents across many plugins: models/Album.php → models/album.go is a mechanical move and the agent's mental map of the plugin stays 1:1 with the PHP source. The alternative (flat single package, which the Phase 3 fonoteka.go plugins use) removes all intra-plugin imports but breaks the directory correspondence that conversion leans on.
The rule: models/ is a leaf package
models/ never imports a sibling package of its own plugin. Consequences for the converter:
- Casts move into
models/. PHP filesclasses/casts/*(e.g. FonotekaMarketPriceCast) are model concerns filed elsewhere; in Go they live inmodels/(or in the frameworklagoonwhen generic). - Service-calling lifecycle hooks move out. A PHP model hook that reaches into
classes/(e.g.Album::beforeSaveusingArtistResolver) becomes a GORM callback registered on the model fromclasses/orplugin.go— the same callback-registry mechanism Phase 5 uses for cross-plugin model extension. One pattern for both intra- and cross-plugin hooks. - Hooks that only touch the model itself (slug generation, timestamps) stay as methods on the model.
The rule is enforced by tooling, not documentation: summer build (or a framework go test) fails when a plugin's models/ package imports another package of the same plugin module.
Evidence (Fonoteka PHP plugin, 2026-09-18)
Cross-directory use edges in plugins/golem15/fonoteka:
classes/→models/: 40 filesmodels/→classes/: 1 (models/Album.php:Classes\ArtistResolver,Classes\Casts\MarketPriceCast)models/→controllers/: 0;classes/→controllers/: 0jobs/,console/,middleware/→classes/: 5;traits/→models/: 2
So the only cycle in the directory graph is models ↔ classes, caused by one file, in exactly the two categories the rule addresses (a cast and a service-calling hook). Everything else is already a DAG: models ← classes ← jobs/console/middleware/controllers ← plugin.go.
Trade-off accepted
A converted model no longer shows its whole lifecycle in one file: models/album.go plus classes/album_hooks.go together replace Album.php. This is the price of the rule being mechanical and checkable before compile.
Open
- The Phase 3
fonoteka.goplugins (golem15.user,golem15.fonoteka) are flat. They are restructured into this layout when Phase 5 widens them, not in Phase 4 (Phase 4 writes only tosummercms.go). - Validate the rule on a second and third plugin (keios.eu stack plugins, a jz/pxpx plugin) before the bulk port — see todo
verify-models-leaf-rule.