Event Registry
Contract for the canonical domain-event catalogue, consumer visibility flags, and system-protected exclusions.
Overview
App\Services\Events\EventRegistry is the canonical domain-event catalogue. Core seeds it from config/events.php at construction; plugins and framework code extend it with register() / registerMany().
Consumer accessors (names() / describe()) filter by visibility flag and always exclude system-protected events. get() / has() remain available for recording regardless of exposure flags.
The public surface is a pinned cross-phase contract (workflow triggers, plugin SDK, and later consumers) — method names and signatures must not change without a coordinated migration.
Public surface
| Method | Purpose |
|---|---|
register() |
Register one EventDefinition by event name |
registerMany() |
Register multiple definitions sequentially |
names() |
Return visible event names for an optional consumer |
describe() |
Return visible EventDefinition objects keyed by name |
get() |
Return one definition or throw for an unknown name |
has() |
Check whether an event name is registered |
Accepted definition shape
Each entry is an immutable App\Services\Events\EventDefinition:
new EventDefinition(
name: 'account.updated',
source: EventSource::Core, // or EventSource::Plugin
payloadSchema: null, // optional array shape metadata
api: false,
webhook: true,
workflow: true,
plugin: true,
systemProtected: false,
)
Visibility flags gate consumer exposure:
| Flag | Consumer key passed to names() / describe() |
|---|---|
api |
'api' |
webhook |
'webhook' |
workflow |
'workflow' |
plugin |
'plugin' |
systemProtected excludes the event from every consumer accessor. It never gates recording via get() / has().
EventDefinition::visibleTo($consumer) returns the matching flag, always false when system-protected, and throws InvalidArgumentException for an unknown consumer string. The definition also derives entity from the substring before the first . in name.
Identity, collisions, and lookup behavior
Event identity is the definition's name string.
register()throwsInvalidArgumentExceptionwithEvent [{name}] is already registered.on collisionregisterMany()callsregister()per item, so the first collision aborts after earlier items have already been stored- Construction from config skips non-string keys and non-array definition values rather than throwing
Lookup behavior:
get($name)throwsInvalidArgumentExceptionwithUnknown event [{name}].when missinghas($name)is the optional guardnames(null)/describe(null)return every non-system-protected eventnames('plugin')/describe('plugin')further require the matching visibility flag
Runtime consumption
Current framework consumers use the catalogue for discovery and recording:
- Consumer pickers and catalogues call
names($consumer)/describe($consumer)so system-protected events stay hidden - Recording paths call
get()/has()without regard to visibility flags - Plugin / workflow seams treat the registry as the authoritative event name catalogue
Config seeding maps system_protected and the four visibility booleans from each config/events.php entry. payload_schema is kept only when it is an array; other types become null.
The webhook consumer slice is authoritative: WebhookEventRegistry is seeded from names('webhook'), so declaring webhook: true here is what makes an event subscribable.
Payload shape and schema
Every entry also declares payload_shape (App\Enums\PayloadShape), describing what producers actually send:
| Shape | Payload | Schema |
|---|---|---|
full_dto |
a complete response DTO nested under payload_key |
resolved from PayloadSchemas for the entry's payload_dto |
lean_envelope |
id + action pointer envelope (the default) |
the shared lean-envelope schema |
ad_hoc |
a bespoke array assembled at the dispatch site | none |
Schemas are not inlined in config: entries name a DTO, and the registry resolves the generated schema from App\Services\Events\PayloadSchemas at construction time, so cached config stays small. An entry may still supply an explicit payload_schema array, which always wins (plugins use this path).
An architecture ratchet enforces that every entry declares a valid payload_shape, that every full_dto event resolves a schema, and that the frozen set of schema-less events only ever shrinks.
Worked example
This is the current registration and consumer-filter shape:
$registry->register(new EventDefinition(
name: 'account.updated',
source: EventSource::Core,
webhook: true,
workflow: true,
plugin: true,
));
$registry->names('plugin'); // includes account.updated when plugin=true and not system-protected
$registry->describe(null); // every non-system-protected definition
$registry->get('account.updated')->visibleTo('webhook'); // true
With a system-protected definition registered under the same catalogue, names() and describe() omit it while has() / get() still succeed.