Architecture
Application lifecycle
What happens when an application binary starts: configuration, the backpack container, plugin ordering, Register and Boot, and database-dependent boot work.
On this page
Every run of an application binary, whether it serves HTTP or runs a single console command, goes through the same start-up. summer build generates the main.go that performs it, so you never write it by hand.
Start-up sequence#
The generated main does the following, in order:
- Loads configuration with
compass.Loadfrom theconfig/directory, applying the environment directory andSUMMER_variables. - Creates the application container with
backpack.New. - Activates the plugins listed in
summer.yamlwithparty.Activate. - Collects the console commands: the framework's runtime commands (from
lagoon.RuntimeCommands,conga.RuntimeCommands,surf.ServeCommand,surf.RouteListCommandandcabana.RuntimeCommands), then the commands of every plugin that implementspact.HasCommands. - Publishes the command set as a
bonfire.Catalog, so the scheduler can run commands in-process. - Runs the command named on the command line.
Plugin ordering#
party.Activate selects the plugins by ID and orders them so that every plugin comes after the plugins its party.Plugin.Requires lists. It fails before any plugin code runs when an ID is empty, duplicated or not compiled in, when a required plugin is missing, or when the requirements form a cycle.
Register, then Boot#
Activation runs in phases, and each phase finishes for every plugin before the next begins:
backpack.App.SetPluginsrecords the complete plugin set, sobackpack.App.HasPluginanswers correctly from the first Register onwards.- The embedded defaults of every plugin that implements
pact.HasConfigare merged into the configuration under the plugin ID. party.Plugin.Registerruns for every plugin. Publish services here; do not use other plugins' services yet.- The framework publishes the translator and the mailer, and registers each plugin's translations and mail templates.
party.Plugin.Bootruns for every plugin. Look up services, register event listeners and extend other plugins here.
This is the WinterCMS register and boot split: when any Boot runs, every plugin has already registered.
The container#
backpack.App is the application container that Register and Boot receive. It holds the configuration in backpack.App.Config, the event bus in backpack.App.Events and a typed service registry. Nothing in it is process-global, so two applications in one test do not share state.
Services are keyed by their Go type. A plugin publishes a value with backpack.App.Publish and another plugin reads it with backpack.App.Lookup and the same type argument. Publish under an interface type when consumers should not depend on your implementation:
app := backpack.New(&compass.Config{})
app.SetPlugins([]string{"acme.greeter", "acme.blog"})
// acme.greeter, in its Register step: publish under the interface type.
var greeter Greeter = englishGreeter{}
if err := app.Publish(greeter); err != nil {
fmt.Println(err)
return
}
// acme.blog, in its Boot step: look the service up by the same type.
if app.HasPlugin("acme.greeter") {
if found, ok := app.Lookup[Greeter](); ok {
fmt.Println(found.Greet("blog"))
}
}
// A second Publish under the same type is refused.
fmt.Println(app.Publish(greeter) != nil)
// Output:
// Hello, blog
// true
Capability interfaces#
Beyond the four party.Plugin methods, a plugin declares what it contributes by implementing interfaces from pact. The framework package that owns a capability finds it with a type assertion: surf asks for pact.HasRoutes and pact.HasMiddleware, lagoon for pact.HasMigrations, cabana for pact.HasAdminControllers, conga for pact.HasJobs and pact.HasSchedule. A plugin that does not implement an interface simply does not take part in that capability.
Database-dependent boot work#
Boot runs before any command opens the database: migrate and serve open it after activation, and commands such as key:generate never open it. Code that needs the database handle during boot, such as registering GORM callbacks, therefore goes through lagoon.OnDatabase. It runs the function immediately when the database is already published, and otherwise queues it until lagoon.Publish makes the shared *sql.DB and *gorm.DB handles available. An error from a queued function is returned by lagoon.Publish, so the command that opened the database fails instead of running with a half-registered plugin. Extending plugins shows where GORM callbacks fit.