feat(11.1-03): add the Coming from WinterCMS concept map with a verified plugin example

- docs/setup/coming-from-wintercms.md maps WinterCMS concepts to checked
  pkg.Ident spans and lists what SummerCMS does not provide
- party BlogPlugin and ExamplePlugin, shown through src= fences
- TestDocsRequiredPages asserts required pages build as .html and .md
- index links the new page
This commit is contained in:
Jakub Zych
2026-09-30 22:06:45 +02:00
parent f77b1d8688
commit d6003cd84b
5 changed files with 212 additions and 1 deletions

View File

@@ -72,6 +72,38 @@ func TestDocsBuildRealTree(t *testing.T) {
}
}
// requiredPages lists the guide pages the docs must keep. Each content plan
// appends the pages it writes.
var requiredPages = []string{
"index",
"setup/installation",
"setup/coming-from-wintercms",
}
// TestDocsRequiredPages asserts that every required page is in the loaded
// tree and is built as both .html and .md on the real tree.
func TestDocsRequiredPages(t *testing.T) {
pages, problems, err := docsite.Pages(docsite.Options{Root: repoRoot, Commands: docsCommands()})
if err != nil || len(problems) > 0 {
t.Fatalf("Pages: %v %v", err, problems)
}
loaded := map[string]bool{}
for _, p := range pages {
loaded[p.URL] = true
}
out, _ := buildRealTree(t)
for _, url := range requiredPages {
if !loaded[url] {
t.Errorf("page %s is not in the docs tree", url)
}
for _, ext := range []string{".html", ".md"} {
if _, err := os.Stat(filepath.Join(out, filepath.FromSlash(url)+ext)); err != nil {
t.Errorf("page %s has no %s output: %v", url, ext, err)
}
}
}
}
// frameworkModules lists modules/<m> directories that hold a non-test Go
// file, discovered independently of docsite.
func frameworkModules(t *testing.T) []string {

View File

@@ -8,4 +8,4 @@ order: 0
SummerCMS keeps what makes WinterCMS productive (plugins that extend each other, YAML-driven admin forms and lists, console scaffolding) and compiles an application into a single Go binary. The framework modules, the application's plugins, the embedded admin SPA and the console commands all ship as one executable.
Start with [Installation](setup/installation.md) to set up the toolchain and the `summer` CLI. The API reference section has one page per framework module.
Start with [Installation](setup/installation.md) to set up the toolchain and the `summer` CLI. If you know WinterCMS, read [Coming from WinterCMS](setup/coming-from-wintercms.md) for a map of its concepts to SummerCMS. The API reference section has one page per framework module.

View File

@@ -0,0 +1,132 @@
---
title: Coming from WinterCMS
description: Map WinterCMS plugins, models, backend controllers, routes, events and commands to their SummerCMS equivalents, and see what is not provided.
section: setup
order: 40
---
# Coming from WinterCMS
SummerCMS keeps the parts of WinterCMS that make a plugin developer productive. Plugins still declare what they add to the application and extend each other through events and shared services. Backend lists and forms are still described in `fields.yaml` and `columns.yaml`. Models, migrations, console commands and scaffolding keep their WinterCMS shape, so a plugin ports file by file.
What changes is everything that depends on PHP at runtime. Plugins are Go packages compiled into one binary, so there is no plugin directory scanned at boot and no runtime autoloading. Magic methods, dynamic properties and behaviours give way to Go interfaces and composition. SummerCMS is headless: it serves a JSON API and the admin SPA, and the frontend is a separate application that calls that API.
## Concept map
Each row names the WinterCMS concept, the SummerCMS identifiers that replace it and the module that documents them.
| WinterCMS | SummerCMS | Where |
|-----------|-----------|-------|
| `Plugin.php` with `pluginDetails`, `register` and `boot` | A type implementing `party.Plugin` (`party.Plugin.ID`, `party.Plugin.Requires`, `party.Plugin.Register`, `party.Plugin.Boot`), registered from `init` with `party.Register` | [party](../../modules/party/README.md) |
| `$require` plugin dependencies | `party.Plugin.Requires`; `party.Activate` orders plugins so each comes after the ones it requires | [party](../../modules/party/README.md) |
| `registerPermissions`, `registerNavigation` | `pact.HasPermissions` returning `pact.Permission` values, `pact.HasNavigation` returning `pact.NavigationItem` values | [pact](../../modules/pact/README.md) |
| `version.yaml` and the `updates/` directory | `pact.HasMigrations` returning an ordered gormigrate set; `lagoon.Migrate` runs every plugin's set and `lagoon.RollbackLast` undoes the last one | [lagoon](../../modules/lagoon/README.md) |
| Eloquent models | GORM structs with lagoon helpers: `lagoon.Fill` for mass assignment, `lagoon.Validate` for rules, `lagoon.Jsonable` for JSON columns, `lagoon.Page` for pagination | [lagoon](../../modules/lagoon/README.md) |
| `fields.yaml` and `columns.yaml` | The same YAML, embedded in the plugin through `pact.AdminAssets` and compiled at boot into a `cabana.CompiledController` | [cabana](../../modules/cabana/README.md) |
| Backend controllers with the Form, List and Relation behaviours | A `pact.AdminController` returned from `pact.HasAdminControllers`; the generic admin API replaces the behaviours, and hooks such as `pact.FormBeforeCreate` and `pact.ListExtendQuery` replace behaviour overrides | [cabana](../../modules/cabana/README.md) |
| `routes.php` | `pact.HasRoutes`, declaring routes on a `pact.Router` with groups, middleware names and `pact.Router.Where` constraints | [surf](../../modules/surf/README.md) |
| Route middleware | Named middleware of type `pact.Middleware`, registered through `pact.HasMiddleware` | [surf](../../modules/surf/README.md) |
| `config/*.php`, `.env` and `Config::get` | `compass.Config` with per-environment directories and `SUMMER_` overrides; plugin defaults through `pact.HasConfig` | [compass](../../modules/compass/README.md) |
| `lang/` files and `Lang::get` | `pact.HasLang` for plugin catalogs, read through `phrasebook.Translator.Get` and `phrasebook.Translator.Choice` | [phrasebook](../../modules/phrasebook/README.md) |
| `Event::listen` and `Event::fire` | `festival.Bus.Listen` and `festival.Bus.Fire` on `backpack.App.Events`, keyed by the event's Go type | [festival](../../modules/festival/README.md) |
| `App::make` and singleton bindings | `backpack.App.Publish` and `backpack.App.Lookup`, keyed by type | [backpack](../../modules/backpack/README.md) |
| Artisan commands and `registerConsoleCommand` | `bonfire.Command` values returned from `pact.HasCommands` | [bonfire](../../modules/bonfire/README.md) |
| Queued jobs | `pact.HasJobs` with jobs built by `conga.Job`, dispatched with `conga.Manager.Dispatch` inside the caller's transaction | [conga](../../modules/conga/README.md) |
| `registerSchedule` and the scheduler | `pact.HasSchedule` returning `pact.ScheduledCommand` entries with a `pact.Cadence` | [pact](../../modules/pact/README.md) |
| Mail templates in `views/mail` | `pact.HasMailTemplates`, sent through `postcard.Mailer` | [postcard](../../modules/postcard/README.md) |
| Settings models and `registerSettings` | `pact.HasSettings` returning `pact.SettingsItem` entries | [cabana](../../modules/cabana/README.md) |
| Laravel broadcasting | `lighthouse.Publisher` drivers and models that implement `lighthouse.Broadcastable` | [lighthouse](../../modules/lighthouse/README.md) |
| Laravel Scout search | Models that implement `beachcomber.Searchable`, synced after commit | [beachcomber](../../modules/beachcomber/README.md) |
| The Laravel HTTP client | `fetchguard.Fetch` with a `fetchguard.Policy` that blocks private addresses and limits size and time | [fetchguard](../../modules/fetchguard/README.md) |
## What is not provided
SummerCMS does not port the WinterCMS frontend or the PHP helpers that Go already covers. Do not look for these when you port a plugin:
| WinterCMS | SummerCMS |
|-----------|-----------|
| CMS pages, themes, layouts and partials | Not provided. The frontend is a separate application that calls the JSON API. |
| Components | Not provided. Write an HTTP handler and declare its route through `pact.HasRoutes`. |
| The AJAX framework and Snowboard | Not provided. The frontend calls the JSON API and subscribes to realtime channels. |
| The media manager | Not provided. Store uploads as model attachments. |
| Import and export in backend lists | Not provided. |
| Record sorting (the Reorder behaviour) | Not provided. |
| Collections | Not provided. Use Go slices and the `slices` and `maps` packages. |
| Behaviours and dynamic class extension | Not provided. Use Go interfaces and composition. |
| Cache | Not provided. Use the Go standard library or a service another plugin publishes. |
| Session | Not provided. The API is stateless and authenticates with tokens. |
## Plugin.php in Go
A WinterCMS plugin registration class for `Acme\Blog` looks like this:
```php
<?php namespace Acme\Blog;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public $require = ['Acme.User'];
public function pluginDetails()
{
return ['name' => 'Blog', 'author' => 'Acme'];
}
public function registerPermissions()
{
return [
'acme.blog.access_posts' => ['tab' => 'Blog', 'label' => 'Manage posts'],
];
}
}
```
The SummerCMS plugin is a Go type. The four `party.Plugin` methods replace the plugin details, `$require`, `register` and `boot`, and each extra capability is one more interface, here `pact.HasPermissions`:
```go src=modules/party/example_plugin_test.go
package party_test
import (
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
)
// BlogPlugin is the acme.blog plugin: the Go form of a WinterCMS Plugin.php.
// A real plugin package also registers it from init with
// party.Register(&BlogPlugin{}).
type BlogPlugin struct{}
// The optional capabilities the plugin opts into, checked at compile time.
var _ pact.HasPermissions = (*BlogPlugin)(nil)
// ID is the plugin identifier in vendor.plugin form.
func (p *BlogPlugin) ID() string { return "acme.blog" }
// Requires lists the plugins that must register and boot first ($require).
func (p *BlogPlugin) Requires() []string { return []string{"acme.user"} }
// Register runs before any plugin boots: publish services here.
func (p *BlogPlugin) Register(app *backpack.App) error { return nil }
// Boot runs after every plugin registered: listen to events and look up
// services other plugins published.
func (p *BlogPlugin) Boot(app *backpack.App) error { return nil }
// Permissions replaces registerPermissions().
func (p *BlogPlugin) Permissions() []pact.Permission {
return []pact.Permission{
{Code: "acme.blog.access_posts", Tab: "Blog", Label: "Manage posts"},
}
}
```
The type satisfies `party.Plugin`, which this example checks every time `go test ./...` runs:
```go src=modules/party/example_test.go#ExamplePlugin
var p party.Plugin = &BlogPlugin{}
fmt.Println(p.ID())
// Output: acme.blog
```
Plugin IDs are lower case in `vendor.plugin` form, so `Acme.User` becomes `acme.user`. The application does not scan for plugins: it lists their IDs in its `summer.yaml` manifest, and `summer build` compiles them in. See [Installation](installation.md) to build your first application.

View File

@@ -0,0 +1,34 @@
package party_test
import (
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
)
// BlogPlugin is the acme.blog plugin: the Go form of a WinterCMS Plugin.php.
// A real plugin package also registers it from init with
// party.Register(&BlogPlugin{}).
type BlogPlugin struct{}
// The optional capabilities the plugin opts into, checked at compile time.
var _ pact.HasPermissions = (*BlogPlugin)(nil)
// ID is the plugin identifier in vendor.plugin form.
func (p *BlogPlugin) ID() string { return "acme.blog" }
// Requires lists the plugins that must register and boot first ($require).
func (p *BlogPlugin) Requires() []string { return []string{"acme.user"} }
// Register runs before any plugin boots: publish services here.
func (p *BlogPlugin) Register(app *backpack.App) error { return nil }
// Boot runs after every plugin registered: listen to events and look up
// services other plugins published.
func (p *BlogPlugin) Boot(app *backpack.App) error { return nil }
// Permissions replaces registerPermissions().
func (p *BlogPlugin) Permissions() []pact.Permission {
return []pact.Permission{
{Code: "acme.blog.access_posts", Tab: "Blog", Label: "Manage posts"},
}
}

View File

@@ -0,0 +1,13 @@
package party_test
import (
"fmt"
"git.golem15.com/golem15/summercms/modules/party"
)
func ExamplePlugin() {
var p party.Plugin = &BlogPlugin{}
fmt.Println(p.ID())
// Output: acme.blog
}