From a9c77fcf49b4cfc7003db23d27e8c1ebb8d4d487 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 18 Sep 2026 18:37:26 +0200 Subject: [PATCH] docs(05-01): confirm models-leaf rule against three more plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Probe keios.eu user, jz chat, and pxpx checkout for models→sibling uses - Every live edge is a cast or service-calling hook; no "other" category - Close folded todo verify-models-leaf-rule; bulk port may proceed --- .../notes/plugin-layout-winter-directories.md | 69 ++++++++++++++++++- .../verify-models-leaf-rule.md | 3 + 2 files changed, 69 insertions(+), 3 deletions(-) rename .planning/todos/{pending => done}/verify-models-leaf-rule.md (72%) diff --git a/.planning/notes/plugin-layout-winter-directories.md b/.planning/notes/plugin-layout-winter-directories.md index 23629f2..d6c769c 100644 --- a/.planning/notes/plugin-layout-winter-directories.md +++ b/.planning/notes/plugin-layout-winter-directories.md @@ -31,8 +31,8 @@ Chosen because conversion is mostly done by agents across many plugins: `models/ `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. +- **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). The same treatment covers other model-owned types filed outside `models/` in PHP: contracts the model implements, jsonable value objects and their factories, money/MIME formatters, and type-mapping policies. +- **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. The same treatment covers other service-calling methods on the model: `afterSave` cache busts, job dispatch, broadcasting traits, and `findOrCreate`/resolver methods that reach a sibling service. - 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. @@ -48,6 +48,70 @@ Cross-directory `use` edges in `plugins/golem15/fonoteka`: 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`. +## Evidence (keios.eu `golem15/user`, 2026-09-18) + +Probe: `grep -rn "Golem15\\\\User\\\\Classes" plugins/golem15/user/models` plus the reverse `classes/ → models/` direction, then every `Golem15\User\` sibling `use` from `models/`. + +Cross-directory `use` edges in `plugins/golem15/user`: + +- `classes/` → `models/`: 2 files (`AuthManager.php`, `FamilyAuth.php`) +- `models/` → `classes/`: **0** +- `models/` → `controllers/`: 0; `controllers/` → `models/`: 9 +- `commands/` → `models/`: 1; `repositories/` → `models/`: 1; `middleware/` → `models/`: 0 +- `models/` → any sibling: **1** (`models/User.php`: `Contracts\UserRepository`) + +| File | Sibling import | Classification | +|------|----------------|----------------| +| `models/User.php` | `Contracts\UserRepository` | **service-calling hook** — `afterSave()` does `Cache::forget(UserRepository::CACHE_KEY_PREFIX . $this->id)`. Conversion: register the cache-bust from `classes/` (or keep the constant next to the model). | + +No `models/` → `classes/` cycle. The one sibling edge is a lifecycle hook, the same category Fonoteka's `ArtistResolver` already covers. DAG: `models ← classes/repositories/commands/controllers ← plugin.go`. + +## Evidence (jz `golem15/chat`, 2026-09-18) + +Probe: `grep -rn "Golem15\\\\Chat\\\\Classes" plugins/golem15/chat/models` plus reverse `classes/ → models/` and every `Golem15\Chat\` sibling `use` from `models/`. + +Cross-directory `use` edges in `plugins/golem15/chat`: + +- `classes/` → `models/`: 2 files (`ChatChannelAuthorizer.php`, `ChatService.php`) +- `models/` → `classes/`: **1** (`models/Message.php`: `Classes\ChatAttachmentPolicy`) +- `models/` → `controllers/`: 0; `controllers/` → `models/`: 1 +- `jobs/` → `models/`: 1; `console/` → `models/`: 1; `traits/` → `models/`: 0 +- `models/` → any sibling: **4** (table below) + +| File | Sibling import | Classification | +|------|----------------|----------------| +| `models/Message.php` | `Classes\ChatAttachmentPolicy` | **cast** — `ChatAttachmentPolicy::kindFor($mime)` is a MIME→kind type mapping used when serializing attachments, the same "model concern filed in `classes/`" as Fonoteka `MarketPriceCast`. Conversion: policy lives in `models/` (or the serialize method moves to `classes/`, matching `SerializesFonoteka`). | +| `models/Message.php` | `Traits\ChatBroadcasting` | **service-calling hook** — `afterCreate` fan-out via Centrifugo. Conversion: register from `classes/` (Phase 11 broadcast seam). | +| `models/Message.php` | `Jobs\MaintainThreadSummaryJob` | **service-calling hook** — `afterCreate`/`afterDelete` dispatch. Conversion: register from `classes/`. | +| `models/Conversation.php` | `Contracts\ChatContextInterface` | **service-calling hook** — `getChatContext()` resolves `app('chat.context.resolver')`; `findOrCreateDmInContext`/`findOrCreateGroupChat` are factory methods that take the adapter. Conversion: resolver/factory methods live in `classes/`; the interface can sit with them or in `models/`. | + +The `models ↔ classes` cycle is one file, one cast. The other three sibling edges are service-calling hooks. Same two categories as Fonoteka. + +## Evidence (pxpx `pixelpixel/checkout`, 2026-09-18) + +Probe: `grep -rn "Pixelpixel\\\\Checkout\\\\Classes"` (and the live namespace `PixelPixel\CheckOut\`) on `plugins/pixelpixel/checkout/models`, plus reverse `classes/ → models/` and every in-plugin sibling `use` from `models/`. PHP namespaces are `PixelPixel\CheckOut\` (mixed case) against directory `pixelpixel/checkout`. + +Cross-directory `use` edges in `plugins/pixelpixel/checkout`: + +- `classes/` → `models/`: 0 +- `models/` → `classes/`: **0** +- `models/` → `controllers/`: 0; `controllers/` → `models/`: 14 +- `jobs/` → `models/`: 2; `console/` → `models/`: 2; `support/` → `models/`: 1; `factories/` → `models/`: 3 +- `models/` → any sibling: **6** `use` lines (4 live, 2 unused imports) + +| File | Sibling import | Classification | +|------|----------------|----------------| +| `models/Cart.php` | `Contracts\CartInterface` | **cast** — the model implements a contract that describes itself. Conversion: interface lives in `models/`. | +| `models/Cart.php` | `ValueObjects\CartItem` | **cast** — jsonable line-item VO stored on `content`. Conversion: VO lives in `models/`. | +| `models/Cart.php` | `Factories\CartItemFactory` | **cast** — factory for that jsonable VO (`getItemsList`). Conversion: factory lives in `models/` next to the VO. | +| `models/Payment.php` | `Facades\MoneyFormatter` | **cast** — display accessors format stored money, same job as Fonoteka `MarketPriceCast`. Conversion: formatter lives in `models/` (or `lagoon` if generic). | +| `models/Cart.php` | `Support\MoneyHelp` | unused import — not a live edge | +| `models/Order.php` | `Support\CartManager` | unused import — not a live edge | + +No `models/` → `classes/` cycle. Every live sibling edge is a model-owned type (the widened "cast" treatment). DAG: `models ← support/factories/jobs/console/controllers ← plugin.go`. + +Across the three plugins plus Fonoteka, every live `models/` → sibling edge is cast or service-calling hook. The leaf rule holds; no "other" category was found. + ## 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. @@ -55,4 +119,3 @@ A converted model no longer shows its whole lifecycle in one file: `models/album ## 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`. diff --git a/.planning/todos/pending/verify-models-leaf-rule.md b/.planning/todos/done/verify-models-leaf-rule.md similarity index 72% rename from .planning/todos/pending/verify-models-leaf-rule.md rename to .planning/todos/done/verify-models-leaf-rule.md index d2e9a0b..d2a4d4c 100644 --- a/.planning/todos/pending/verify-models-leaf-rule.md +++ b/.planning/todos/done/verify-models-leaf-rule.md @@ -3,6 +3,7 @@ title: Verify the models-leaf layout rule against keios.eu and a jz/pxpx plugin date: 2026-09-18 priority: medium area: plugin layout / conversion +status: done --- Before the bulk model port (Phase 5) and before any second-project port, repeat the dependency probe done for Fonoteka on 2026-09-18 (see `.planning/notes/plugin-layout-winter-directories.md`) on at least: @@ -17,3 +18,5 @@ For each, count `models/ → classes/` (and `models/ → any sibling dir`) `use` grep -rl "Vendor\\\\Plugin\\\\Classes" plugins/vendor/plugin/models | wc -l grep -rn "Vendor\\\\Plugin\\\\Classes" plugins/vendor/plugin/models ``` + +**Resolution (2026-09-18):** Probed `keios.eu/plugins/golem15/user`, `wavepath.org/plugins/golem15/chat`, and `PXSTARTER/plugins/pixelpixel/checkout`. Every live `models/` → sibling edge is cast or service-calling hook (see the three dated Evidence sections in `.planning/notes/plugin-layout-winter-directories.md`). No "other" category. The leaf rule holds; the bulk port may proceed.