SIGNALS Documentation
API Reference

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() throws InvalidArgumentException with Event [{name}] is already registered. on collision
  • registerMany() calls register() 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) throws InvalidArgumentException with Unknown event [{name}]. when missing
  • has($name) is the optional guard
  • names(null) / describe(null) return every non-system-protected event
  • names('plugin') / describe('plugin') further require the matching visibility flag

Runtime consumption

Current framework consumers use the catalogue for discovery and recording:

  1. Consumer pickers and catalogues call names($consumer) / describe($consumer) so system-protected events stay hidden
  2. Recording paths call get() / has() without regard to visibility flags
  3. 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.