Files
summercms/.planning/notes/plugin-layout-winter-directories.md

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 files classes/casts/* (e.g. Fonoteka MarketPriceCast) are model concerns filed elsewhere; in Go they live in models/ (or in the framework lagoon when generic).
  • Service-calling lifecycle hooks move out. A PHP model hook that reaches into classes/ (e.g. Album::beforeSave using ArtistResolver) becomes a GORM callback registered on the model from classes/ or plugin.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 files
  • models/ → classes/: 1 (models/Album.php: Classes\ArtistResolver, Classes\Casts\MarketPriceCast)
  • models/ → controllers/: 0; classes/ → controllers/: 0
  • jobs/, 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.go plugins (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 to summercms.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.