Skip to content

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 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():

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

Dispatched by core:

Event class Location When dispatched
StreamsChangedEvent Events/Stream/ Streams were created or changed (form, import, EPG, categories, servers, …)
StreamSavedEvent Events/Stream/ The stream form, an import or the admin API wrote live streams (carries module tab fields)
StreamsDeletedEvent Events/Stream/ Streams were deleted
StreamArgumentsChangedEvent Events/Stream/ Global stream argument defaults changed
TranscodeProfileSavedEvent Events/Stream/ A transcode profile was saved
BouquetDeletedEvent Events/Bouquet/ A bouquet was deleted
VodImportedEvent Events/Vod/ A VOD item was imported
MediaAnalyzedEvent Events/Vod/ A VOD file was analysed
ServerSavedEvent Events/Server/ A server was saved
ClusterLicenceLapsedEvent Events/Cluster/ xcvm_core refused a node's lease for want of a licence (the token went out without it); once per refused issue
SettingsChangedEvent Events/Settings/ Settings were saved
CrontabChangedEvent Events/Settings/ The crontab changed
PackageInstalledEvent Events/Module/ A marketplace package was installed

Defined but never dispatched

StreamStartingEvent, StreamStartedEvent, StreamStoppedEvent, ModuleLoadedEvent, ModuleBootedEvent, UserAuthenticatedEvent and UserLoggedOutEvent exist as classes, but nothing in core dispatches them. A listener on them never runs; do not build on them. The examples on this page use StreamStartedEvent only to show the API.


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:

EventDispatcher::dispatch(new MyModuleEvent($lineId, 'reason'));

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 event
  • priority — listener priority (higher = called first; default 0)
  • Placed on public methods of classes that extend BaseModule
  • IS_REPEATABLE — multiple #[ListensTo] on the same method are all registered
  • If eventClass does not exist at runtime, the attribute is skipped gracefully (no exception)
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)