feat(11.1-05): pin the walkthrough to the scaffolder and link it from the concept map

- TestScaffoldLayout runs make:plugin, make:model, make:migration,
  make:admin-controller and make:command for acme.blog in a copy of
  examples/hello and compares the file set with docs/examples/blog
- the page lists the exact make commands, the go.mod a scaffolded plugin
  gets, what the scaffolder leaves to the developer and a checklist
- scaffolding.md no longer claims same-second migrations get consecutive
  timestamps; only same-name ones do
- coming-from-wintercms.md and index.md link the walkthrough
This commit is contained in:
Jakub Zych
2026-09-30 23:41:00 +02:00
parent dd82b8a2ad
commit 63290c66d3
5 changed files with 217 additions and 3 deletions

View File

@@ -10,6 +10,52 @@ This walkthrough ports a small WinterCMS plugin, `Acme.Blog`, to SummerCMS. The
The name `acme/blog` is a neutral example. The SummerCMS code on this page is not a sketch: every Go and YAML block is a copy of a file under `docs/examples/blog` in the framework repository, a compiled plugin whose tests activate it, serve its route, run its command and run its migrations up and down against PostgreSQL. Read [Coming from WinterCMS](coming-from-wintercms.md) first for the map of concepts.
## Scaffold it yourself
Every file of the plugin starts as scaffolder output. From the application directory, these commands produce the same file layout as `docs/examples/blog`, which a test in the framework checks:
```sh
summer make:plugin acme.blog
summer make:model acme.blog Post
summer make:migration acme.blog AddPublishedAt
summer make:admin-controller acme.blog Posts
summer make:command acme.blog Publish
summer plugin:add plugins/blog
summer build
```
| Command | Writes |
|---------|--------|
| `summer make:plugin acme.blog` | `plugins/blog` as its own Go module: `plugin.go`, `routes.go`, `registry.gen.go`, `go.mod`, `config/config.yaml`, `lang/en/lang.yaml`, `views/mail/welcome.htm` and a `doc.go` in `classes`, `console`, `controllers`, `jobs`, `middleware`, `models` and `updates` |
| `summer make:model acme.blog Post` | `models/post.go` and `updates/<timestamp>_create_acme_blog_posts.go` |
| `summer make:migration acme.blog AddPublishedAt` | `updates/<timestamp>_add_published_at.go` |
| `summer make:admin-controller acme.blog Posts` | `controllers/posts.go`, `controllers/posts/config_form.yaml`, `controllers/posts/config_list.yaml`, `models/posts/fields.yaml` and `models/posts/columns.yaml` |
| `summer make:command acme.blog Publish` | `console/publish.go` |
| `summer plugin:add plugins/blog` | The plugin in `summer.yaml`, a `require` and a local `replace` in the application's `go.mod`, and the directory in `go.work` |
| `summer build` | `bin/acme`, the application binary with the plugin compiled in |
Each `make:` command also rewrites `registry.gen.go` and runs `go mod tidy` in the plugin. The sections below fill in what each generated file leaves empty. See [Scaffolding](../console/scaffolding.md) for every option of these commands.
The copy in the framework repository has no `go.mod`, because it is a package of the framework module so that the framework's own `go test ./...` covers it. A plugin you scaffold is a module of its own, and its `go.mod` starts like this for an application whose module is `example.com/acme` (indirect requirements left out):
```text
module example.com/acme/plugins/blog
go 1.27.0
toolchain go1.27.0
require (
git.golem15.com/golem15/summercms v0.0.0
github.com/go-gormigrate/gormigrate/v2 v2.1.7
gorm.io/gorm v1.31.2
)
replace git.golem15.com/golem15/summercms => ../../../summercms.go
```
The `replace` points at the same framework checkout as the application's `go.mod`, so the plugin builds against your local framework while you develop.
## Plugin registration
In WinterCMS, `Plugin.php` describes the plugin and registers what it adds:
@@ -758,3 +804,26 @@ summer build
```
See [Writing commands](../console/writing-commands.md) for arguments, flags, prompts and output.
## What the scaffolder leaves to you
The `make:` commands write files that compile, not a finished plugin. These are the steps this walkthrough had to do by hand, and the scaffolder behaviour behind them:
- **The generated-code header stays.** Files the `make:` commands write start with `// Code generated by summer make. DO NOT EDIT.`, although you are meant to edit them. Keep the line: the commands rebuild `registry.gen.go` from the files that carry it, so a model, migration, command or admin controller whose header you delete disappears from the accessors the next time you run a `make:` command. Linters also treat these files as generated and skip them.
- **Check the migration order.** A migration's file name and ID start with the second it was created in. Migrations with different names created in the same second get the same timestamp and run in file-name order, so `add_published_at` would run before `create_acme_blog_posts` and fail on the missing table. Look at `updates/` after scaffolding; if two files share a timestamp, rename the later one and its ID. The example uses fixed timestamps, `20260101000000` and `20260101000100`.
- **The admin controller names the model after itself.** `make:admin-controller acme.blog Posts` returns `Posts` from `ModelName` and writes `modelClass: Posts`, and it puts `fields.yaml` and `columns.yaml` under `models/posts/`. Change `ModelName` and both `modelClass` values to the model, `Post`; the YAML can stay where it is, as in this example.
- **The admin controller needs more than the scaffolder writes.** The generated controller implements only `pact.AdminController`. Add `NewRecord` (`pact.AdminRecordSource`) so the admin API has a model to query, and `RequiredPermissions` (`pact.AdminPermissioned`): without it, any signed-in administrator can open the controller. The model needs `Fillable` and `Rules` before the admin API can save it. The plugin needs `AdminFS` (`pact.AdminAssets`) to embed the YAML; without it the application refuses to start once the admin is enabled.
- **A command that needs the application takes it as a parameter.** The function `make:command` writes takes no arguments, which is what the generated accessor looks for, so it cannot reach the database or the config. Give it the dependency as a parameter and return it from `Commands` yourself, as `blog:publish` does.
## Checklist
What changed on the way from WinterCMS to SummerCMS:
- `Plugin.php` became a `Plugin` type in `plugin.go`, registered from `init` with `party.Register`; each `register*` method became a capability interface such as `pact.HasPermissions` or `pact.HasNavigation`.
- The plugin is a Go module the application imports; `summer plugin:add` and `summer build` replace dropping a directory into `plugins/`.
- The Eloquent model became a GORM struct; `$fillable` and `$rules` became `Fillable` and `Rules` methods, and every mass assignment goes through `lagoon.Fill`.
- `version.yaml` and the update scripts became timestamped gormigrate entries, each with a real `Rollback`.
- `routes.php` became `Routes` on a `pact.Router`, with handlers that answer through a response type of their own and `lagoon.Paginate`.
- The backend controller class became a `pact.AdminController` with a model, a permission and the same YAML; the generic admin API replaces the behaviours and their views.
- The artisan command became a `bonfire.Command`, run with `./bin/acme blog:publish`.
- Language strings and config defaults stay in YAML files embedded in the binary.