# Event System XC_VM uses a PSR-14-style typed event dispatcher. All events are plain PHP classes dispatched and received by name. The dispatcher is instance-based and stored in the DI container under the key `events`. --- ## EventDispatcher `EventDispatcher` is a singleton with an instance bridge. Static methods delegate to the active instance so existing call sites work without changes. ```php // bootstrap.php wires the canonical instance: $dispatcher = new EventDispatcher(); EventDispatcher::setInstance($dispatcher); $container->set('events', $dispatcher); // Both paths reach the same listener store: EventDispatcher::dispatch(new MyEvent(...)); // static call $container->get('events')->dispatch(new MyEvent(...)); // instance call ``` In tests, isolate state per-test with: ```php protected function setUp(): void { $dispatcher = new EventDispatcher(); EventDispatcher::setInstance($dispatcher); } protected function tearDown(): void { EventDispatcher::resetInstance(); } ``` --- ## Dispatching and listening ```php // Dispatch EventDispatcher::dispatch(new StreamStartedEvent($lineId, $streamId)); // Listen EventDispatcher::listen(StreamStartedEvent::class, function (StreamStartedEvent $e): void { // handle }, priority: 10); // Remove a listener EventDispatcher::unlisten(StreamStartedEvent::class, $myCallable); // Check EventDispatcher::hasListeners(StreamStartedEvent::class); // bool ``` **Priority** — higher integer = called first. Default `0`. --- ## Registering listeners in a module ### Option 1 — getEventSubscribers() array ```php public function getEventSubscribers(): array { // One entry per event class (it is an array key). The value is either a // plain callable, or a [callable, int $priority] tuple (higher = called first). return [ StreamStartedEvent::class => [$this, 'onStreamStarted'], StreamStoppedEvent::class => [[$this, 'onStreamStopped'], 20], // with priority ]; } ``` > An event class may appear only once in this array. To attach **several** > listeners to the **same** event from one module, use the repeatable > `#[ListensTo]` attribute (Option 2) instead — `getEventSubscribers()` keeps a > single handler entry per event. ### Option 2 — #[ListensTo] attribute ```php use ListensTo; class MyModuleModule extends BaseModule { #[ListensTo(StreamStartedEvent::class, priority: 20)] public function onStreamStarted(StreamStartedEvent $e): void { // handle } // IS_REPEATABLE — multiple attributes on the same method #[ListensTo(StreamStartedEvent::class)] #[ListensTo(StreamStoppedEvent::class)] public function onStreamChange(object $e): void { // handle both events } } ``` Both mechanisms work simultaneously and can coexist in the same module. `ModuleLoader::bootAll()` runs both passes for every loaded module. > The examples above write `use ListensTo;` / `use AbstractEvent;` for brevity. The real classes are `XcVm\Core\Events\ListensTo` and `XcVm\Core\Events\AbstractEvent` — import those FQCNs (there is no global alias). --- ## Stoppable events Extend `AbstractEvent` and call `$e->stopPropagation()`: ```php class MyGatingEvent extends AbstractEvent { public bool $allowed = true; } EventDispatcher::listen(MyGatingEvent::class, function (MyGatingEvent $e): void { if (!$this->check()) { $e->allowed = false; $e->stopPropagation(); } }, priority: 100); ``` Listeners are skipped once `isPropagationStopped()` returns `true`. > **Listener errors are not caught.** `EventDispatcher::dispatch()` calls listeners in a plain loop with no `try/catch`, so if a listener throws, the exception propagates out of `dispatch()` and the remaining listeners for that event do **not** run. Keep listeners defensive (catch your own errors) if one failing subscriber must not abort the others. --- ## Built-in core events | Event class | Location | When dispatched | Stoppable | | ----------- | -------- | --------------- | :-------: | | `ModuleLoadedEvent` | `Events/Module/` | After module file is loaded | No | | `ModuleBootedEvent` | `Events/Module/` | After `boot()` is called | No | | `PackageInstalledEvent` | `Events/Module/` | After marketplace install | No | | `UserAuthenticatedEvent` | `Events/Auth/` | After successful login | Yes | | `UserLoggedOutEvent` | `Events/Auth/` | After logout | No | | `StreamStartingEvent` | `Events/Stream/` | Before a stream starts (gate — extends `AbstractEvent`) | Yes | | `StreamStartedEvent` | `Events/Stream/` | After stream started | No | | `StreamStoppedEvent` | `Events/Stream/` | After stream stopped | No | | `SettingsChangedEvent` | `Events/Settings/` | After settings saved | No | --- ## Writing a custom event Plain class — use `readonly` properties for immutable payloads: ```php