feat(11.1-03): add the Architecture and Plugins docs sections
- architecture: introduction, Go modules and workspaces, application lifecycle, request lifecycle - plugins: registration, scheduling, extending, testing - verified Examples for backpack services, towel request context, pact schedules and festival events - TestDocsRequiredPages lists the eight new pages
This commit is contained in:
75
docs/architecture/application-lifecycle.md
Normal file
75
docs/architecture/application-lifecycle.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Application lifecycle
|
||||
description: "What happens when an application binary starts: configuration, the backpack container, plugin ordering, Register and Boot, and database-dependent boot work."
|
||||
section: architecture
|
||||
order: 30
|
||||
---
|
||||
# Application lifecycle
|
||||
|
||||
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:
|
||||
|
||||
1. Loads configuration with `compass.Load` from the `config/` directory, applying the environment directory and `SUMMER_` variables.
|
||||
2. Creates the application container with `backpack.New`.
|
||||
3. Activates the plugins listed in `summer.yaml` with `party.Activate`.
|
||||
4. Collects the console commands: the framework's runtime commands (from `lagoon.RuntimeCommands`, `conga.RuntimeCommands`, `surf.ServeCommand`, `surf.RouteListCommand` and `cabana.RuntimeCommands`), then the commands of every plugin that implements `pact.HasCommands`.
|
||||
5. Publishes the command set as a `bonfire.Catalog`, so the scheduler can run commands in-process.
|
||||
6. 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:
|
||||
|
||||
1. `backpack.App.SetPlugins` records the complete plugin set, so `backpack.App.HasPlugin` answers correctly from the first Register onwards.
|
||||
2. The embedded defaults of every plugin that implements `pact.HasConfig` are merged into the configuration under the plugin ID.
|
||||
3. `party.Plugin.Register` runs for every plugin. Publish services here; do not use other plugins' services yet.
|
||||
4. The framework publishes the translator and the mailer, and registers each plugin's translations and mail templates.
|
||||
5. `party.Plugin.Boot` runs 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:
|
||||
|
||||
```go src=modules/backpack/example_test.go#ExampleApp_Publish
|
||||
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](../../modules/pact/README.md). 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](../plugins/extending.md) shows where GORM callbacks fit.
|
||||
Reference in New Issue
Block a user