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.
// 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:
protected function setUp(): void {
$dispatcher = new EventDispatcher();
EventDispatcher::setInstance($dispatcher);
}
protected function tearDown(): void {
EventDispatcher::resetInstance();
}
Dispatching and listening¶
// 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¶
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¶
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 areXcVm\Core\Events\ListensToandXcVm\Core\Events\AbstractEvent— import those FQCNs (there is no global alias).
Stoppable events¶
Extend AbstractEvent and call $e->stopPropagation():
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 notry/catch, so if a listener throws, the exception propagates out ofdispatch()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
namespace XcVm\Module\MyModule;
final class MyModuleEvent {
public function __construct(
public readonly int $lineId,
public readonly string $reason,
) {}
}
Stoppable event — extend AbstractEvent:
<?php
namespace XcVm\Module\MyModule;
use AbstractEvent;
final class MyModuleGatingEvent extends AbstractEvent {
public bool $vetoed = false;
public function __construct(
public readonly int $resourceId,
) {}
}
Dispatch from anywhere after bootstrap:
ListensTo attribute reference¶
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class ListensTo {
public function __construct(
public readonly string $eventClass,
public readonly int $priority = 0,
) {}
}
eventClass— fully-qualified class name of the eventpriority— listener priority (higher = called first; default0)- Placed on public methods of classes that extend
BaseModule IS_REPEATABLE— multiple#[ListensTo]on the same method are all registered- If
eventClassdoes not exist at runtime, the attribute is skipped gracefully (no exception)
Related files¶
| File | Role |
|---|---|
src/Core/Events/EventDispatcher.php |
PSR-14 event dispatcher |
src/Core/Events/ListensTo.php |
Listener attribute |
src/Core/Events/ |
Event classes (Auth, Module, Settings, Stream) |