Plugins
Extending plugins
Extend other plugins through typed events, published services, optional dependencies and GORM callbacks, and replace a plugin by forking its module.
On this page
Plugins in WinterCMS extend each other by listening to events and by calling extend on another plugin's classes at runtime. Go has no runtime class extension, so SummerCMS gives you four explicit mechanisms: events, published services, optional dependencies and database callbacks. When none of them fits, you fork the plugin.
Events#
The event bus from festival is the Go form of Event::listen and Event::fire. Each application has one bus, backpack.App.Events. A plugin that wants to be extensible defines an event type and fires it; other plugins listen for that type from their Boot step.
Events are routed by Go type, not by a string name, so a listener for PostPublished receives exactly that type and a payload mismatch does not compile:
bus := festival.New() // in a plugin, use app.Events
// acme.search and acme.notify extend acme.blog from their Boot steps.
bus.Listen("acme.search", func(ctx context.Context, e PostPublished) error {
fmt.Println("index", e.Title)
return nil
})
bus.ListenPriority("acme.notify", 10, func(ctx context.Context, e PostPublished) error {
fmt.Println("notify subscribers of", e.Title)
return nil
})
// acme.blog fires the event; higher priorities run first.
if err := bus.Fire(context.Background(), PostPublished{Title: "Hello"}); err != nil {
fmt.Println(err)
}
// Output:
// notify subscribers of Hello
// index Hello
Each listener names the plugin that owns it, so an error or a recovered panic in a listener reports which plugin failed. The bus has three dispatch modes:
festival.Bus.Fireruns every listener and returns their joined errors.festival.Bus.Collectruns every listener and merges the payload each one adds, for events that gather contributions such as extra fields or menu items. The event implementsfestival.Collectable.festival.Bus.UntilHandledstops at the first listener that handles the event, the WinterCMS halting fire. The event implementsfestival.Handleable.
festival.Bus.ListenPriority sets a priority: higher priorities run first, and equal priorities run in registration order.
Services#
A plugin that offers functionality to others publishes it on the container during Register with backpack.App.Publish, preferably under an interface type. Other plugins read it during Boot with backpack.App.Lookup. Because both sides use the same type, the consumer only imports the package that declares the interface, not the provider's internals. See Application lifecycle for a complete example.
Optional dependencies#
A required dependency goes in party.Plugin.Requires, and activation fails when it is missing. For an integration that should work only when another plugin happens to be installed, check for it instead:
backpack.App.HasPluginreports whether a plugin ID is part of this build. It answers correctly during Register, before that plugin has booted.pact.OptionalMessageis a small service an optional plugin can publish so others integrate with it without importing its package.
This replaces PluginManager::exists checks in WinterCMS.
Model hooks and GORM callbacks#
A model reacts to its own lifecycle with GORM hook methods such as BeforeSave; lagoon names them as interfaces (lagoon.HasBeforeSave, lagoon.HasBeforeDelete and the rest) so you can assert them at compile time.
To react to another plugin's models, the equivalent of Post::extend with model events, register a GORM callback on the shared database handle. Boot runs before the database is open, so register it through lagoon.OnDatabase, which calls your function with the shared *gorm.DB once it is available. For work that must wait until the transaction commits, such as sending mail or publishing a realtime event, use lagoon.AfterCommit.
Replacing a plugin#
When an extension point is missing, fork the plugin's module and point the application at your copy with a replace directive in its go.mod, keeping the plugin ID. The rest of the application keeps importing and requiring the original path. Go modules and workspaces shows the directive.
Prefer adding an event or a published service to the original plugin over a long-lived fork: a fork has to be kept in step with every change upstream.