- docs/database: models, migrations, queries and pagination, relations, casts and validation, attachments and transactions (lagoon.Transaction, lagoon.AfterCommit, nested savepoints, lagoon.OnDatabase) - docs/services: configuration, events, routing with auth groups, rate limiting, authentication, the OAuth server, mail and localization - runnable Examples for lagoon, attach, compass, surf, wire, bouncer, wristband, postcard, phrasebook and festival; lagoon TestDocs* regions run on the package's Postgres harness through DocsDB - 15 new required pages
99 lines
4.9 KiB
Markdown
99 lines
4.9 KiB
Markdown
---
|
|
title: Events
|
|
description: Listen for and fire typed events on the application bus with festival, with priorities, collected results and a halting fire that stops when handled.
|
|
section: services
|
|
order: 20
|
|
---
|
|
# Events
|
|
|
|
`Event::listen` and `Event::fire` are how WinterCMS plugins extend each other. SummerCMS keeps the pattern with [festival](../../modules/festival/README.md): each application has one bus, `backpack.App.Events`, and plugins listen from their `Boot` step for events that other plugins fire. [Extending plugins](../plugins/extending.md) shows where events fit among the other extension points; this page covers the bus itself.
|
|
|
|
## Typed events
|
|
|
|
An event is a Go type, not a string. A listener is a function that takes a context and the event, and the bus routes by type, so a listener never receives a payload of the wrong shape and a mismatch does not compile. Name events after what happened, and keep them in the package of the plugin that fires them so listeners can import the type.
|
|
|
|
`festival.Bus.Listen` registers a listener at priority 0 and `festival.Bus.ListenPriority` at a given priority. Higher priorities run first, and listeners with the same priority run in registration order. The first argument is the ID of the plugin that owns the listener:
|
|
|
|
```go src=modules/festival/example_test.go#ExampleBus_Fire
|
|
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
|
|
```
|
|
|
|
`festival.Bus.Fire` runs every listener, even after one fails, and returns the failures joined with `errors.Join`. A listener that panics is recovered and reported as an error that names its plugin, so one faulty plugin cannot stop the others.
|
|
|
|
All three dispatch methods run the listeners on the caller's goroutine, before they return. For work that should not delay the request, a listener dispatches a job; see [Queued jobs](jobs.md).
|
|
|
|
## Collecting contributions
|
|
|
|
WinterCMS events often gather something from their listeners, such as extra form fields or menu items. `festival.Bus.Collect` runs every listener and, after each one, merges the map the event returns from `festival.Collectable.Collected`. A later listener wins when two set the same key. Use a pointer event so listeners can write to it:
|
|
|
|
```go src=modules/festival/example_test.go#ExampleBus_Collect
|
|
bus := festival.New()
|
|
bus.Listen("acme.seo", func(ctx context.Context, e *PostFormExtended) error {
|
|
e.fields = map[string]any{"meta_title": "text"}
|
|
return nil
|
|
})
|
|
bus.Listen("acme.gallery", func(ctx context.Context, e *PostFormExtended) error {
|
|
e.fields = map[string]any{"cover": "fileupload"}
|
|
return nil
|
|
})
|
|
|
|
fields, err := bus.Collect(context.Background(), &PostFormExtended{})
|
|
fmt.Println(fields, err)
|
|
// Output: map[cover:fileupload meta_title:text] <nil>
|
|
```
|
|
|
|
`festival.Bus.Collect` returns the payload gathered so far together with the joined errors, so one failing listener does not lose the others' contributions.
|
|
|
|
## Stopping at the first handler
|
|
|
|
The WinterCMS halting fire stops at the first listener that returns a result. `festival.Bus.UntilHandled` stops as soon as the event's `festival.Handleable.IsHandled` reports true, or at the first error, and returns whether the event was handled:
|
|
|
|
```go src=modules/festival/example_test.go#ExampleBus_UntilHandled
|
|
bus := festival.New()
|
|
bus.ListenPriority("acme.pages", 10, func(ctx context.Context, e *SlugResolving) error {
|
|
if e.Slug == "about" {
|
|
e.Found = "page"
|
|
}
|
|
return nil
|
|
})
|
|
bus.Listen("acme.blog", func(ctx context.Context, e *SlugResolving) error {
|
|
fmt.Println("acme.blog asked for", e.Slug)
|
|
e.Found = "post"
|
|
return nil
|
|
})
|
|
|
|
for _, slug := range []string{"about", "hello-world"} {
|
|
e := &SlugResolving{Slug: slug}
|
|
handled, err := bus.UntilHandled(context.Background(), e)
|
|
fmt.Println(slug, handled, e.Found, err)
|
|
}
|
|
// Output:
|
|
// about true page <nil>
|
|
// acme.blog asked for hello-world
|
|
// hello-world true post <nil>
|
|
```
|
|
|
|
Here `acme.pages` listens at a higher priority, so it gets the first chance to claim a slug, and `acme.blog` is asked only when no page matched.
|
|
|
|
## Events and transactions
|
|
|
|
Listeners run where the event is fired, inside any transaction the caller has open. A listener that writes to the database joins that transaction when it uses the transaction handle the event carries. A listener with a side effect outside the database, such as a mail or a broadcast, should defer it with `lagoon.AfterCommit`, so it does not announce a write that rolls back. See [Transactions](../database/transactions.md).
|